Plotly in nimib
I want to be able to show a plot by plotly in a nimib document. This will likely be integrated in nimib and is currently based on a 0.3 in dev version.
import plotly
import chroma
const colors = @[Color(r:0.9, g:0.4, b:0.0, a: 1.0),
Color(r:0.9, g:0.4, b:0.2, a: 1.0),
Color(r:0.2, g:0.9, b:0.2, a: 1.0),
Color(r:0.1, g:0.7, b:0.1, a: 1.0),
Color(r:0.0, g:0.5, b:0.1, a: 1.0)]
let
d = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
size = @[16.int]
d.marker = Marker[int](size: size, color: colors)
d.xs = @[1, 2, 3, 4, 5]
d.ys = @[1, 2, 1, 9, 5]
d.text = @["hello", "data-point", "third", "highest", "<b>bold</b>"]
let
layout = Layout(title: "testing", width: 1200, height: 400,
xaxis: Axis(title:"my x-axis"),
yaxis: Axis(title: "y-axis too"),
autosize: false)
p = Plot[int](layout: layout, traces: @[d])
echo p.save()
p.show()
C:\Users\PPETER~1\AppData\Local\Temp\nim_plotly\nim_plotly_2022-10-07_16-41-00.771.html
the above creates a temporary html file and opens it in the browser.
This is the source of the html file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>testing</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<div id="plot0"></div>
<script>
runRelayout = function() {
var margin = 50; // if 0, would introduce scrolling
Plotly.relayout('plot0', {width: window.innerWidth - margin, height: window.innerHeight - margin } );
};
window.onresize = runRelayout;
Plotly.newPlot('plot0', [{"x":[1,2,3,4,5],"y":[1,2,1,9,5],"mode":"lines+markers","type":"scatter","text":["hello","data-point","third","highest","<b>bold</b>"],"marker":{"size":16,"color":["#E56600","#E56633","#33E533","#19B219","#007F19"]}}], {"title":"testing","width":1200,"height":400,"xaxis":{"title":"my x-axis","autorange":true},"yaxis":{"title":"y-axis too","autorange":true},"hovermode":"closest"}).then(runRelayout);
</script>
</body>
</html>
Note that the above structure can be seen as plotly.tmpl_html.defaultTmplString.
(there is also a saveImage
that I will not bother with for the moment).
The goal is now to create a nbShow(p: plotly.Plot)
that will add the block and show it in the document.
We will need to:
- add
plotly
library in head section (this will be a newnbUsePlotly
template) - add a partial with the above structure of
div
andscript
, th only variable elements are:- an id for every new plot (e.g.
plot0
), a new feature in nimib 0.3 hasnb.newId
that will give us increasing integers - the serialized plot data and layout (lines that starts with
[{"x":[1,2,3,4,5],"y":[1,2,1,9,5],"mode": ...
),
- an id for every new plot (e.g.
Note that the way plotly produces the serialized plot is by calling the resizeScript
and the key line is:
Plotly.newPlot('plot0', $data, $layout).then(runRelayout);
(I did not even notice there were both data and layout in that line...)
resizeScript
is called only once elsewhere in plotly codebase (thanks github search!) and I adapt below the relevant lines
(again ignoring the imageInject
stuff)
proc fillHtmlTemplate(htmlTemplate,
data_string: string,
p: SomePlot,
filename = "",
autoResize = true): string =
var
slayout = "{}"
title = ""
if p.layout != nil:
when type(p) is Plot:
slayout = $(%p.layout)
title = p.layout.title
else:
slayout = $p.layout
title = p.layout{"title"}.getStr
...
let scriptTag = if autoResize: resizeScript()
else: staticScript()
let scriptFilled = scriptTag % [ "data", data_string,
"layout", slayout ]
...
and the last element we need is how data_string
is generated. Again I github search fillHtmlTemplate
and find only one usage in the same file where it is called by save
when type(p) is Plot:
# convert traces to data suitable for plotly and fill Html template
let data_string = parseTraces(p.traces)
else:
let data_string = $p.traces
it is also useful to have here an excerpt of plotly's type:
type
Plot*[T: SomeNumber] = ref object
traces* : seq[Trace[T]]
layout*: Layout
PlotJson* = ref object
traces* : JsonNode
layout*: JsonNode
SomePlot* = Plot | PlotJson
and I am guessing the imports needed to have $
for Trace
, Layout
and to have parseTraces
are included in:
import api, plotly_types, plotly_display
and now I have all I need to start coding:
template usePlotly(doc: var NbDoc) =
import plotly / [api, plotly_types, plotly_display]
# I should create a doc.addToHead proc for this:
doc.partials["head"] &= """<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>"""
doc.partials["nbPlotly"] = """
<div id="{{plot_id}}"></div>
<script>
Plotly.newPlot('{{plot_id}}', {{&plot_data}}, {{&plot_layout}});
</script>
"""
nb.usePlotly
template nbPlotly(plot, body: untyped) =
## plot must be the identifier of Plotly's plot in body
newNbCodeBlock("nbPlotly", body):
body
nb.blk.context["plot_id"] = "plot-" & $nb.newid
nb.blk.context["plot_data"] = block:
when type(p) is Plot:
parseTraces(p.traces)
else:
$p.traces
nb.blk.context["plot_layout"] = block:
if plot.layout != nil:
when type(plot) is Plot:
$(%plot.layout)
else:
$plot.layout
else:
"{}"
Note that above I removed the call to relayout (I am fixing the width to be smaller than in original example) and I am not currently showing the code that produces the plot below (see source code).