From fc62e88ebf3b0790bc8a10d8522be29f738f775d Mon Sep 17 00:00:00 2001 From: Vindaar Date: Thu, 31 Jan 2019 19:09:29 +0100 Subject: [PATCH 01/14] move impl of `plotly.nim` to `plotly_display` to make it importable --- src/plotly.nim | 168 +------------------------------- src/plotly/plotly_display.nim | 175 ++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 166 deletions(-) create mode 100644 src/plotly/plotly_display.nim diff --git a/src/plotly.nim b/src/plotly.nim index 22ca91f..6030278 100644 --- a/src/plotly.nim +++ b/src/plotly.nim @@ -12,170 +12,6 @@ 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 -else: - 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) + import plotly / plotly_display + export plotly_display diff --git a/src/plotly/plotly_display.nim b/src/plotly/plotly_display.nim new file mode 100644 index 0000000..4edb1f9 --- /dev/null +++ b/src/plotly/plotly_display.nim @@ -0,0 +1,175 @@ +import strutils +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 + +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 +else: + 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 `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) From fcc091406b0f43324f6e29501d48b6d5b494373a Mon Sep 17 00:00:00 2001 From: Vindaar Date: Thu, 31 Jan 2019 19:10:24 +0100 Subject: [PATCH 02/14] 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]`. --- examples/fig18_subplots.nim | 18 ++++++++ src/plotly/plotly_subplots.nim | 81 ++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/examples/fig18_subplots.nim b/examples/fig18_subplots.nim index 7327082..7e73ddf 100644 --- a/examples/fig18_subplots.nim +++ b/examples/fig18_subplots.nim @@ -101,3 +101,21 @@ 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() diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index ba1828c..1e06f6f 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -1,14 +1,20 @@ import json, macros, math -import plotly_types, plotly_sugar, api +import plotly_types, plotly_sugar, api, plotly_display type # subplot specific object, which stores intermediate information about # the grid layout to use for multiple plots - Grid = object + GridLayout = object useGrid: bool rows: int columns: int + Grid* = object + # layout of the plot itself + layout*: Layout + numPlotsPerRow*: int + plots: seq[PlotJson] + proc convertDomain*(d: Domain | DomainAlt): Domain = ## proc to get a `Domain` from either a `Domain` or `DomainAlt` tuple. ## That is a tuple of: @@ -48,7 +54,7 @@ proc calcRowsColumns(rows, columns: int, nPlots: int): (int, int) = else: result = (rows, columns) -proc assignGrid(plt: PlotJson, grid: Grid) = +proc assignGrid(plt: PlotJson, grid: GridLayout) = ## assigns the `grid` to the layout of `plt` ## If a grid is desired, but the user does not specify rows and columns, ## plots are aranged in a rectangular grid automatically. @@ -62,7 +68,7 @@ proc assignGrid(plt: PlotJson, grid: Grid) = proc combine(baseLayout: Layout, plts: openArray[PlotJson], domains: openArray[Domain], - grid: Grid): PlotJson = + grid: GridLayout): PlotJson = # we need to combine the plots on a JsonNode level to avoid problems with # different plot types! var res = newPlot() @@ -210,12 +216,12 @@ proc handleGrid(stmt: NimNode): NimNode = ## rows: 2 ## columns: 3 ## which is rewritten to an object constructor for a - ## `Grid` object storing the information. + ## `GridLayout` object storing the information. let gridIdent = ident"gridImpl" var gridVar = quote do: - var `gridIdent` = Grid() + var `gridIdent` = GridLayout() var gridObj = nnkObjConstr.newTree( - bindSym"Grid", + bindSym"GridLayout", nnkExprColonExpr.newTree( ident"useGrid", ident"true") @@ -271,7 +277,7 @@ macro subplots*(stmts: untyped): untyped = grid: NimNode let gridIdent = ident"gridImpl" grid = quote do: - var `gridIdent` = Grid(useGrid: false) + var `gridIdent` = GridLayout(useGrid: false) for stmt in stmts: case stmt.kind @@ -288,7 +294,7 @@ macro subplots*(stmts: untyped): untyped = case stmt.strVal of "grid": grid = quote do: - var `gridIdent` = Grid(useGrid: true) + var `gridIdent` = GridLayout(useGrid: true) else: error("Statement needs to be `baseLayout`, `plot`, `grid`! " & @@ -315,6 +321,63 @@ macro subplots*(stmts: untyped): untyped = `grid` combine(`layout`, `pltArray`, `domainArray`, `gridIdent`) +proc createGrid*(numPlots: int, numPlotsPerRow = 0, layout = Layout()): Grid = + ## creates a `Grid` object with `numPlots` to which one can assign plots + ## at runtime. Optionally the number of desired plots per row of the grid + ## may be given. If left empty, the grid will attempt to produce a square, + ## resorting to more columns than rows if not possible. + result = Grid(layout: layout, + numPlotsPerRow: numPlotsPerRow, + plots: newSeq[PlotJson](numPlots)) + +proc add*[T](grid: var Grid, plt: Plot[T]) = + ## add a new plot to the grid. Extends the number of plots stored in the + ## `Grid` by one. + ## NOTE: the given `Plot[T]` object is converted to a `PlotJson` object + ## upon assignment! + grid.plots.add plt.toPlotJson + +proc `[]=`*[T](grid: var Grid, idx: int, plt: Plot[T]) = + ## converts the given `Plot[T]` to a `PlotJson` and assigns to the given + ## index. + grid.plots[idx] = plt.toPlotJson + +proc `[]`*(grid: Grid, idx: int): PlotJson = + ## returns the plot at index `idx`. + ## NOTE: the plot is returned as a `PlotJson` object, not as the `Plot[T]` + ## originally put in! + result = grid.plots[idx] + +proc showImpl(grid: Grid): PlotJson = + ## helper proc containing the actual implementation that takes care of the + ## conversion of `Grid` to something we can plot + let + (rows, cols) = calcRowsColumns(rows = 0, + columns = grid.numPlotsPerRow, + nPlots = grid.plots.len) + gridLayout = GridLayout(useGrid: true, rows: rows, columns: cols) + result = combine(grid.layout, grid.plots, [], gridLayout) + +# show command for a `Grid` +when not hasThreadSupport and not defined(js): + when false: + # NOTE: for some weird reason this currently always activates the + # fatal pragma if not compiled with `--threads:on`, even if it's not + # being called somewhere + proc show*(grid: Grid, filename: string) = + {.fatal: "`filename` argument to `show` only supported if compiled " & + "with --threads:on!".} + + proc show*(grid: Grid) = + ## display the `Grid` plot. Converts the `grid` to a call to + ## `combine` and calls `show` on it. + grid.showImpl.show() +elif not defined(js): + proc show*(grid: Grid, filename = "") = + ## display the `Grid` plot. Converts the `grid` to a call to + ## `combine` and calls `show` on it. + grid.showImpl.show(filename) + when isMainModule: # test the calculation of rows and columns doAssert calcRowsColumns(2, 0, 4) == (2, 1) From a7cabcf9370cf3f3ab20596964301258bb2323d3 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sun, 17 Feb 2019 18:56:23 +0100 Subject: [PATCH 03/14] export `showImpl` plot to get `PlotJson` from grid --- src/plotly/plotly_subplots.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index 1e06f6f..a32632f 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -348,7 +348,7 @@ proc `[]`*(grid: Grid, idx: int): PlotJson = ## originally put in! result = grid.plots[idx] -proc showImpl(grid: Grid): PlotJson = +proc showImpl*(grid: Grid): PlotJson = ## helper proc containing the actual implementation that takes care of the ## conversion of `Grid` to something we can plot let From d4bb0162a4e382371ec895c2595b1409b5f9556e Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sun, 17 Feb 2019 18:56:36 +0100 Subject: [PATCH 04/14] fix JS backend --- src/plotly.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/plotly.nim b/src/plotly.nim index 6030278..f218bdc 100644 --- a/src/plotly.nim +++ b/src/plotly.nim @@ -15,3 +15,6 @@ export plotly_subplots when not defined(js): import plotly / plotly_display export plotly_display +else: + import plotly / plotly_js + export plotly_js From c53fcdcc11c264a068da72261746a08720b1cb44 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 18:49:05 +0100 Subject: [PATCH 05/14] 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)! --- src/plotly/plotly_subplots.nim | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index a32632f..c98aa88 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -79,28 +79,28 @@ proc combine(baseLayout: Layout, useGrid = true for i, p in plts: #doAssert p.traces.len == 1 - let trIdx = result.traces.len # first add traces of `*each Plot*`, only afterwards flatten them! - result.traces.add p.traces - # first plot needs to be treated differently than all others - let idx = result.traces.len - var - xaxisStr = "xaxis" - yaxisStr = "yaxis" - if i > 0: - xaxisStr &= $idx - yaxisStr &= $idx + if not p.isNil: + result.traces.add p.traces + # first plot needs to be treated differently than all others + let idx = result.traces.len + var + xaxisStr = "xaxis" + yaxisStr = "yaxis" + if i > 0: + xaxisStr &= $idx + yaxisStr &= $idx - result.layout[xaxisStr] = p.layout["xaxis"] - result.layout[yaxisStr] = p.layout["yaxis"] + result.layout[xaxisStr] = p.layout["xaxis"] + result.layout[yaxisStr] = p.layout["yaxis"] - if not useGrid: - result.assignDomain(xaxisStr, yaxisStr, domains[i]) + if not useGrid: + result.assignDomain(xaxisStr, yaxisStr, domains[i]) - if i > 0: - # anchor xaxis to y data and vice versa - result.layout[xaxisStr]["anchor"] = % ("y" & $idx) - result.layout[yaxisStr]["anchor"] = % ("x" & $idx) + if i > 0: + # anchor xaxis to y data and vice versa + result.layout[xaxisStr]["anchor"] = % ("y" & $idx) + result.layout[yaxisStr]["anchor"] = % ("x" & $idx) var i = 0 # flatten traces and set correct axis for correct original plots From c790be7f1faeeed4524c58fa5588b3cf279449d9 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 18:51:03 +0100 Subject: [PATCH 06/14] add bounds check for `[]=` at grid index --- src/plotly/plotly_subplots.nim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index c98aa88..35fed73 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -340,6 +340,10 @@ proc add*[T](grid: var Grid, plt: Plot[T]) = proc `[]=`*[T](grid: var Grid, idx: int, plt: Plot[T]) = ## converts the given `Plot[T]` to a `PlotJson` and assigns to the given ## index. + if idx > grid.plots.high: + raise newException(IndexError, "Index position " & $idx & " is out of " & + "bounds for grid with " & $grid.plots.len & " plots.") + grid.plots[idx] = plt.toPlotJson grid.plots[idx] = plt.toPlotJson proc `[]`*(grid: Grid, idx: int): PlotJson = From 6eeb7ef7983e0b6d8c518f11d5140af4c3aa0adb Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 18:54:40 +0100 Subject: [PATCH 07/14] 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". --- src/plotly/plotly_subplots.nim | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index 35fed73..e1a7c6a 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -38,13 +38,24 @@ proc assignDomain(plt: PlotJson, xaxis, yaxis: string, domain: Domain) = proc calcRowsColumns(rows, columns: int, nPlots: int): (int, int) = ## Calculates the desired rows and columns for # of `nPlots` given the user's - ## input for `rows` and `columns`. If no input is given, calculate the next - ## possible rectangle of plots that favors columns over rows - if rows == 0 and columns == 0: + ## input for `rows` and `columns`. + ## - If no input is given, calculate the next possible rectangle of plots + ## that favors columns over rows. + ## - If either row or column is 0, sets this dimension to 1 + ## - If either row or column is -1, calculate square of nPlots for rows / cols + ## - If both row and column is -1 or either -1 and the other 0, default back + ## to the next possible square. + if rows <= 0 and columns <= 0: # calc square of plots let sqPlt = sqrt(nPlots.float) result[1] = sqPlt.ceil.int result[0] = sqPlt.round.int + elif rows == -1 and columns > 0: + result[0] = (nPlots.float / columns.float).ceil.int + result[1] = columns + elif rows > 0 and columns == -1: + result[0] = rows + result[1] = (nPlots.float / rows.float).ceil.int elif rows == 0 and columns > 0: # 1 row, user desired # cols result = (1, columns) @@ -356,7 +367,7 @@ proc showImpl*(grid: Grid): PlotJson = ## helper proc containing the actual implementation that takes care of the ## conversion of `Grid` to something we can plot let - (rows, cols) = calcRowsColumns(rows = 0, + (rows, cols) = calcRowsColumns(rows = -1, columns = grid.numPlotsPerRow, nPlots = grid.plots.len) gridLayout = GridLayout(useGrid: true, rows: rows, columns: cols) @@ -396,3 +407,6 @@ when isMainModule: doAssert calcRowsColumns(0, 0, 7) == (3, 3) doAssert calcRowsColumns(0, 0, 8) == (3, 3) doAssert calcRowsColumns(0, 0, 9) == (3, 3) + doAssert calcRowsColumns(-1, 2, 4) == (2, 2) + doAssert calcRowsColumns(-1, 0, 4) == (2, 2) + doAssert calcRowsColumns(2, -1, 4) == (2, 2) From 4463cdafcf274b2caec58a86d177b8bad41ba52e Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 18:57:08 +0100 Subject: [PATCH 08/14] add creation and assignment of `Grid` based on row, col tuples --- examples/fig18_subplots.nim | 14 ++++++++++++++ src/plotly/plotly_subplots.nim | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/examples/fig18_subplots.nim b/examples/fig18_subplots.nim index 7e73ddf..66358f0 100644 --- a/examples/fig18_subplots.nim +++ b/examples/fig18_subplots.nim @@ -119,3 +119,17 @@ 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: 0, col: 3)] = 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() diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index e1a7c6a..d367544 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -337,10 +337,18 @@ proc createGrid*(numPlots: int, numPlotsPerRow = 0, layout = Layout()): Grid = ## at runtime. Optionally the number of desired plots per row of the grid ## may be given. If left empty, the grid will attempt to produce a square, ## resorting to more columns than rows if not possible. + ## Optionally a base layout can be given for the grid. result = Grid(layout: layout, numPlotsPerRow: numPlotsPerRow, plots: newSeq[PlotJson](numPlots)) +proc createGrid*(size: tuple[rows, cols: int], layout = Layout()): Grid = + ## creates a `Grid` object with `rows` x `cols` plots to which one can assign + ## plots at runtime. + ## Optionally a base layout can be given for the grid. + let nPlots = size.rows * size.cols + result = createGrid(nPlots, size.cols, layout) + proc add*[T](grid: var Grid, plt: Plot[T]) = ## add a new plot to the grid. Extends the number of plots stored in the ## `Grid` by one. @@ -355,6 +363,17 @@ proc `[]=`*[T](grid: var Grid, idx: int, plt: Plot[T]) = raise newException(IndexError, "Index position " & $idx & " is out of " & "bounds for grid with " & $grid.plots.len & " plots.") grid.plots[idx] = plt.toPlotJson + +proc `[]=`*[T](grid: var Grid, coord: tuple[row, col: int], plt: Plot[T]) = + ## converts the given `Plot[T]` to a `PlotJson` and assigns to specified + ## (row, column) coordinate of the grid. + let idx = grid.numPlotsPerRow * coord.row + coord.col + if coord.col > grid.numPlotsPerRow: + raise newException(IndexError, "Column " & $coord.col & " is out of " & + "bounds for grid with " & $grid.numPlotsPerRow & " columns!") + if idx > grid.plots.high: + raise newException(IndexError, "Position (" & $coord.row & ", " & $coord.col & + ") is out of bounds for grid with " & $grid.plots.len & " plots.") grid.plots[idx] = plt.toPlotJson proc `[]`*(grid: Grid, idx: int): PlotJson = @@ -363,6 +382,13 @@ proc `[]`*(grid: Grid, idx: int): PlotJson = ## originally put in! result = grid.plots[idx] +proc `[]`*(grid: Grid, coord: tuple[row, col: int]): PlotJson = + ## returns the plot at (row, column) coordinate `coord`. + ## NOTE: the plot is returned as a `PlotJson` object, not as the `Plot[T]` + ## originally put in! + let idx = grid.numPlotsPerRow * coord.row + coord.col + result = grid.plots[idx] + proc showImpl*(grid: Grid): PlotJson = ## helper proc containing the actual implementation that takes care of the ## conversion of `Grid` to something we can plot From 5cd526d92990c3d1495ca792735f64d3f7d08844 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 19:06:15 +0100 Subject: [PATCH 09/14] 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. --- src/plotly/plotly_display.nim | 285 +++++++++++++++++----------------- src/plotly/plotly_js.nim | 4 + 2 files changed, 143 insertions(+), 146 deletions(-) diff --git a/src/plotly/plotly_display.nim b/src/plotly/plotly_display.nim index 4edb1f9..2c464e8 100644 --- a/src/plotly/plotly_display.nim +++ b/src/plotly/plotly_display.nim @@ -1,4 +1,5 @@ import strutils +import os import json import sequtils @@ -9,40 +10,33 @@ import api, plotly_types, 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) +# 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 -else: - import plotly/plotly_js - export plotly_js +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 and not defined(js): +when hasThreadSupport: import threadpool import plotly/image_retrieve @@ -51,125 +45,124 @@ proc parseTraces*[T](traces: seq[Trace[T]]): string = ## 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: +# `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: - slayout = $(%p.layout) - title = p.layout.title + let swidth = $p.layout.width + let sheight = $p.layout.height 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) + 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: - 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!".} + 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: - # 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) + 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) diff --git a/src/plotly/plotly_js.nim b/src/plotly/plotly_js.nim index 4cc7659..492c646 100644 --- a/src/plotly/plotly_js.nim +++ b/src/plotly/plotly_js.nim @@ -17,3 +17,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) From 7726df715097c8f00dab25824697d2dff31146a0 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 19:07:25 +0100 Subject: [PATCH 10/14] actually run the subplots example with nimble test --- examples/all.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/all.nim b/examples/all.nim index ce1baaf..9f691d5 100644 --- a/examples/all.nim +++ b/examples/all.nim @@ -13,3 +13,4 @@ import fig14_autoBarWidth import fig15_horizontalBarPlot import fig16_plotly_sugar import fig17_color_font_legend +import fig18_subplots From 7527d137a10bbf1f5d7cbaf595e279627bd016da Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 19:14:04 +0100 Subject: [PATCH 11/14] fix assignment coord tuple in grid example using --- examples/fig18_subplots.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/fig18_subplots.nim b/examples/fig18_subplots.nim index 66358f0..17c6fac 100644 --- a/examples/fig18_subplots.nim +++ b/examples/fig18_subplots.nim @@ -126,7 +126,7 @@ var gridAlt = createGrid((rows: 2, cols: 2)) gridAlt[(0, 0)] = plt1 # or as named tuples gridAlt[(row: 0, col: 1)] = plt2 -gridAlt[(row: 0, col: 3)] = plt3 +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! From 857121a4a4597ac6a06a8ce36d6fa454aac4c427 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 18 Feb 2019 19:14:33 +0100 Subject: [PATCH 12/14] rename `showImpl` to `toPlotJson` If the proc is exported its name should better reflect what it actually does --- src/plotly/plotly_subplots.nim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index d367544..d5bfa03 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -389,9 +389,9 @@ proc `[]`*(grid: Grid, coord: tuple[row, col: int]): PlotJson = let idx = grid.numPlotsPerRow * coord.row + coord.col result = grid.plots[idx] -proc showImpl*(grid: Grid): PlotJson = - ## helper proc containing the actual implementation that takes care of the - ## conversion of `Grid` to something we can plot +proc toPlotJson*(grid: Grid): PlotJson = + ## converts the `Grid` object to a `PlotJson` object ready to be plotted + ## via the normal `show` procedure. let (rows, cols) = calcRowsColumns(rows = -1, columns = grid.numPlotsPerRow, @@ -412,12 +412,12 @@ when not hasThreadSupport and not defined(js): proc show*(grid: Grid) = ## display the `Grid` plot. Converts the `grid` to a call to ## `combine` and calls `show` on it. - grid.showImpl.show() + grid.toPlotJson.show() elif not defined(js): proc show*(grid: Grid, filename = "") = ## display the `Grid` plot. Converts the `grid` to a call to ## `combine` and calls `show` on it. - grid.showImpl.show(filename) + grid.toPlotJson.show(filename) when isMainModule: # test the calculation of rows and columns From c095080f6f0cb5a0d00dd1449af86003ec554377 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Fri, 1 Mar 2019 13:26:12 +0100 Subject: [PATCH 13/14] 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. --- src/plotly/plotly_subplots.nim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/plotly/plotly_subplots.nim b/src/plotly/plotly_subplots.nim index d5bfa03..e1840cf 100644 --- a/src/plotly/plotly_subplots.nim +++ b/src/plotly/plotly_subplots.nim @@ -1,5 +1,7 @@ import json, macros, math -import plotly_types, plotly_sugar, api, plotly_display +import plotly_types, plotly_sugar, api +when not defined(js): + from plotly_display import show type # subplot specific object, which stores intermediate information about @@ -400,6 +402,7 @@ proc toPlotJson*(grid: Grid): PlotJson = result = combine(grid.layout, grid.plots, [], gridLayout) # show command for a `Grid` +const hasThreadSupport = compileOption("threads") when not hasThreadSupport and not defined(js): when false: # NOTE: for some weird reason this currently always activates the From 9311dd4b1bcff857ac47b9fce82c922ee603d73c Mon Sep 17 00:00:00 2001 From: Vindaar Date: Fri, 1 Mar 2019 13:27:00 +0100 Subject: [PATCH 14/14] import types in plotly_js to get `parseTraces` working --- src/plotly/plotly_js.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plotly/plotly_js.nim b/src/plotly/plotly_js.nim index 492c646..454d707 100644 --- a/src/plotly/plotly_js.nim +++ b/src/plotly/plotly_js.nim @@ -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.