Mathematica as my tool of choice for early-stage prototyping and data exploration. Unlike many other language ecosystems, Mathematica comes with a lot built in, including many data visualization functions. However, plots are scattered throughout the landscape without a high-level system unifying them: every new chart lives in its own world, and there’s nothing resembling The Grammar of Graphics unifying them.
So I decided to see how much work it would take to create a more uniform and flexible plotting system like Plot, my current tool of choice for browser-based visualizations, in Mathematica. It’s been a few days and I’ve made some progress and have the following picture to show for it:
Which was made by this code:
plot
The library works along very similar lines to Observable Plot, which is the existing library I’m most familiar with, and directly inspired this effort.
The following was written in something of a hurry, and I plan to refine it later.
The basic idea is that a plot is a collection of marks overlaid atop each other. (This plot has one mark, dot
.)
A plot encodes meaning through visual channels, such as x or y or color. Each mark provides data to some number of channels. Based on this data, together with preferences provided by the user, the plot function comes up with an encoding for each channel that maps from the data domain to the visual domain (eg. taking a category label such as “a” and turning it into a color, or taking a numeric data value and turning it into a position along the x-axis.
The usability of the library depends strongly on the amount of deduction the library can make for you, and its ability to add explanatory marks to the plot, such as legends and axes, so that the resulting picture can be made sense of.
An interesting point that I had not sufficiently appreciated is the importance of representing scales in a way that allows you to make these explanatory marks. For example, the library needs to be able to tell when a log scale is used so that it can generate logarithmic axis ticks. Another example is that in Plot, the “shorthand” field specification syntax is what allows the library to know how to label your axes. A possibility in Mathematica is to use controlled evaluation for the same purpose.
This also raises some questions about evaluation order. I was hoping to use regular marks to build the axis legends, but had to figure out a solution to the time-ordering problem: Normal marks determine the set of inputs for each channel, from which scales for each channel are inferred, and ticks generated. But axis marks need to know the tick values. My current solution is to allow a mark to be a function which will be invoked with scale information. Any points plotted by such marks will not be used to determine the domain of the plot.
Another Plot design point that I hadn’t fully appreciated is that one reason it uses a z channel to allow eg. the line mark to make multiple lines is because sometimes one data column wants to be plotted as multiple curves, eg. with categorical time series, where you don’t want the user to need to reshape their data to calling your mark function.
My implementation so far is about 250 lines of code, and doesn’t yet include the ability to make bar charts, does not include faceting, or support for something like Plot’s z channel. I’m also not yet sure about the best way to support interactive selection, highlighting, and animated transitions. But you can use continuous, ordinal, and categorical encodings to plot text, dots, and lines, with scale and tick inference and a composable mark system.
We’ll see if my current design survives the addition of any of these new features. Part of the pleasure of using such a high-level programming language is that rewrites are cheap: when you know what you want to say, and if performance isn’t too much of a concern, you can express yourself very concisely.
Plot – My favorite plotting library, from the folks behind d3.
Wolfram Videos: ggplot: A Grammar of Graphics for the Wolfram Language – I came across this while writing this note. Haven’t watched it yet.
(* Names of all channels *)
= ;
(* Mapping from option names to channel names *)
= ;
(* Converts its argument to a list, if it isn't one already *)
:= If
(* "Broadcasts" scalar values in an association to lists, so the \
result is tabular in structure. *)
:=
AssociationThread
(* Normalization functions to rescale a domain to [0, 1] in the case \
of continuous and ordinal, or to [0, n] in the case of indexed *)
:= Rescale &
:=
AssociationThread
:=
indexed/Length - 1/
(* Map an association to another association: <| k->v |> -> <| \
k->f[k,v ]|> *)
(* This is a bit confusing, but have a look at its usage in mark \
render functions. *)
:=
AssociationMap
(* Applies the appropriate scale function to each option in assocs *)
:= kvMap
(* Returns a list of k->v where k is the channel name and v is a list \
of values *)
:=
mark // List // Prepend // KeyIntersection //
Values // Transpose // MapApply //
KeyDrop
(* Dot *)
:= Module
:= Association
Options[] = ;
(* Line *)
:= Module
:= Association
Options[] = ;
(* Text *)
:= Module
:= Association
Options[] = ;
(* Link *)
:= Module
Options[] = ;
:= Association
(* Axes are given as function marks, which accept the scales as an \
argument, and whose values do not inform the scale domains. *)
(* This solves the order-of-operations problem where the domain is \
inferred from marks, and wants to be "visualized" by axis marks. *)
(* We still need a way to signal to Plot to not add its own axes if \
the user provided some; maybe with some magic options. *)
:=
If&
:=
If&
(* Default color schemes *)
:=
Module
:=
Blend&
(* Scale inference fills in the subcomponents of a channel scale \
based on the channel, whether the values are numeric, and \
already-specified subcomponents. *)
(* Base case *)
:=
(* Default "out" for the radius channel *)
:=
(* Default domain for the "opacity" channel *)
inferScale:=
(* Default domain and normalization for channels with all numeric \
values *)
inferScale:=
(* Default domain and normalization for channels with nonnumeric \
values *)
inferScale:=
:=
(* Default continuous|ordinal color scheme *)
inferScale:=
(* Default indexed color scheme *)
inferScale:=
(* Default normalization is continuous *)
:=
(* Default out transform is Identity *)
inferScale:=
(* Tick inference figures out the ticks for a scale based on the \
properties of its subcomponents. *)
:= domain
:=
Range
:=
Range
(* Remove Null elements from a list, since it's a common mistake to \
call plot[mark1, mark2, ] with a trailing comma. *)
:= xs /. Null -> Sequence
(* The plot function accepts a list of marks, and returns a graphics \
object. *)
Options[] = ;
:= Module
= ExampleData;
= raw // Map;
plot