-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support subplot creation at runtime via
createGrid
(#45)
* move impl of `plotly.nim` to `plotly_display` to make it importable * add `createGrid` to create a grid subplot at runtime `createGrid` returns a `Grid` object for which `[]`, `[]=` and `add` are defined, so that the user may hand any `Plot[T]` object to the `Grid`. Internally all plots are stored in a `seq[PlotJson]`. * export `showImpl` plot to get `PlotJson` from grid * fix JS backend * only add plots, which have been assigned, i.e. not nil The result of this is we can have e.g. a 2x2 grid of plots and only assign 3 without a crash. However, if we only assign (0, 0), (0, 1), and (1, 1), the plot at (1, 1) will be shifted to (1, 0)! * add bounds check for `[]=` at grid index * fix calculation of grid size if numPlotsPerRow > 0 Previously `numPlotsPerRow` caused the calculation of the # of rows and columns to break, because we assumed `rows == 0` as a placeholder. Introduces `-1` as a special case for row or column, which means "infer from the other dimension + nPlots". * add creation and assignment of `Grid` based on row, col tuples * remove left over JS related code from plotly_display Since `parseTraces` is also useful on the JS backend, it was in addition added to `plotly_js`, since either putting it into `plotly_sugar` or creating a separate file for JS + C specific functions seemed unreasonable. * actually run the subplots example with nimble test * fix assignment coord tuple in grid example using * rename `showImpl` to `toPlotJson` If the proc is exported its name should better reflect what it actually does * only import display in subplots if not JS target `show` is only needed for the targets other than JS. `hasThreadSupport` is defined here again to avoid having to import it too from display. * import types in plotly_js to get `parseTraces` working
- Loading branch information
Showing
6 changed files
with
348 additions
and
193 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import strutils | ||
import os | ||
import json | ||
import sequtils | ||
|
||
# we now import the plotly modules and export them so that | ||
# the user sees them as a single module | ||
import api, plotly_types, plotly_subplots | ||
|
||
when defined(webview): | ||
import webview | ||
|
||
# normally just import browsers module. Howver, in case we run | ||
# tests on travis, we need a way to open a browser, which is | ||
# non-blocking. For some reason `xdg-open` does not return immediately | ||
# on travis. | ||
when not defined(travis): | ||
import browsers | ||
|
||
proc showPlot(file: string) = | ||
when defined(travis): | ||
# patched version of Nim's `openDefaultBrowser` which always | ||
# returns immediately | ||
var u = quoteShell(file) | ||
discard execShellCmd("xdg-open " & u & " &") | ||
elif defined(webview): | ||
let w = newWebView("Nim Plotly", "file://" & file) | ||
w.run() | ||
w.exit() | ||
else: | ||
# default normal browser | ||
openDefaultBrowser(file) | ||
|
||
include plotly/tmpl_html | ||
|
||
# check whether user is compiling with thread support. We can only compile | ||
# `saveImage` if the user compiles with it! | ||
const hasThreadSupport* = compileOption("threads") | ||
when hasThreadSupport: | ||
import threadpool | ||
import plotly/image_retrieve | ||
|
||
proc parseTraces*[T](traces: seq[Trace[T]]): string = | ||
## parses the traces of a Plot object to strings suitable for | ||
## plotly by creating a JsonNode and converting to string repr | ||
result.toUgly(% traces) | ||
|
||
# `show` and `save` are only used for the C target | ||
proc fillImageInjectTemplate(filetype, width, height: string): string = | ||
## fill the image injection code with the correct fields | ||
## Here we use numbering of elements to replace in the template. | ||
# Named replacements don't seem to work because of the characters | ||
# around the `$` calls | ||
result = injectImageCode % [filetype, | ||
filetype, | ||
width, | ||
height, | ||
filetype, | ||
width, | ||
height] | ||
|
||
proc fillHtmlTemplate(html_template, | ||
data_string: string, | ||
p: SomePlot, | ||
filename = ""): string = | ||
## fills the HTML template with the correct strings and, if compiled with | ||
## ``--threads:on``, inject the save image HTML code and fills that | ||
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 | ||
|
||
# read the HTML template and insert data, layout and title strings | ||
# imageInject is will be filled iff the user compiles with ``--threads:on`` | ||
# and a filename is given | ||
var imageInject = "" | ||
when hasThreadSupport: | ||
if filename.len > 0: | ||
# prepare save image code | ||
let filetype = parseImageType(filename) | ||
when type(p) is Plot: | ||
let swidth = $p.layout.width | ||
let sheight = $p.layout.height | ||
else: | ||
let swidth = $p.layout{"width"} | ||
let sheight = $p.layout{"height"} | ||
imageInject = fillImageInjectTemplate(filetype, swidth, sheight) | ||
|
||
# now fill all values into the html template | ||
result = html_template % ["data", data_string, "layout", slayout, | ||
"title", title, "saveImage", imageInject] | ||
|
||
proc save*(p: SomePlot, path = "", html_template = defaultTmplString, filename = ""): string = | ||
result = path | ||
if result == "": | ||
when defined(Windows): | ||
result = getEnv("TEMP") / "x.html" | ||
else: | ||
result = "/tmp/x.html" | ||
|
||
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 | ||
let html = html_template.fillHtmlTemplate(data_string, p, filename) | ||
|
||
var | ||
f: File | ||
if not open(f, result, fmWrite): | ||
quit "could not open file for json" | ||
f.write(html) | ||
f.close() | ||
|
||
when not hasThreadSupport: | ||
# some violation of DRY for the sake of better error messages at | ||
# compile time | ||
proc show*(p: SomePlot, | ||
filename: string, | ||
path = "", | ||
html_template = defaultTmplString) = | ||
{.fatal: "`filename` argument to `show` only supported if compiled " & | ||
"with --threads:on!".} | ||
|
||
proc show*(p: SomePlot, path = "", html_template = defaultTmplString) = | ||
## creates the temporary Html file using `save`, and opens the user's | ||
## default browser | ||
let tmpfile = p.save(path, html_template) | ||
|
||
showPlot(tmpfile) | ||
sleep(1000) | ||
## remove file after thread is finished | ||
removeFile(tmpfile) | ||
|
||
proc saveImage*(p: SomePlot, filename: string) = | ||
{.fatal: "`saveImage` only supported if compiled with --threads:on!".} | ||
|
||
else: | ||
# if compiled with --threads:on | ||
proc show*(p: SomePlot, filename = "", path = "", html_template = defaultTmplString) = | ||
## creates the temporary Html file using `save`, and opens the user's | ||
## default browser | ||
# if we are handed a filename, the user wants to save the file to disk. | ||
# Start a websocket server to receive the image data | ||
var thr: Thread[string] | ||
if filename.len > 0: | ||
# wait a short while to make sure the server is up and running | ||
thr.createThread(listenForImage, filename) | ||
|
||
let tmpfile = p.save(path, html_template, filename) | ||
showPlot(tmpfile) | ||
if filename.len > 0: | ||
# wait for thread to join | ||
thr.joinThread | ||
removeFile(tmpfile) | ||
|
||
proc saveImage*(p: SomePlot, filename: string) = | ||
## saves the image under the given filename | ||
## supported filetypes: | ||
## - jpg, png, svg, webp | ||
## Note: only supported if compiled with --threads:on! | ||
p.show(filename = filename) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.