Skip to content

Commit

Permalink
Support subplot creation at runtime via createGrid (#45)
Browse files Browse the repository at this point in the history
* 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
Vindaar authored and brentp committed Mar 3, 2019
1 parent eeef6a3 commit 3f1d4c1
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 193 deletions.
1 change: 1 addition & 0 deletions examples/all.nim
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ import fig14_autoBarWidth
import fig15_horizontalBarPlot
import fig16_plotly_sugar
import fig17_color_font_legend
import fig18_subplots
32 changes: 32 additions & 0 deletions examples/fig18_subplots.nim
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,35 @@ let pltC2 = subplots:
plot:
plt3
pltC2.show()

# Finally you may want to create a grid, to which you only add
# plots at a later time, potentially at runtime. Use `createGrid` for this.
# Note: internally the returned `Grid` object stores all plots already
# converted to `PlotJson` (i.e. the `layout` and `traces` fields are
# `JsonNodes`).
var grid = createGrid(numPlots = 2) #,
# allows to set the desired number of columns
# if not set will try to arange in a square
# numPlotsPerRow = 2,
# optionally set a layout for the plots
# layout = baseLayout)
# the returned grid has space for 2 plots.
grid[0] = plt1
grid[1] = plt2
# However, you may also extend the grid by using `add`
grid.add plt3
grid.show()

# alternatively define grid using rows and columns directly:
var gridAlt = createGrid((rows: 2, cols: 2))
# to which you can assign also in tuples
gridAlt[(0, 0)] = plt1
# or as named tuples
gridAlt[(row: 0, col: 1)] = plt2
gridAlt[(row: 1, col: 0)] = plt3
# Assigning the third plot in a 2x2 grid to coord (1, 1) moves it to (1, 0),
# i.e. the rows are always filled from left to right, if plots are missing!

# Note that the underlying `Grid` object is the same, so both can
# be used interchangeably.
gridAlt.show()
167 changes: 3 additions & 164 deletions src/plotly.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,170 +12,9 @@ export errorbar
export plotly_sugar
export plotly_subplots

when defined(webview):
import webview

when not defined(js):
# not available on JS backend
import os

# 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
import plotly / plotly_display
export plotly_display
else:
import plotly/plotly_js
import plotly / plotly_js
export plotly_js

# 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 and not defined(js):
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)

when not defined(js):
# `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 save plot 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)
168 changes: 168 additions & 0 deletions src/plotly/plotly_display.nim
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)
5 changes: 5 additions & 0 deletions src/plotly/plotly_js.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import jsbind
import jsffi
import dom
import plotly_types
# defines some functions and types used for the JS target. In this case
# we call the plotly.js functions directly.

Expand All @@ -17,3 +18,7 @@ proc restyle*(p: PlotlyObj; divname: cstring, update: JsObject) {.jsimport.}
# seems to behave differently
proc parseJsonToJs*(json: cstring): JsObject {.jsimportgWithName: "JSON.parse".}

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)
Loading

0 comments on commit 3f1d4c1

Please sign in to comment.