Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support subplot creation at runtime via createGrid #45

Merged
merged 14 commits into from
Mar 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) #,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer a rows, columns: int and a []/[]= that takes a row/column pair in order to better work with MxN subplots

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally agree with you. That's what I intuitively would expect when dealing with a 2D grid anyways. @timotheecour your opinion?
I'm fine either way though.

# 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