16 Creating plots with Plotly
author: Cvetomir Dimov
last change: 2025-04-14
Plotly is a popular data visualization library that can be accessed from multiple programming languages such as Javascript and Python. There is even a library that translates R’s ggplot specifications to Plotly. Relevant to Noj, it is one of the backends that the Tableplot library supports. With one of the subsequent tutorials, we will present how to work with Tableplot.
With this tutorial, we will present how to directly specify a Plotly plot from Clojure. Our task is greatly simplified by the fact that it is one of the data visualization kinds supported by Kindly. There are multiple example plots at Plotly JS’s website that can be translated to Clojure in a few simple steps. We will focus on a complex plot, a 2D histogram contour plot with histogram subplots, in order to demonstrate the full potential of this approach.
16.1 Setup
ns noj-book.plotly-tutorial
(:require [scicloj.kindly.v4.kind :as kind]
(:as fd]
[fitdistr.core :as fdd]
[fitdistr.distributions :as math])) [clojure.math
16.2 Overview
Our target plot consists of a 2D histogram contour plot, which visualize the bivariate distribution of two randomly generated variables. In addition, histogram subplots visualize the univariate distribution of each variable. In Plotly, each subplot is specified separately as a trace and then a layout specification tells Plotly how to arrange these subplots. Generating the plot consists of generating the random data, specifying all traces and layout, and then calling kind/plotly
.
16.3 Random number generation
Our two variables, x
and y
, and power functions of t
, which is uniformly distributed between -1 and 2.2.
def t
(->> (range 0 2001)
(map #(/ % 2000))
(map #(* % 2.2))
(map #(- % 1)))) (
Both x
and y
are generated by adding random noise drawn from a normal distribution with a mean of 0 and SD of 0.3. We define it with the distribution
function from the fitdistr
package.
def xy-distr (fd/distribution :normal {:mu 0 :sd 0.3})) (
x
is t
to the third power.
def x
(map +
(count t))
(fd/->seq xy-distr (map #(math/pow % 3) t))) (
y
is t
to the sixth power.
def y
(map +
(count t))
(fd/->seq xy-distr (map #(math/pow % 6) t))) (
16.4 Translating a Plotly specification to Clojure
Each component from a Plotly JS plot is defined in JSON. To translate it to Clojure, it needs to be transformed to a Clojure map. This is as simple removing the colon after the key and changing the key to a keyword. For example, the first trace, which specifies a scatterplot of the two variables, is defined as follows:
def trace1
(:x x,
{:y y,
:mode "markers",
:name "points",
:marker {
:color "rgb(102,0,0)",
:size 2,
:opacity 0.4
},:type "scatter"
;
} )
The second trace adds a histogram 2d contour of the same data.
def trace2
(:x x,
{:y y,
:name "density",
:ncontours 20,
:colorscale "Hot",
:reversescale true,
:showscale false,
:type "histogram2dcontour"
} )
The third and fourth traces specify the histograms of our two variables. Note that the :yaxis
of trace3
and the :xaxis
of trace4
are not the same as those of trace1
and trace2
. This is because we don’t want these histograms to overlap with the 2d contour plot.
def trace3
(:x x,
{:name "x density",
:marker {:color "rgb(102,0,0)"},
:yaxis "y2",
:type "histogram"
} )
def trace4
(:y y,
{:name "y density",
:marker {:color "rgb(102,0,0)"},
:xaxis "x2",
:type "histogram"
;
} )
The layout JSON specification is transformed to a Clojure map just as trivially. It defines two x-axis regions and two y-axis regions, which take 85% and 15% of each axis, respectively.
def layout
(:showlegend false,
{:autosize false,
:width 600,
:height 550,
:margin {:t 50},
:hovermode "closest",
:bargap 0,
:xaxis {:domain [0, 0.85],
:showgrid false,
:zeroline false
},:yaxis {:domain [0, 0.85],
:showgrid false,
:zeroline false
},:xaxis2 {:domain [0.85, 1],
:showgrid false,
:zeroline false
},:yaxis2 {:domain [0.85, 1],
:showgrid false,
:zeroline false
};
} )
16.5 Realizing the plot with kindly
All we need to do now is tell Clojure that this is of kind plotly
, where we specify in a map a vector of traces as our data and our layout.
:data [trace1, trace2, trace3, trace4]
(kind/plotly {:layout layout})