How to Create Art Styles in Julia
Overview
RecipesBase.jl
is a package that allows users to create their own styles for new plots, similar to how ggplot
works in the R programming language, with its own unique syntax1 separate from the base Julia. Let’s learn through examples.
Code
using Plots
using DataFrames
df = DataFrame(x = 1:10, y = rand(10))
plot(df)
@userplot TimeEvolution
@recipe function f(te::TimeEvolution)
df = te.args[1]
linealpha --> 0.5
column_names = names(df)
for (column_index, column_name) ∈ enumerate(column_names)
@series begin
label --> column_name
df[:,column_index]
end
end
end
timeevolution(df); png("1")
timeevolution(df, legend = :left); png("2")
First, running the above code results in the following error:
julia> df = DataFrame(x = 1:10, y = rand(10))
10×2 DataFrame
Row │ x y
│ Int64 Float64
─────┼──────────────────
1 │ 1 0.636532
2 │ 2 0.463764
3 │ 3 0.604559
4 │ 4 0.654089
5 │ 5 0.883409
6 │ 6 0.91667
7 │ 7 0.0609783
8 │ 8 0.602259
9 │ 9 0.460372
10 │ 10 0.479944
julia> plot(df)
ERROR: Cannot convert DataFrame to series data for plotting
This is because plot()
is not defined by default to draw pictures by taking a dataframe.
@userplot
and @recipe
@userplot TimeEvolution
@recipe function f(te::TimeEvolution)
...
end
In the example, we will draw a line chart directly with the time series data.
@userplot
: Specifies the name of the function that will inherit the properties of theplot()
function. Note that although case-sensitive here, the resulting function can only use lowercase.@recipe
: Specifies the style of the plot explicitly. The name of the function that follows is usually conventionallyf
.
f(te::TimeEvolution)
df = te.args[1]
linealpha --> 0.5
column_names = names(df)
for (column_index, column_name) ∈ enumerate(column_names)
@series begin
label --> column_name
df[:,column_index]
end
end
Let’s understand the above code line by line.
df = te.args[1]
It considers the first argument received as df
. In this example, since it’s assumed that the argument provided is a dataframe, the abbreviation df
is used. Keep in mind that f
is not the function we will be using, and its argument te
is also not directly utilized.
linealpha --> 0.5
The transparency of the lines to be drawn in this plot is set to 0.5. Note that this value is given with -->
, not linealpha = 0.5
, which is completely different from the common Julia syntax.
column_names = names(df)
for (column_index, column_name) ∈ enumerate(column_names)
...
end
Even if plot()
direct maps a 2D array to a plot, the labels are automatically given as y1
, y2
, etc. To prevent this, labels will be given separately by fetching the column names of the dataframe as shown above. The enumerate()
function is used to iterate through both the index and the column names simultaneously. For more details, see the explanation on enumerate()
.
@series
for ...
@series begin
label --> column_name
df[:,column_index]
end
end
The @series
macro specifies the data and its style that will be repeatedly drawn. The label of the column name is given with label --> column_name
, and by writing df[:,column_index]
in the last row, the data from that column name will be drawn. Note that the data to be drawn must be in the last row.
Results
As a result, we can now draw pictures in the style we specified with timeevolution()
or timeevolution!()
.
timeevolution(df)
It can be confirmed that even with a dataframe inserted, the picture is drawn successfully without error. The transparency of the line is 0.5, and the label is also exactly carried over from the column names in the dataframe.
timeevolution(df, legend = :left)
We have arbitrarily adjusted the position of the legend. Despite no mention of legend
when defining f
, it can be confirmed to apply well. This is because timeevolution()
inherits the rest of the elements of plot()
.
Environment
- OS: Windows
- julia: v1.6.3