├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── changelog.org
├── docs
├── plotly.html
└── plotly
│ ├── api.html
│ ├── color.html
│ ├── errorbar.html
│ └── plotly_types.html
├── examples
├── all.nim
├── fig10_candlestick.nim
├── fig11_histogram_settings.nim
├── fig12_save_figure.nim
├── fig13_contour.nim
├── fig14_autoBarWidth.nim
├── fig15_horizontalBarPlot.nim
├── fig16_plotly_sugar.nim
├── fig17_color_font_legend.nim
├── fig18_subplots.nim
├── fig19_log_histogram.nim
├── fig1_simple_scatter.nim
├── fig20_custom_cmaps.nim
├── fig21_error_band.nim
├── fig2_scatter_colors_sizes.nim
├── fig3_multiple_plot_types.nim
├── fig4_multiple_axes.nim
├── fig5_errorbar.nim
├── fig6_histogram.nim
├── fig7_stacked_histogram.nim
├── fig8_js_interactive.nim
├── fig9_heatmap.nim
├── index_javascript.html
└── nim.cfg
├── plotly.nimble
├── src
├── plotly.nim
└── plotly
│ ├── api.nim
│ ├── color.nim
│ ├── errorbar.nim
│ ├── image_retrieve.nim
│ ├── plotly_display.nim
│ ├── plotly_js.nim
│ ├── plotly_subplots.nim
│ ├── plotly_sugar.nim
│ ├── plotly_types.nim
│ ├── predefined_colormaps.nim
│ └── tmpl_html.nim
└── tests
├── config.nims
└── plotly
└── test_api.nim
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: plotly CI
2 | on:
3 | push:
4 | paths:
5 | - 'tests/**'
6 | - 'src/**'
7 | - 'plotly.nimble'
8 | - '.github/workflows/ci.yml'
9 | pull_request:
10 | paths:
11 | - 'tests/**'
12 | - 'src/**'
13 | - 'plotly.nimble'
14 | - '.github/workflows/ci.yml'
15 |
16 | jobs:
17 | build:
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | branch: [version-1-6, version-2-0, devel]
22 | target: [linux, macos, windows]
23 | include:
24 | - target: linux
25 | builder: ubuntu-latest
26 | - target: macos
27 | builder: macos-latest
28 | - target: windows
29 | builder: windows-latest
30 | name: '${{ matrix.target }} (${{ matrix.branch }})'
31 | runs-on: ${{ matrix.builder }}
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v2
35 | with:
36 | path: plotly
37 |
38 | - name: Setup Nim
39 | uses: alaviss/setup-nim@0.1.1
40 | with:
41 | path: nim
42 | version: ${{ matrix.branch }}
43 |
44 | - name: Install dependencies (Ubuntu)
45 | if: ${{matrix.target == 'linux'}}
46 | run: |
47 | sudo apt-get update
48 | sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev \
49 | at-spi2-core firefox
50 |
51 | - name: Install dependencies (OSX)
52 | if: ${{matrix.target == 'macos'}}
53 | run: |
54 | brew install firefox
55 |
56 | - name: Setup MSYS2 (Windows)
57 | if: ${{matrix.target == 'windows'}}
58 | uses: msys2/setup-msys2@v2
59 | with:
60 | path-type: inherit
61 | update: true
62 | install: base-devel git mingw-w64-x86_64-toolchain
63 |
64 | - name: Install dependencies (Windows)
65 | if: ${{matrix.target == 'windows'}}
66 | shell: msys2 {0}
67 | run: |
68 | pacman -Syu --noconfirm
69 | pacman -S --needed --noconfirm mingw-w64-x86_64-qtwebkit
70 |
71 | - name: Setup nimble & deps
72 | shell: bash
73 | run: |
74 | cd plotly
75 | nimble refresh -y
76 | nimble install -y
77 |
78 | - name: Run tests (Linux & Mac)
79 | if: ${{matrix.target != 'windows'}}
80 | shell: bash
81 | run: |
82 | cd plotly
83 | export BROWSER=firefox
84 | nimble -y testCINoSave
85 |
86 | - name: Run tests (Windows)
87 | if: ${{matrix.target == 'windows'}}
88 | shell: msys2 {0}
89 | run: |
90 | cd plotly
91 | export BROWSER=firefox
92 | nimble -y testCINoSave
93 |
94 | - name: Build docs
95 | if: >
96 | github.event_name == 'push' && github.ref == 'refs/heads/master' &&
97 | matrix.target == 'linux' && matrix.branch == 'devel'
98 | shell: bash
99 | run: |
100 | cd plotly
101 | branch=${{ github.ref }}
102 | branch=${branch##*/}
103 | nimble doc --project --path="." --outdir:docs \
104 | '--git.url:https://github.com/${{ github.repository }}' \
105 | '--git.commit:${{ github.sha }}' \
106 | "--git.devel:$branch" \
107 | src/plotly.nim
108 | # Ignore failures for older Nim
109 | cp docs/{the,}index.html || true
110 |
111 | - name: Publish docs
112 | if: >
113 | github.event_name == 'push' && github.ref == 'refs/heads/master' &&
114 | matrix.target == 'linux' && matrix.branch == 'devel'
115 | uses: crazy-max/ghaction-github-pages@v1
116 | with:
117 | build_dir: plotly/docs
118 | env:
119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | nimcache
2 | examples/all
3 | examples/fig1_simple_scatter
4 | examples/fig3_multiple_plot_types
5 | examples/fig4_multiple_axes
6 |
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Brent Pedersen - Bioinformatics
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## nim-plotly: simple plots in nim
2 |
3 | [](http://scinim.github.io/nim-plotly/)
4 | [](https://github.com/SciNim/nim-plotly/actions/workflows/ci.yml)
5 |
6 | This is a functioning plotting library. It supports, *line* (with fill below), *scatter* (with errors), *bar*
7 | , *histogram*, *heatmap*, *candlestick* and combinations of those plot types. More standard types can be added on request.
8 |
9 |
10 | This is **not** specifically for the javascript nim target (but the
11 | javascript target is supported!).
12 |
13 | Internally, it serializes typed `nim` datastructures to JSON that matches what [plotly](https://plot.ly/javascript/) expects.
14 |
15 | ## Examples
16 |
17 | See a collection of real-world examples in the [wiki](https://github.com/brentp/nim-plotly/wiki/Examples)
18 |
19 | #### Simple Scatter plot
20 |
21 | ```Nim
22 | import plotly
23 | import chroma
24 |
25 | var colors = @[Color(r:0.9, g:0.4, b:0.0, a: 1.0),
26 | Color(r:0.9, g:0.4, b:0.2, a: 1.0),
27 | Color(r:0.2, g:0.9, b:0.2, a: 1.0),
28 | Color(r:0.1, g:0.7, b:0.1, a: 1.0),
29 | Color(r:0.0, g:0.5, b:0.1, a: 1.0)]
30 | var d = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
31 | var size = @[16.int]
32 | d.marker =Marker[int](size:size, color: colors)
33 | d.xs = @[1, 2, 3, 4, 5]
34 | d.ys = @[1, 2, 1, 9, 5]
35 | d.text = @["hello", "data-point", "third", "highest", "bold"]
36 |
37 | var layout = Layout(title: "testing", width: 1200, height: 400,
38 | xaxis: Axis(title:"my x-axis"),
39 | yaxis:Axis(title: "y-axis too"), autosize:false)
40 | var p = Plot[int](layout:layout, traces: @[d])
41 | p.show()
42 | ```
43 |
44 | 
45 |
46 | The `show` call opens a browser pointing to a plot like above, but the actual plot will
47 | be interactive.
48 |
49 | #### Scatter with custom colors and sizes
50 |
51 | [source](https://github.com/brentp/nim-plotly/blob/master/examples/fig2_scatter_colors_sizes.nim)
52 |
53 | 
54 |
55 | #### Multiple plot types
56 |
57 | [source](https://github.com/brentp/nim-plotly/blob/master/examples/fig3_multiple_plot_types.nim)
58 |
59 | 
60 |
61 | #### Stacked Histogram
62 |
63 | [source](https://github.com/brentp/nim-plotly/blob/master/examples/fig7_stacked_histogram.nim)
64 |
65 | 
66 |
67 | #### Other examples
68 |
69 | [in examples](https://github.com/brentp/nim-plotly/blob/master/examples/)
70 |
71 |
72 | ## Note about C & JS targets / interactive plots
73 |
74 | The library supports both the `C` as well as `Javascript` targets of
75 | Nim. In case of the `C` target, the data and layout is statically
76 | parsed and inserted into a template Html file, which is stored in
77 | `/tmp/x.html`. A call to the default browser is made, which loads said
78 | file. The file is deleted thereafter.
79 |
80 | This static nature has the implication that it is not possible to
81 | update the data in the plots. However, thanks to Nim's ability to
82 | compile to Javascript, this can still be achieved if needed. When
83 | compiling to the `JS` target the native plotly functions are
84 | available, including `react` and `restyle`, which allow to change the
85 | data and / or layout of a plot defined in a `div` container. See the
86 | `fig8_js_interactive.nim` for such an example.
87 |
88 | ## Note about plotly under Windows Subsystem for Linux (WSL)
89 |
90 | Starting from version `v0.3.0` of plotly, WSL is supported. This
91 | requires the user to define the `BROWSER` environment variable and
92 | assumes the user wishes to use a normal Windows browser.
93 |
94 | When setting the `BROWSER` variable, make sure to handle the possible
95 | spaces (e.g. if browser installed in `Program Files`) by either
96 | escaping spaces and parenthesis with a backslash or just putting the
97 | whole path into quotation marks. E.g:
98 |
99 | ```sh
100 | export BROWSER="/mnt/c/Program Files (x86)/MyBrowserCompany/Browser.exe"
101 | ```
102 |
103 | to set the variable for the local session.
104 |
105 |
106 | ## TODO
107 |
108 | + [X] add .show() method to plot which looks for and opens a browser (similar to python webbrowser module)
109 | + [X] support multiple axes (2 y-axes supported).
110 | + [ ] experiment with syntax for multiple plots (https://plot.ly/javascript/subplots/ or use separate divs.)
111 | + [ ] better side-stepping of https://github.com/nim-lang/Nim/issues/7794
112 | + [ ] convert `%` procs into macros so I don't have to re-write the same code over and over.
113 | + [ ] more of plotly API
114 | + [ ] ergonomics / plotting DSL
115 | + [ ] custom interactivity.
116 |
--------------------------------------------------------------------------------
/changelog.org:
--------------------------------------------------------------------------------
1 | * v0.3.3
2 | - add ~hideLine~ for ~Trace~ object for scatter plots, in order to set
3 | the line width to 0. Used for backwards compatibility, because
4 | setting a value of 0 would be the default.
5 | - add ~FillMode~ value of ~toSelf~ to allow colored bands / filled
6 | polygons
7 | - add example for a manual error band
8 | * v0.3.2
9 | - fix behavior of =show= when =--threads:on= for =Grid= usage, in
10 | particular for example 18. =filename= argument is now optional,
11 | implying just viewing a plot when none given instead of saving.
12 | * v0.3.1
13 | - fix link to docs in the README
14 | * v0.3.0
15 | - =nim-plotly= now lives under the SciNim organization
16 | - adds option for auto resizing of plots (=autoResize= argument to
17 | =show/saveImage=); #72
18 | - temporary HTML files of plotly are now not removed by default
19 | (=removeTempFiles= argument to =show/saveImage=); #72
20 | - temporary HTML files are stored in a =nim_plotly= subdirectory and
21 | the file names are generated based on a timestamp; #72
22 |
--------------------------------------------------------------------------------
/docs/plotly/color.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Module color
20 |
1183 |
1184 |
1185 |
1186 |
1211 |
1212 |
1213 |
1214 |
1215 |
1216 |
Module color
1217 |
1218 |
1219 |
1223 |
1224 | Search:
1226 |
1227 |
1228 | Group by:
1229 |
1233 |
1234 |
1235 | -
1236 | Imports
1237 |
1240 |
1241 | -
1242 | Funcs
1243 |
1250 |
1251 |
1252 |
1253 |
1254 |
1255 |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 | chroma
1262 |
1263 |
1264 |
1265 |
1266 | func empty(c: Color): bool {.raises: [], tags: []
.}
1267 | -
1268 |
1269 |
1270 |
1271 | func toHtmlHex(colors: seq[Color]): seq[string] {.raises: [], tags: []
.}
1272 | -
1273 |
1274 |
1275 |
1276 |
1277 |
1278 |
1279 |
1280 |
1281 |
1282 |
1283 |
1284 |
1285 |
1286 | Made with Nim. Generated: 2018-05-29 07:34:57 UTC
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 |
1294 |
--------------------------------------------------------------------------------
/examples/all.nim:
--------------------------------------------------------------------------------
1 | import fig1_simple_scatter
2 | import fig2_scatter_colors_sizes
3 | import fig3_multiple_plot_types
4 | import fig4_multiple_axes
5 | import fig5_errorbar
6 | import fig6_histogram
7 | import fig7_stacked_histogram
8 | import fig9_heatmap
9 | import fig10_candlestick
10 | import fig11_histogram_settings
11 | import fig13_contour
12 | import fig14_autoBarWidth
13 | import fig15_horizontalBarPlot
14 | import fig16_plotly_sugar
15 | import fig17_color_font_legend
16 | import fig18_subplots
17 | import fig21_error_band
18 |
--------------------------------------------------------------------------------
/examples/fig10_candlestick.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import chroma
3 | import sequtils
4 |
5 | var d = Trace[float32](mode: PlotMode.Lines, `type`: PlotType.Candlestick)
6 |
7 | d.xs = @[1.0'f32, 2.0, 3.0, 4.0, 5.0]
8 | d.open = @[10.0'f32, 20.0, 10.0, 90.0, 50.0]
9 | d.low = @[7'f32, 15, 7, 90, 45]
10 | d.high = @[10'f32, 22, 10, 110, 55]
11 | d.close = @[7'f32, 22, 7, 105, 55]
12 |
13 |
14 | let
15 | layout = Layout(title: "Candlestick example", width: 800, height: 800,
16 | xaxis: Axis(title: "x-axis", rangeslider: RangeSlider(visible: false)),
17 | yaxis: Axis(title: "y-axis"), autosize: false)
18 | p = Plot[float32](layout: layout, traces: @[d])
19 | echo p.save()
20 | p.show()
21 |
--------------------------------------------------------------------------------
/examples/fig11_histogram_settings.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import math
3 | import sequtils
4 | import mersenne
5 |
6 | proc gauss(x, mean, sigma: float): float =
7 | # unsafe helper proc producing gaussian distribution
8 | let arg = (x - mean) / sigma
9 | result = exp(-0.5 * arg * arg) / sqrt(2 * PI)
10 |
11 | proc draw(samples: int): seq[float] =
12 | # create some gaussian data (not very efficient :))
13 | var random = newMersenneTwister(42)
14 | const
15 | mean = 0.5
16 | sigma = 0.1
17 | result = newSeqOfCap[float](samples)
18 | while result.len < samples:
19 | let
20 | r = random.getNum().float / uint32.high.float
21 | rejectProb = gauss(r, mean, sigma)
22 | if (random.getNum().float / uint32.high.float) < rejectProb:
23 | result.add r
24 |
25 | var data = draw(10_000)
26 |
27 |
28 | # The following simply showcases a few different ways to set different binning
29 | # ranges and sizes
30 | # NOTE: the `nBins` field of a histogram does not force that number of bins!
31 | # It is merely used as an input for plotly's autobinning algorithm. `nBins`
32 | # is the maximum number of allowed bins. But in some cases it might decide
33 | # that a few bins less visualize the data better. Plotly's description states:
34 | # "Specifies the maximum number of desired bins. This value will be used in
35 | # an algorithm that will decide the optimal bin size such that the histogram
36 | # best visualizes the distribution of the data."
37 | block:
38 | let
39 | d = Trace[float](`type`: PlotType.Histogram, cumulative: true,
40 | # set a range for the bins and a bin size
41 | bins: (0.0, 1.0), binSize: 0.01)
42 | d.xs = data
43 | let
44 | layout = Layout(title: "cumulative histogram in range (0.0 / 1.0) with custom bin size and range",
45 | width: 1200, height: 800,
46 | # set the range of the axis manually. If not, plotly may not show
47 | # empty bins in its range
48 | xaxis: Axis(title:"values", range: (0.0, 1.0)),
49 | yaxis: Axis(title: "counts"),
50 | autosize: false)
51 | p = Plot[float](layout: layout, traces: @[d])
52 | p.show()
53 |
54 | block:
55 | let
56 | d = Trace[float](`type`: PlotType.Histogram, cumulative: true,
57 | nBins: 100)
58 | d.xs = data
59 | let
60 | layout = Layout(title: "cumulative histogram in range (0.0 / 1.0) with specific max number of bins",
61 | width: 1200, height: 800,
62 | # set the range of the axis manually. If not, plotly may not show
63 | # empty bins in its range
64 | xaxis: Axis(title:"values", range: (0.0, 1.0)),
65 | yaxis: Axis(title: "counts"),
66 | autosize: false)
67 | p = Plot[float](layout: layout, traces: @[d])
68 | p.show()
69 |
70 | block:
71 | # here we only specify the number of bins, but not the bin range nor the axis
72 | # range. This may result in less than 50 bins (if some bins slightly wider bins
73 | # fit better according to plotly's algorithm). Additionally, the range of the
74 | # plot may be cut to bins which contain data.
75 | let
76 | d = Trace[float](`type`: PlotType.Histogram, nBins: 50)
77 | d.xs = data
78 | let
79 | layout = Layout(title: "histogram in automatic range with specific max number of bins",
80 | width: 1200, height: 800,
81 | xaxis: Axis(title:"values"),
82 | yaxis: Axis(title: "counts"),
83 | autosize: false)
84 | p = Plot[float](layout: layout, traces: @[d])
85 | p.show()
86 |
87 | block:
88 | # Setting the bin size and bin range manually. Without specifying the axis range
89 | # empty bins may still be discarded from the range.
90 | let
91 | d = Trace[float](`type`: PlotType.Histogram,
92 | bins: (0.0, 1.0), binSize: 0.05)
93 | d.xs = data
94 | let
95 | layout = Layout(title: "histogram in automatic range with specific bin range and size",
96 | width: 1200, height: 800,
97 | xaxis: Axis(title:"values"),
98 | yaxis: Axis(title: "counts"),
99 | autosize: false)
100 | p = Plot[float](layout: layout, traces: @[d])
101 | p.show()
102 |
--------------------------------------------------------------------------------
/examples/fig12_save_figure.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import chroma
3 |
4 | const colors = @[Color(r:0.9, g:0.4, b:0.0, a: 1.0),
5 | Color(r:0.9, g:0.4, b:0.2, a: 1.0),
6 | Color(r:0.2, g:0.9, b:0.2, a: 1.0),
7 | Color(r:0.1, g:0.7, b:0.1, a: 1.0),
8 | Color(r:0.0, g:0.5, b:0.1, a: 1.0)]
9 | let
10 | d = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
11 | size = @[16.int]
12 | d.marker = Marker[int](size: size, color: colors)
13 | d.xs = @[1, 2, 3, 4, 5]
14 | d.ys = @[1, 2, 1, 9, 5]
15 | d.text = @["hello", "data-point", "third", "highest", "bold"]
16 |
17 | let
18 | layout = Layout(title: "Saving a figure!", width: 1200, height: 400,
19 | xaxis: Axis(title:"my x-axis"),
20 | yaxis: Axis(title: "y-axis too"),
21 | autosize: false)
22 | p = Plot[int](layout: layout, traces: @[d])
23 | # now call the show proc with the `filename` argument to save the
24 | # file with the given filetype
25 | # p.show(filename = "HelloImage.png")
26 | # alternatively call the `saveImage` proc instead of show. If the webview target
27 | # is used, this will open a webview window, save the file and close the webview
28 | # window automatically.
29 | p.saveImage("HelloImage.svg")
30 | # NOTE: if we compile this without --threads:on support, we'll get
31 | # an error at compile time that thread support is needed.
32 |
--------------------------------------------------------------------------------
/examples/fig13_contour.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import sequtils
3 |
4 | let
5 | d = Trace[float32](`type`: PlotType.Contour)
6 |
7 | d.xs = @[-2.0, -1.5, -1.0, 0.0, 1.0, 1.5, 2.0, 2.5].mapIt(it.float32)
8 | d.ys = @[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6].mapIt(it.float32)
9 | # The data needs to be supplied as a nested seq.
10 | d.zs = @[@[2, 4, 7, 12, 13, 14, 15, 16],
11 | @[3, 1, 6, 11, 12, 13, 16, 17],
12 | @[4, 2, 7, 7, 11, 14, 17, 18],
13 | @[5, 3, 8, 8, 13, 15, 18, 19],
14 | @[7, 4, 10, 9, 16, 18, 20, 19],
15 | @[9, 10, 5, 27, 23, 21, 21, 21],
16 | @[11, 14, 17, 26, 25, 24, 23, 22]].mapIt(it.mapIt(it.float32))
17 |
18 | d.colorscale = ColorMap.Jet
19 | # d.heatmap = true # smooth colors
20 | # d.smoothing = 0.001 # rough lines
21 | # d.contours = (2.0, 26.0, 4.0)
22 |
23 | let
24 | layout = Layout(title: "Contour example", width: 600, height: 600,
25 | xaxis: Axis(title: "x-axis"),
26 | yaxis: Axis(title: "y-axis"), autosize: false)
27 | p = Plot[float32](layout: layout, traces: @[d])
28 |
29 | echo p.save()
30 | p.show()
31 |
--------------------------------------------------------------------------------
/examples/fig14_autoBarWidth.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import math, random
3 | import sequtils
4 |
5 | block:
6 | # `x` can be either the real bin edges (i.e. N + 1 bin edges for
7 | # N bins, last element being right edge of last bin)
8 | let x = @[0.0, 5.0, 10.0, 15.0]
9 | # or last right edge can be dropped. Last bins width will be assumed
10 | # to be same as before, if `autoWidth` is used.
11 | # let x = @[0.0, 5.0, 10.0]
12 | let y = @[5.0, 12.0, 3.3]
13 | let
14 | d = Trace[float](`type`: PlotType.Bar,
15 | xs: x,
16 | ys: y,
17 | align: BarAlign.Edge,
18 | autoWidth: true)
19 |
20 | let
21 | layout = Layout(title: "Bar plot with left aligned bins, width calculated automatically",
22 | width: 1200, height: 800,
23 | autosize: false)
24 | p = Plot[float](layout: layout, traces: @[d])
25 | p.show()
26 |
27 | block:
28 | # center aligned, automatical width calculation
29 | let
30 | x = @[0.0, 5.0, 10.0]
31 | y = @[5.0, 12.0, 3.3]
32 | d = Trace[float](`type`: PlotType.Bar,
33 | xs: x,
34 | ys: y,
35 | align: BarAlign.Center,
36 | autoWidth: true)
37 |
38 | let
39 | layout = Layout(title: "Bar plot with centered bins, width calculated automatically ",
40 | width: 1200, height: 800,
41 | autosize: false)
42 | p = Plot[float](layout: layout, traces: @[d])
43 | p.show()
44 |
45 | block:
46 | # hand sequence of widths, left aligned
47 | let
48 | x = @[0.0, 5.0, 10.0]
49 | y = @[5.0, 12.0, 3.3]
50 | widths = @[5.0, 5.0, 5.0]
51 | d = Trace[float](`type`: PlotType.Bar,
52 | xs: x,
53 | ys: y,
54 | widths: widths, # don't confuse with `width` field (no `s`)!
55 | align: BarAlign.Edge)
56 |
57 | let
58 | layout = Layout(title: "Bar plot with left aligned bins, manual sequence of bin widths",
59 | width: 1200, height: 800,
60 | autosize: false)
61 | p = Plot[float](layout: layout, traces: @[d])
62 | p.show()
63 |
64 | block:
65 | # hand scalar width, left aligned
66 | let
67 | x = @[0.0, 5.0, 10.0]
68 | y = @[5.0, 12.0, 3.3]
69 | width = 5.0
70 | d = Trace[float](`type`: PlotType.Bar,
71 | xs: x,
72 | ys: y,
73 | width: width, # don't confuse with `widths` (with `s`)!
74 | align: BarAlign.Edge)
75 |
76 | let
77 | layout = Layout(title: "Bar plot with left aligned bins, single manual bin width",
78 | width: 1200, height: 800,
79 | autosize: false)
80 | p = Plot[float](layout: layout, traces: @[d])
81 | p.show()
82 |
83 | block:
84 | # hand scalar width, center aligned
85 | let
86 | x = @[0.0, 5.0, 10.0]
87 | y = @[5.0, 12.0, 3.3]
88 | width = 5.0
89 | d = Trace[float](`type`: PlotType.Bar,
90 | xs: x,
91 | ys: y,
92 | width: width, # don't confuse with `widths` (with `s`)!
93 | align: BarAlign.Center)
94 |
95 | let
96 | layout = Layout(title: "Bar plot with centered bins, single manual bin width",
97 | width: 1200, height: 800,
98 | autosize: false)
99 | p = Plot[float](layout: layout, traces: @[d])
100 | p.show()
101 |
102 | block:
103 | # bar plot with default settings
104 | let
105 | x = @[0.0, 5.0, 10.0]
106 | y = @[5.0, 12.0, 3.3]
107 | d = Trace[float](`type`: PlotType.Bar,
108 | xs: x,
109 | ys: y)
110 |
111 | let
112 | layout = Layout(title: "Bar plot with default width and alignment",
113 | width: 1200, height: 800,
114 | autosize: false)
115 | p = Plot[float](layout: layout, traces: @[d])
116 | p.show()
117 |
118 | block:
119 | # example of non equal bin widths, calculated automatically
120 | randomize(42)
121 | let
122 | x = toSeq(0 ..< 50).mapIt(pow(it.float, 3.0))
123 | y = toSeq(0 ..< 50).mapIt(rand(100.0))
124 | d = Trace[float](`type`: PlotType.Bar,
125 | xs: x,
126 | ys: y,
127 | align: BarAlign.Edge,
128 | autoWidth: true)
129 |
130 | let
131 | layout = Layout(title: "Bar plot unequal bin widths, automatically calculated",
132 | width: 1200, height: 800,
133 | autosize: false)
134 | p = Plot[float](layout: layout, traces: @[d])
135 | p.show()
136 |
--------------------------------------------------------------------------------
/examples/fig15_horizontalBarPlot.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import random
3 | import sequtils
4 |
5 | randomize(42)
6 | let
7 | x = toSeq(0 ..< 50)
8 | y = toSeq(0 ..< 50).mapIt(rand(50))
9 | d = Trace[int](`type`: PlotType.Bar,
10 | xs: x,
11 | ys: y,
12 | orientation: Orientation.Horizontal)
13 |
14 | let
15 | layout = Layout(title: "Horizontal bar plot",
16 | width: 1200, height: 800,
17 | autosize: false)
18 | p = Plot[int](layout: layout, traces: @[d])
19 | p.show()
20 |
--------------------------------------------------------------------------------
/examples/fig16_plotly_sugar.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import plotly / plotly_sugar
3 | import math
4 | import sequtils
5 | import random
6 |
7 | block:
8 | # example of a scatter plot with the markers in color
9 | # of the `y` value
10 | const
11 | n = 70
12 |
13 | var
14 | y = newSeq[float64](n)
15 | x = newSeq[float64](n)
16 | text = newSeq[string](n)
17 | sizes = newSeq[float64](n)
18 | for i in 0 .. y.high:
19 | x[i] = i.float
20 | y[i] = sin(i.float)
21 | text[i] = $i & " has the sin value: " & $y[i]
22 | sizes[i] = float64(10 + (i mod 10))
23 |
24 | scatterColor(x, y, y)
25 | .mode(PlotMode.LinesMarkers)
26 | .markersize(15)
27 | .show()
28 |
29 | block:
30 | # example of a heatmap from seq[seq[float]]
31 | var zs = newSeqWith(28, newSeq[float32](28))
32 | for x in 0 ..< 28:
33 | for y in 0 ..< 28:
34 | zs[x][y] = rand(1.0)
35 | heatmap(zs)
36 | .xlabel("Some x label!")
37 | .ylabel("Some y label too!")
38 | .show()
39 |
40 | block:
41 | # example of a heatmap from x, y, z: seq[T]
42 | var
43 | xs = newSeq[float](28 * 28)
44 | ys = newSeq[float](28 * 28)
45 | zs = newSeq[float](28 * 28)
46 | for i in 0 .. xs.high:
47 | xs[i] = rand(27.0)
48 | ys[i] = rand(27.0)
49 | zs[i] = rand(1.0)
50 | heatmap(xs, ys, zs)
51 | .xlabel("Some x label!")
52 | .ylabel("Some y label too!")
53 | .show()
54 |
55 | block:
56 | var hist: seq[int]
57 | for i in 0 .. 1000:
58 | hist.add rand(25)
59 | histPlot(hist)
60 | .binSize(2.0)
61 | .show()
62 |
63 | block:
64 | var bars = newSeq[int](100)
65 | var counts = newSeq[int](100)
66 | for i in 0 .. 25:
67 | bars[i] = i * 4
68 | counts[i] = rand(100)
69 | barPlot(bars, counts).show()
70 |
71 | block:
72 | var bars = newSeq[string](10)
73 | var counts = newSeq[int](10)
74 | var i = 0
75 | for x in {'a' .. 'j'}:
76 | bars[i] = $x
77 | counts[i] = rand(100)
78 | inc i
79 |
80 | barPlot(bars, counts)
81 | .title("Some char label bar plot")
82 | .show()
83 |
--------------------------------------------------------------------------------
/examples/fig17_color_font_legend.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import math
3 | import chroma
4 |
5 |
6 | const
7 | n = 70
8 |
9 | var
10 | y = newSeq[float64](n)
11 | x = newSeq[float64](n)
12 | y2 = newSeq[float64](n)
13 | x2 = newSeq[float64](n)
14 | sizes = newSeq[float64](n)
15 | for i in 0 .. y.high:
16 | x[i] = i.float
17 | y[i] = sin(i.float)
18 | x2[i] = (i.float + 0.5)
19 | y2[i] = i.float * 0.1
20 | sizes[i] = float64(10 + (i mod 10))
21 |
22 | let d = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
23 | xs: x, ys: y, lineWidth: 10)
24 | let d2 = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
25 | xs: x2, ys: y2)
26 | d.marker = Marker[float64](size: sizes)
27 |
28 | let legend = Legend(x: 0.1,
29 | y: 0.9,
30 | backgroundColor: color(0.6, 0.6, 0.6),
31 | orientation: Vertical,
32 | font: Font(color: color(0, 0, 0))
33 | )
34 |
35 | let layout = Layout(title: "saw the sin", width: 800, height: 600,
36 | xaxis: Axis(title: "my x-axis"),
37 | yaxis: Axis(title: "y-axis too"),
38 | autosize: false,
39 | backgroundColor: color(0.92, 0.92, 0.92),
40 | legend: legend,
41 | showLegend: true
42 | )
43 |
44 | Plot[float64](layout: layout, traces: @[d, d2]).show()
45 |
46 | # alternatively using plotly_sugar
47 | # scatterPlot(x, y)
48 | # .addTrace(scatterTrace(x2, y2))
49 | # .mode(LinesMarkers)
50 | # .mode(LinesMarkers, idx = 1)
51 | # .markerSizes(sizes)
52 | # .legend(legend)
53 | # .lineWidth(10, idx = 0)
54 | # .xlabel("my x-axis")
55 | # .ylabel("y-axis too")
56 | # .backgroundColor(color(0.92, 0.92, 0.92))
57 | # .width(800)
58 | # .height(600)
59 | # .show()
60 |
--------------------------------------------------------------------------------
/examples/fig18_subplots.nim:
--------------------------------------------------------------------------------
1 | import plotly, sequtils, macros, algorithm
2 | import json
3 | import math
4 | import chroma
5 | import strformat
6 |
7 | # given some data
8 | const
9 | n = 5
10 | var
11 | y = new_seq[float64](n)
12 | x = new_seq[float64](n)
13 | x2 = newSeq[int](n)
14 | y2 = newSeq[int](n)
15 | x3 = newSeq[int](n)
16 | y3 = newSeq[int](n)
17 | sizes = new_seq[float64](n)
18 | for i in 0 .. y.high:
19 | x[i] = i.float
20 | y[i] = sin(i.float)
21 | x2[i] = i
22 | y2[i] = i * 5
23 | x3[i] = i
24 | y3[i] = -(i * 5)
25 | sizes[i] = float64(10 + (i mod 10))
26 |
27 | # and with it defined plots of possibly different datatypes (note `float` and `int`)
28 | let d = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
29 | xs: x, ys: y)
30 | let d2 = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
31 | xs: x2, ys: y2)
32 | let d3 = Trace[float](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
33 | xs: x2.mapIt(it.float), ys: y2.mapIt(it.float))
34 | let d4 = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
35 | xs: x3, ys: y3)
36 |
37 | let layout = Layout(title: "saw the sin, colors of sin value!", width: 1000, height: 400,
38 | xaxis: Axis(title: "my x-axis"),
39 | yaxis: Axis(title: "y-axis too"), autosize: false)
40 |
41 | let baseLayout = Layout(title: "A bunch of subplots!", width: 800, height: 800,
42 | xaxis: Axis(title: "linear x"),
43 | yaxis: Axis(title: "y also linear"), autosize: false)
44 |
45 | let plt1 = Plot[float64](layout: layout, traces: @[d, d3])
46 | let plt2 = Plot[int](layout: baseLayout, traces: @[d2, d4])
47 | let plt3 = scatterPlot(x3, y3).title("Another plot!").width(1000)
48 |
49 | # we wish to create a subplot including all three plots. The `subplots` macro
50 | # returns a special `PlotJson` object, which stores the same information as
51 | # a `Plot[T]` object, but already converted to `JsonNodes`. This is done for easier
52 | # handling of different data types. But fear not, this object is given straight to
53 | # `show` or `saveImage` unless you wish to manually add something to the `JsonNodes`.
54 | let pltCombined = subplots:
55 | # first we need to define a base layout for our plot, which defines size
56 | # of canvas and other applicable properties
57 | baseLayout: baseLayout
58 | # now we define all plots in `plot` blocks
59 | plot:
60 | # the first identifier points to a `Plot[T]` object
61 | plt1
62 | # it follows the description of the `Domain`, i.e. the location and
63 | # size of the subplot. This can be done explicitly as follows:
64 | # Note that the order of the fields is not important, but you need to
65 | # define all 4!
66 | left: 0.0
67 | bottom: 0.0
68 | width: 0.45
69 | height: 1.0
70 | plot:
71 | plt2
72 | # alternatively a nameless tuple conforming to the order
73 | (0.6, 0.5, 0.4, 0.5)
74 | plot:
75 | plt3
76 | # or instead of defining via `:`, you can use `=`
77 | left = 0.7
78 | bottom = 0.0
79 | # and also replace `widht` and `height` by the right and top edge of the plot
80 | # NOTE: you *cannot* mix e.g. right with height!
81 | right = 1.0
82 | top = 0.3
83 | pltCombined.show()
84 |
85 | # if you do not wish to define domains for each plot, you also simply define
86 | # grid as we do here
87 | let pltC2 = subplots:
88 | baseLayout: baseLayout
89 | # this requires the `grid` block
90 | grid:
91 | # it may contain a `rows` and `column` field, although both are optional
92 | # If only one is set, the other will be set to 1. If neither is set,
93 | # nor any domains on the plots, a grid will be calculated automatically,
94 | # favoring more columns than rows.
95 | rows: 3
96 | columns: 1
97 | plot:
98 | plt1
99 | plot:
100 | plt2
101 | plot:
102 | plt3
103 | pltC2.show()
104 |
105 | # Finally you may want to create a grid, to which you only add
106 | # plots at a later time, potentially at runtime. Use `createGrid` for this.
107 | # Note: internally the returned `Grid` object stores all plots already
108 | # converted to `PlotJson` (i.e. the `layout` and `traces` fields are
109 | # `JsonNodes`).
110 | var grid = createGrid(numPlots = 2) #,
111 | # allows to set the desired number of columns
112 | # if not set will try to arange in a square
113 | # numPlotsPerRow = 2,
114 | # optionally set a layout for the plots
115 | # layout = baseLayout)
116 | # the returned grid has space for 2 plots.
117 | grid[0] = plt1
118 | grid[1] = plt2
119 | # However, you may also extend the grid by using `add`
120 | grid.add plt3
121 | grid.show()
122 |
123 | # alternatively define grid using rows and columns directly:
124 | var gridAlt = createGrid((rows: 2, cols: 2))
125 | # to which you can assign also in tuples
126 | gridAlt[(0, 0)] = plt1
127 | # or as named tuples
128 | gridAlt[(row: 0, col: 1)] = plt2
129 | gridAlt[(row: 1, col: 0)] = plt3
130 | # Assigning the third plot in a 2x2 grid to coord (1, 1) moves it to (1, 0),
131 | # i.e. the rows are always filled from left to right, if plots are missing!
132 |
133 | # Note that the underlying `Grid` object is the same, so both can
134 | # be used interchangeably.
135 | gridAlt.show()
136 |
--------------------------------------------------------------------------------
/examples/fig19_log_histogram.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import random
3 | let
4 | d = Trace[int](`type`: PlotType.Histogram)
5 |
6 | # using ys will make a horizontal bar plot
7 | # using xs will make a vertical.
8 | d.ys = newSeq[int](200)
9 |
10 | for i, x in d.ys:
11 | d.ys[i] = rand(20)
12 |
13 | for i in 0..40:
14 | d.ys[i] = 12
15 |
16 | # `ty: "log"` on an axis makes it log scale
17 | let
18 | layout = Layout(title: "histogram", width: 1200, height: 400,
19 | xaxis: Axis(title:"frequency", ty: AxisType.Log),
20 | yaxis: Axis(title: "values"),
21 | autosize: false)
22 | p = Plot[int](layout: layout, traces: @[d])
23 | p.show()
24 |
--------------------------------------------------------------------------------
/examples/fig1_simple_scatter.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import chroma
3 |
4 | const colors = @[Color(r:0.9, g:0.4, b:0.0, a: 1.0),
5 | Color(r:0.9, g:0.4, b:0.2, a: 1.0),
6 | Color(r:0.2, g:0.9, b:0.2, a: 1.0),
7 | Color(r:0.1, g:0.7, b:0.1, a: 1.0),
8 | Color(r:0.0, g:0.5, b:0.1, a: 1.0)]
9 | let
10 | d = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
11 | size = @[16.int]
12 | d.marker = Marker[int](size: size, color: colors)
13 | d.xs = @[1, 2, 3, 4, 5]
14 | d.ys = @[1, 2, 1, 9, 5]
15 | d.text = @["hello", "data-point", "third", "highest", "bold"]
16 |
17 | let
18 | layout = Layout(title: "testing", width: 1200, height: 400,
19 | xaxis: Axis(title:"my x-axis"),
20 | yaxis: Axis(title: "y-axis too"),
21 | autosize: false)
22 | p = Plot[int](layout: layout, traces: @[d])
23 | echo p.save()
24 | p.show()
25 |
--------------------------------------------------------------------------------
/examples/fig20_custom_cmaps.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import plotly / color
3 | import chroma
4 | import random
5 | import sequtils
6 |
7 | var data = newSeqWith(28, newSeq[float32](28))
8 | for x in 0 ..< 28:
9 | for y in 0 ..< 28:
10 | data[x][y] = max(rand(1.0), 0.3)
11 |
12 | let randomCustomMap = @[
13 | (r: 0.9719701409339905, g: 0.463617742061615, b: 0.4272273480892181),
14 | (r: 0.638210654258728, g: 0.6486857533454895, b: 0.0),
15 | (r: 0.0, g: 0.7498401999473572, b: 0.4914137721061707),
16 | (r: 0.0, g: 0.6900160312652588, b: 0.9665122032165527),
17 | (r: 0.9064756631851196, g: 0.4206041693687439, b: 0.9523735642433167)
18 | ]
19 |
20 | block:
21 | let d = Trace[float32](mode: PlotMode.Lines, `type`: PlotType.HeatMap)
22 | # generate some random data
23 | d.zs = data
24 | proc customHeatmap(name: PredefinedCustomMaps) =
25 | # use `getCustomMap` to get one of the predefined colormaps and assign
26 | # it to the `customCmap` field of the `Trace`
27 | d.customColormap = getCustomMap(name)
28 | # for the custom map to have any effect, we have to choose the
29 | # `Custom` value for the `colorMap` field.
30 | d.colorMap = Custom
31 | let
32 | layout = Layout(title: $name, width: 800, height: 800,
33 | xaxis: Axis(title: "x"),
34 | yaxis: Axis(title: "y"), autosize: false)
35 | p = Plot[float32](layout: layout, traces: @[d])
36 | p.show()
37 |
38 | for map in PredefinedCustomMaps:
39 | customHeatmap(map)
40 |
41 | # and now a fully custom map
42 | d.customColormap = CustomColorMap(rawColors: randomCustomMap)
43 | # for the custom map to have any effect, we have to choose the
44 | # `Custom` value for the `colorMap` field.
45 | d.colorMap = Custom
46 | let
47 | layout = Layout(title: "fully custom", width: 800, height: 800,
48 | xaxis: Axis(title: "x"),
49 | yaxis: Axis(title: "y"), autosize: false)
50 | p = Plot[float32](layout: layout, traces: @[d])
51 | p.show()
52 |
53 | block:
54 | # using plotly_sugar
55 | proc customHeatmap(name: PredefinedCustomMaps) =
56 | heatmap(data)
57 | .title($name & " using sugar")
58 | # colormap takes one of:
59 | # - `ColorMap: enum`
60 | # - `PredefinedCustomMaps: enum`
61 | # - `CustomColorMap: ref object`
62 | # - `colormapData: seq[tuple[r, g, b: float64]]`
63 | .colormap(name)
64 | .show()
65 |
66 | for map in PredefinedCustomMaps:
67 | customHeatmap(map)
68 |
69 | heatmap(data)
70 | .title("fully custom using sugar")
71 | .colormap(randomCustomMap)
72 | .show()
73 |
--------------------------------------------------------------------------------
/examples/fig21_error_band.nim:
--------------------------------------------------------------------------------
1 | import sequtils
2 | import plotly
3 | import chroma
4 | from std / algorithm import reversed
5 |
6 | # simple example showcasing error bands (by hand)
7 |
8 | let
9 | d = Trace[float](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
10 | size = @[16.float]
11 | d.marker = Marker[float](size: size)
12 | d.xs = @[1'f64, 2, 3, 4, 5]
13 | d.ys = @[1'f64, 2, 1, 9, 5]
14 |
15 | # Create a Trace for the error band
16 | let
17 | dBand = Trace[float](mode: PlotMode.Lines, `type`: PlotType.Scatter,
18 | opacity: 0.75, # opacity 75% to be prettier
19 | fill: ToSelf, # `ToSelf` means the filling is done to its own data
20 | hideLine: true) # line width 0 disables the outline
21 | # Create X data that is first increasing and then decreasing
22 | dBand.xs = concat(d.xs, d.xs.reversed)
23 | # Assign the actual ribbon band. Currently needs to be a seq
24 | dBand.marker = Marker[float](color: @[color(0.6, 0.6, 0.6)])
25 |
26 | # define some errors we will use (symmetric)
27 | let yErr = d.ys.mapIt(0.25)
28 | # now create the first upper band range
29 | var yErrs = newSeqOfCap[float](d.ys.len * 2) # first upper, then lower
30 | for i in 0 ..< d.ys.len: # upper errors
31 | yErrs.add(d.ys[i] + yErr[i])
32 | # and now the lower
33 | for i in countdown(d.ys.high, 0): # lower errors
34 | yErrs.add(d.ys[i] - yErr[i])
35 | dBand.ys = yErrs
36 |
37 | let
38 | layout = Layout(title: "testing", width: 1200, height: 400,
39 | xaxis: Axis(title: "my x-axis"),
40 | yaxis: Axis(title: "y-axis too"), autosize: false)
41 | p = Plot[float](layout: layout, traces: @[d, dBand]) # assign both traces
42 | echo p.save()
43 | p.show()
44 |
--------------------------------------------------------------------------------
/examples/fig2_scatter_colors_sizes.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import math
3 | import chroma
4 |
5 |
6 | const
7 | n = 70
8 | color_choice = @[Color(r: 0.9, g: 0.1, b: 0.1, a: 1.0),
9 | Color(r: 0.1, g: 0.1, b: 0.9, a: 1.0)]
10 |
11 | var
12 | y = new_seq[float64](n)
13 | x = new_seq[float64](n)
14 | text = new_seq[string](n)
15 | colors = new_seq[Color](n)
16 | sizes = new_seq[float64](n)
17 | for i in 0 .. y.high:
18 | x[i] = i.float
19 | y[i] = sin(i.float)
20 | text[i] = $i & " has the sin value: " & $y[i]
21 | sizes[i] = float64(10 + (i mod 10))
22 | if i mod 3 == 0:
23 | colors[i] = color_choice[0]
24 | else:
25 | colors[i] = color_choice[1]
26 | text[i] = text[i] & "" & colors[i].toHtmlHex() & ""
27 |
28 | block:
29 | let d = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
30 | xs: x, ys: y, text: text)
31 | d.marker = Marker[float64](size: sizes, color: colors)
32 |
33 | let layout = Layout(title: "saw the sin", width: 1200, height: 400,
34 | xaxis: Axis(title: "my x-axis"),
35 | yaxis: Axis(title: "y-axis too"), autosize: false)
36 | Plot[float64](layout: layout, traces: @[d]).show()
37 | block:
38 | let d = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL,
39 | xs: x, ys: y, text: text)
40 | d.marker = Marker[float64](size: sizes, colorVals: y, colorMap: ColorMap.Viridis)
41 |
42 | let layout = Layout(title: "saw the sin, colors of sin value!", width: 1200, height: 400,
43 | xaxis: Axis(title: "my x-axis"),
44 | yaxis: Axis(title: "y-axis too"), autosize: false)
45 | Plot[float64](layout: layout, traces: @[d]).show()
46 |
--------------------------------------------------------------------------------
/examples/fig3_multiple_plot_types.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import chroma
3 |
4 | const
5 | text = @["a", "b", "c", "d"]
6 | # some data
7 | y = @[25.5'f64, 5, 9, 10.0]
8 | y2 = @[35.5'f64, 1, 19, 20.0]
9 | y3 = @[15.5'f64, 41, 29, 30.0]
10 |
11 | let
12 | layout = Layout(title: "nim-plotly bar+scattter chart example", width: 1200, height: 400,
13 | xaxis: Axis(title: "category"),
14 | yaxis: Axis(title: "value"), autosize: false)
15 | # some `Trace` instances
16 | d1 = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.Bar, ys: y,
17 | text: text, name: "first group")
18 | d2 = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.Bar, ys: y2,
19 | text: text, name: "second group")
20 | d3 = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.Bar, ys: y3,
21 | text: text, name: "third group")
22 | d4 = Trace[float64](mode: PlotMode.LinesMarkers, `type`: PlotType.ScatterGL, ys: y3,
23 | text: text, name: "scatter", fill: PlotFill.ToZeroY)
24 | d5 = Trace[float64](mode: PlotMode.Markers, `type`: PlotType.ScatterGL, ys: y,
25 | text: text, name: "just markers")
26 | d5.marker = Marker[float64](size: @[25'f64])
27 |
28 | Plot[float64](layout:layout, traces: @[d1, d2, d3, d4, d5]).show()
29 |
--------------------------------------------------------------------------------
/examples/fig4_multiple_axes.nim:
--------------------------------------------------------------------------------
1 | import math
2 | import plotly
3 |
4 | const n = 50
5 | var
6 | y1 = newSeq[float64](n)
7 | y2 = newSeq[float64](n)
8 | x = newSeq[float64](n)
9 |
10 | for i in 0 .. x.high:
11 | x[i] = i.float64
12 | y1[i] = sin(i.float64) * 100
13 | y2[i] = cos(i.float64) / 100
14 |
15 | let
16 | t1 = Trace[float64](mode: PlotMode.Lines, `type`: PlotType.Scatter, ys: y1,
17 | name: "sin*100")
18 | t2 = Trace[float64](mode: PlotMode.Lines, `type`: PlotType.Scatter, ys: y2,
19 | name: "cos/100", yaxis: "y2")
20 | layout = Layout(title: "multiple axes in nim plotly", width: 1200, height: 400,
21 | xaxis: Axis(title: "x"),
22 | yaxis: Axis(title: "sin"),
23 | yaxis2: Axis(title: "cos", side: PlotSide.Right), autosize: false)
24 |
25 | Plot[float64](layout:layout, traces: @[t1, t2]).show()
26 |
--------------------------------------------------------------------------------
/examples/fig5_errorbar.nim:
--------------------------------------------------------------------------------
1 | import sequtils
2 | import plotly
3 | import chroma
4 |
5 | # simple example showcasing scatter plot with error bars
6 |
7 | const colors = @[Color(r:0.9, g:0.4, b:0.0, a: 1.0),
8 | Color(r:0.9, g:0.4, b:0.2, a: 1.0),
9 | Color(r:0.2, g:0.9, b:0.2, a: 1.0),
10 | Color(r:0.1, g:0.7, b:0.1, a: 1.0),
11 | Color(r:0.0, g:0.5, b:0.1, a: 1.0)]
12 | let
13 | d = Trace[float](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
14 | size = @[16.float]
15 | d.marker = Marker[float](size: size, color: colors)
16 | d.xs = @[1'f64, 2, 3, 4, 5]
17 | d.ys = @[1'f64, 2, 1, 9, 5]
18 |
19 | # Note: all `newErrorBar` procs receive an optional `color` argument
20 |
21 | # example of constant error
22 | # d.xs_err = newErrorBar(0.5)
23 | # example of constant percentual error. Note that the value given is in actual
24 | # percent and not a ratio
25 | # d.xs_err = newErrorBar(10.0, percent = true)
26 | # example of an asymmetric error bar on x
27 | d.xs_err = newErrorBar((0.1, 0.25), color = colors[0])
28 |
29 | # create a sequence of increasing error bars for y
30 | let yerrs = mapIt(toSeq(0..5), it.float * 0.25)
31 | d.ys_err = newErrorBar(yerrs)
32 | # import algorithm
33 | # example of asymmetric error bars for each element
34 | # let yerrs_high = @[0.1, 0.2, 0.3, 0.4, 0.5].reversed
35 | # d.ys_err = newErrorBar((yerrs_low, yerrs_high))
36 | # example of a sqrt error on y. Need to hand the correct type here manually,
37 | # otherwise we'll get a "cannot instantiate `ErrorBar[T]`" error, due to
38 | # no value from which type can be deduced is present
39 | # d.ys_err = newErrorBar[float]()
40 |
41 | d.text = @["hello", "data-point", "third", "highest", "bold"]
42 |
43 | let
44 | layout = Layout(title: "testing", width: 1200, height: 400,
45 | xaxis: Axis(title: "my x-axis"),
46 | yaxis: Axis(title: "y-axis too"), autosize: false)
47 | p = Plot[float](layout: layout, traces: @[d])
48 | echo p.save()
49 | p.show()
50 |
--------------------------------------------------------------------------------
/examples/fig6_histogram.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import random
3 | let
4 | d = Trace[int](`type`: PlotType.Histogram)
5 |
6 | # using ys will make a horizontal bar plot
7 | # using xs will make a vertical.
8 | d.ys = newSeq[int](200)
9 |
10 | for i, x in d.ys:
11 | d.ys[i] = rand(20)
12 |
13 | for i in 0..40:
14 | d.ys[i] = 12
15 |
16 | let
17 | layout = Layout(title: "histogram", width: 1200, height: 400,
18 | xaxis: Axis(title:"frequency"),
19 | yaxis: Axis(title: "values"),
20 | autosize: false)
21 | p = Plot[int](layout: layout, traces: @[d])
22 | p.show()
23 |
--------------------------------------------------------------------------------
/examples/fig7_stacked_histogram.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import random
3 | let
4 | d1 = Trace[int](`type`: PlotType.Histogram, opacity: 0.8, name:"some values")
5 | d2 = Trace[int](`type`: PlotType.Histogram, opacity: 0.8, name:"other stuff")
6 |
7 | # using ys will make a horizontal bar plot
8 | # using xs will make a vertical.
9 | d1.ys = newSeq[int](200)
10 | d2.ys = newSeq[int](200)
11 |
12 | for i, x in d1.ys:
13 | d1.ys[i] = rand(20)
14 | d2.ys[i] = rand(30)
15 |
16 | for i in 0..40:
17 | d1.ys[i] = 12
18 |
19 | let
20 | layout = Layout(title: "stacked histogram", width: 1200, height: 400,
21 | yaxis: Axis(title:"values"),
22 | xaxis: Axis(title: "count"),
23 | barmode: BarMode.Stack,
24 | #barmode: BarMode.Overlay,
25 | autosize: false)
26 | p = Plot[int](layout: layout, traces: @[d1, d2])
27 | p.show()
28 |
--------------------------------------------------------------------------------
/examples/fig8_js_interactive.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import chroma
3 | import jsffi
4 | import dom
5 | import json
6 | import sequtils, strutils
7 |
8 | # compile this file with
9 | # nim js fig8_js_javascript.nim
10 | # and then open the `index_javascript.html` with a browser
11 |
12 | proc animate*(p: Plot) =
13 | let
14 | # create JsObjects from data and layout
15 | data = parseJsonToJs(parseTraces(p.traces))
16 | layout = parseJsonToJs($(% p.layout))
17 |
18 | # create a new `Plotly` object
19 | let plotly = newPlotly()
20 | plotly.newPlot("lineplot", data, layout)
21 | var i = 0
22 | proc loop() =
23 | # update the data we plot
24 | let update = @[1, 2, 1, 9, i]
25 | # get first Trace and set new data
26 | p.traces[0].ys = update
27 | let dataNew = parseJsonToJs(parseTraces(p.traces))
28 | # using react we update the plot contained in the `lineplot` div of
29 | # the index_javascript.html
30 | plotly.react("lineplot", dataNew, layout)
31 | inc i
32 |
33 | # using setInterval we update the plot every 100ms with an increased last datapoint
34 | discard window.setInterval(loop, 100)
35 |
36 | when isMainModule:
37 | const colors = @[Color(r:0.9, g:0.4, b:0.0, a: 1.0),
38 | Color(r:0.9, g:0.4, b:0.2, a: 1.0),
39 | Color(r:0.2, g:0.9, b:0.2, a: 1.0),
40 | Color(r:0.1, g:0.7, b:0.1, a: 1.0),
41 | Color(r:0.0, g:0.5, b:0.1, a: 1.0)]
42 | let
43 | d = Trace[int](mode: PlotMode.LinesMarkers, `type`: PlotType.Scatter)
44 | size = @[16.int]
45 | d.marker = Marker[int](size: size, color: colors)
46 | d.xs = @[1, 2, 3, 4, 5]
47 | d.ys = @[1, 2, 1, 9, 5]
48 |
49 | let
50 | layout = Layout(title: "Interactive plot using Plotly.react with JS backend", width: 1200, height: 400,
51 | xaxis: Axis(title:"my x-axis"),
52 | yaxis: Axis(title: "y-axis too"),
53 | autosize: false)
54 | p = Plot[int](layout: layout, traces: @[d])
55 |
56 | p.animate()
57 |
--------------------------------------------------------------------------------
/examples/fig9_heatmap.nim:
--------------------------------------------------------------------------------
1 | import plotly
2 | import chroma
3 | import random
4 | import sequtils
5 |
6 | let
7 | # The GL heatmap is also supported as HeatMapGL
8 | d = Trace[float32](mode: PlotMode.Lines, `type`: PlotType.HeatMap)
9 |
10 | d.colormap = ColorMap.Viridis
11 | # fill data for colormap with random values. The data needs to be supplied
12 | # as a nested seq.
13 | d.zs = newSeqWith(28, newSeq[float32](28))
14 | for x in 0 ..< 28:
15 | for y in 0 ..< 28:
16 | d.zs[x][y] = rand(1.0)
17 | let
18 | layout = Layout(title: "Heatmap example", width: 800, height: 800,
19 | xaxis: Axis(title: "A heatmap x-axis"),
20 | yaxis: Axis(title: "y-axis too"), autosize: false)
21 | p = Plot[float32](layout: layout, traces: @[d])
22 | echo p.save()
23 | p.show()
24 |
--------------------------------------------------------------------------------
/examples/index_javascript.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | title
5 |
6 |
7 |
8 | Plotly Examples in Nim using Javascript target
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/examples/nim.cfg:
--------------------------------------------------------------------------------
1 | path = "$projectPath/../src"
2 |
--------------------------------------------------------------------------------
/plotly.nimble:
--------------------------------------------------------------------------------
1 | # Package
2 |
3 | version = "0.3.3"
4 | author = "Brent Pedersen"
5 | description = "plotting library for nim"
6 | license = "MIT"
7 |
8 |
9 | requires "nim >= 0.18.0", "chroma", "jsbind", "webview", "ws"
10 |
11 | srcDir = "src"
12 |
13 | skipDirs = @["tests"]
14 |
15 | import os, strutils
16 |
17 | task test, "run the tests":
18 | exec "nim c -r tests/plotly/test_api.nim"
19 | exec "nim c --lineDir:on --debuginfo -r examples/all"
20 | exec "nim c --lineDir:on --debuginfo --threads:on -r examples/fig12_save_figure.nim"
21 |
22 | task testCI, "run the tests on github actions":
23 | exec "nim c -r tests/plotly/test_api.nim"
24 | # define the `testCI` flag to use our custom `xdg-open` based proc to open
25 | # firefox, which is non-blocking
26 | exec "nim c --lineDir:on -d:testCI --debuginfo -r examples/all"
27 | exec "nim c --lineDir:on -d:testCI -d:DEBUG --debuginfo --threads:on -r examples/fig12_save_figure.nim"
28 |
29 | task testCINoSave, "run the tests on travis":
30 | exec "nim c -r tests/plotly/test_api.nim"
31 | # TODO: check if this works
32 | exec "nim c --lineDir:on -d:testCI --debuginfo -r examples/all"
33 |
34 | task docs, "Builds documentation":
35 | mkDir("docs"/"plotly")
36 | #exec "nim doc2 --verbosity:0 --hints:off -o:docs/index.html src/hts.nim"
37 | for file in listfiles("src/"):
38 | if splitfile(file).ext == ".nim":
39 | exec "nim doc2 --verbosity:0 --hints:off -o:" & "docs" /../ file.changefileext("html").split("/", 1)[1] & " " & file
40 | for file in listfiles("src/plotly/"):
41 | if splitfile(file).ext == ".nim":
42 | exec "nim doc2 --verbosity:0 --hints:off -o:" & "docs/plotly" /../ file.changefileext("html").split("/", 1)[1] & " " & file
43 |
--------------------------------------------------------------------------------
/src/plotly.nim:
--------------------------------------------------------------------------------
1 | # we now import the plotly modules and export them so that
2 | # the user sees them as a single module
3 | import plotly / [api, plotly_types, errorbar, plotly_sugar, plotly_subplots]
4 | export api
5 | export plotly_types
6 | export errorbar
7 | export plotly_sugar
8 | export plotly_subplots
9 |
10 | when not defined(js):
11 | import plotly / plotly_display
12 | export plotly_display
13 | else:
14 | import plotly / plotly_js
15 | export plotly_js
16 |
--------------------------------------------------------------------------------
/src/plotly/api.nim:
--------------------------------------------------------------------------------
1 | import tables
2 | import json
3 | import chroma
4 | import strformat
5 | import sequtils
6 |
7 | # plotly internal modules
8 | import plotly_types
9 | import color
10 | import errorbar
11 |
12 | proc toPlotJson*[T](plt: Plot[T]): PlotJson =
13 | ## converts a given `Plot[T]` object to a `PlotJson` object
14 | result = new PlotJson
15 | result.traces = % plt.traces
16 | result.layout = % plt.layout
17 |
18 | func parseHistogramFields[T](fields: var OrderedTable[string, JsonNode], t: Trace[T]) =
19 | ## parse the fields of the histogram type. Usese a separate proc
20 | ## for clarity.
21 | fields["cumulative"] = %* {
22 | "enabled" : % t.cumulative
23 | }
24 | fields["histfunc"] = % t.histFunc
25 | fields["histnorm"] = % t.histNorm
26 |
27 | # string to store direction of bars, used to assign
28 | # the fields without explcitily naming 'x' or 'y' fields
29 | var bars = "x"
30 | if t.xs.len == 0:
31 | bars = "y"
32 |
33 | if t.nbins > 0:
34 | fields[&"nbins{bars}"] = % t.nbins
35 | # if nbins is set, this provides the maximum number of bins allowed to be
36 | # calculated by the autobins algorithm
37 | fields[&"autobin{bars}"] = % true
38 |
39 | elif t.bins.start != t.bins.stop:
40 | fields[&"{bars}bins"] = %* {
41 | "start" : % t.bins.start,
42 | "end" : % t.bins.stop,
43 | "size" : % t.binSize
44 | }
45 | # in case bins are set manually, disable autobins
46 | fields[&"autobin{bars}"] = % false
47 |
48 | func calcBinWidth[T](t: Trace[T]): seq[float] =
49 | ## returns the correct bin width according to the bin width priority
50 | ## explained in `setWidthField`.
51 | ## `xs` may contain `ys.len + 1` elements, i.e. the last right edge of
52 | ## all bars is given too.
53 | ## Returns an empty seq, if sequence not needed further
54 | if t.width.float > 0.0:
55 | result = repeat(t.width.float, t.ys.len)
56 | elif t.widths.len > 0:
57 | when T isnot float:
58 | result = t.widths.mapIt(it.float)
59 | else:
60 | result = t.widths
61 | elif t.align == BarAlign.Edge or t.autoWidth:
62 | # have to calculate from `t.xs` bar locations
63 | result = newSeq[float](t.ys.len)
64 | for i in 0 ..< t.xs.high:
65 | result[i] = (t.xs[i+1] - t.xs[i]).float
66 | if t.xs.len == t.ys.len:
67 | # duplicate last element
68 | result[^1] = result[^2]
69 |
70 | func setWidthField[T](fields: var OrderedTable[string, JsonNode],
71 | t: Trace[T], widths: seq[float] = @[]) =
72 | ## Bar width priority:
73 | ## 1. width <- single value
74 | ## 2. widths <- sequence of values
75 | ## 3. autoWidth <- if neither given
76 | ## If all 3 are empty, let Plotly calculate widths automatically
77 | if t.width.float > 0.0:
78 | fields["width"] = % t.width
79 | elif t.widths.len > 0:
80 | fields["width"] = % t.widths
81 | elif t.autoWidth:
82 | fields["width"] = % widths
83 |
84 | func shiftToLeftEdge[T](t: Trace[T], widths: seq[float]): seq[float] =
85 | ## calculates the new bars if left aligned bars are selected
86 | # `xs` values represent *left* edge of bins
87 | result = newSeq[float](t.ys.len)
88 | for i in 0 .. widths.high:
89 | result[i] = t.xs[i].float + (widths[i] / 2.0)
90 |
91 | func parseBarFields[T](fields: var OrderedTable[string, JsonNode], t: Trace[T]) =
92 | ## parses the `Trace` fields for the Bar kind
93 | # calculate width of needed bars
94 | let widths = calcBinWidth(t)
95 | fields.setWidthField(t, widths)
96 | case t.align
97 | of BarAlign.Edge:
98 | # need bin width
99 | fields["x"] = % shiftToLeftEdge(t, widths)
100 | of BarAlign.Center:
101 | # given data are bar positions already
102 | fields["x"] = % t.xs
103 | else: discard
104 |
105 | case t.orientation
106 | of Orientation.Vertical, Orientation.Horizontal:
107 | fields["orientation"] = % t.orientation
108 | else: discard
109 |
110 | func serializeColormap(cmap: ColorMap, customColorMap: CustomColorMap): JsonNode =
111 | ## Given an element of the ColorMap enum `cmap`, returns the correct
112 | ## `JsonNode`. The result is either a `JString` if `cmap` is not `Custom`.
113 | ## Else the `customColorMap` must not be `nil` and the custom colormap will
114 | ## be converted to plotly valid JSON.
115 | case cmap
116 | of Custom:
117 | doAssert not customColorMap.isNil, "CustomColorMap must not be nil if a " &
118 | "custom map is desired!"
119 | result = makePlotlyCustomMap(customColorMap)
120 | else:
121 | result = % $cmap
122 |
123 | func `%`*(c: Color): JsonNode =
124 | result = % c.toHtmlHex()
125 |
126 | func `%`*(f: Font): JsonNode =
127 | var fields = initOrderedTable[string, JsonNode](4)
128 | if f.size != 0:
129 | fields["size"] = % f.size
130 | if not f.color.isEmpty:
131 | fields["color"] = % f.color
132 | if f.family.len > 0:
133 | fields["family"] = % f.family
134 | result = JsonNode(kind: JObject, fields: fields)
135 |
136 | func `%`*(a: Axis): JsonNode =
137 | var fields = initOrderedTable[string, JsonNode](4)
138 | if a.title.len > 0:
139 | fields["title"] = % a.title
140 | if a.font != nil:
141 | fields["titlefont"] = % a.font
142 | if a.domain.len > 0:
143 | fields["domain"] = % a.domain
144 | if a.side != PlotSide.Unset:
145 | fields["side"] = % a.side
146 | fields["overlaying"] = % "y"
147 | if a.hideticklabels:
148 | fields["showticklabels"] = % false
149 | if a.ty != AxisType.Default:
150 | fields["type"] = % a.ty
151 |
152 | if a.range.start != a.range.stop:
153 | fields["autorange"] = % false
154 | # range is given as an array of two elements, start and stop
155 | fields["range"] = % [a.range.start, a.range.stop]
156 | else:
157 | fields["autorange"] = % true
158 |
159 | if a.rangeslider != nil:
160 | fields["rangeslider"] = % a.rangeslider
161 |
162 | if not a.gridColor.isEmpty:
163 | fields["gridcolor"] = % a.gridColor
164 | if a.gridWidth != 0:
165 | fields["gridwidth"] = % a.gridWidth
166 |
167 | result = JsonNode(kind: JObject, fields: fields)
168 |
169 | func `%`*(l: Legend): JsonNode =
170 | var fields = initOrderedTable[string, JsonNode](4)
171 | if l.font != nil:
172 | fields["font"] = % l.font
173 | if not l.backgroundColor.isEmpty:
174 | fields["bgcolor"] = % l.backgroundColor
175 | if not l.bordercolor.isEmpty:
176 | fields["bordercolor"] = % l.borderColor
177 | if l.borderwidth != 0:
178 | fields["borderwidth"] = % l.borderWidth
179 | case l.orientation
180 | of Orientation.Vertical, Orientation.Horizontal:
181 | fields["orientation"] = % l.orientation
182 | else: discard
183 | # fields for x and y are used always. Zero initialized means that if no
184 | # x, y given, but colors / width set, location will be at x / y == 0 / 0
185 | # alternative would be to check for != 0 on both, which would disallow 0 / 0!
186 | fields["x"] = % l.x
187 | fields["y"] = % l.y
188 | result = JsonNode(kind: JObject, fields: fields)
189 |
190 | func `%`*(l: Layout): JsonNode =
191 | var fields = initOrderedTable[string, JsonNode](4)
192 | if l == nil:
193 | return JsonNode(kind: JObject, fields: fields)
194 | if l.title != "":
195 | fields["title"] = % l.title
196 | if l.width != 0:
197 | fields["width"] = % l.width
198 | if l.font != nil:
199 | fields["font"] = % l.font
200 | if l.height != 0:
201 | fields["height"] = % l.height
202 | if l.xaxis != nil:
203 | fields["xaxis"] = % l.xaxis
204 | if l.yaxis != nil:
205 | fields["yaxis"] = % l.yaxis
206 | if l.yaxis2 != nil:
207 | fields["yaxis2"] = % l.yaxis2
208 | if $l.barmode != "":
209 | fields["barmode"] = % l.barmode
210 | if l.legend != nil:
211 | fields["legend"] = % l.legend
212 | fields["showlegend"] = % l.showlegend
213 | # default to closest because other modes suck.
214 | fields["hovermode"] = % "closest"
215 | if $l.hovermode != "":
216 | fields["hovermode"] = % l.hovermode
217 | if 0 < l.annotations.len:
218 | fields["annotations"] = % l.annotations
219 | if not l.backgroundColor.isEmpty:
220 | fields["plot_bgcolor"] = % l.backgroundColor
221 | if not l.paperColor.isEmpty:
222 | fields["paper_bgcolor"] = % l.paperColor
223 |
224 | result = JsonNode(kind: JObject, fields: fields)
225 |
226 | func `%`*(a: Annotation): JsonNode =
227 | ## creates a JsonNode from an `Annotations` object depending on the object variant
228 | result = %[ ("x", %a.x)
229 | , ("xshift", %a.xshift)
230 | , ("y", %a.y)
231 | , ("yshift", %a.yshift)
232 | , ("text", %a.text)
233 | , ("showarrow", %a.showarrow)
234 | ]
235 |
236 | func `%`*(b: ErrorBar): JsonNode =
237 | ## creates a JsonNode from an `ErrorBar` object depending on the object variant
238 | var fields = initOrderedTable[string, JsonNode](4)
239 | fields["visible"] = % b.visible
240 | if not b.color.isEmpty:
241 | fields["color"] = % b.color.toHtmlHex
242 | if b.thickness > 0:
243 | fields["thickness"] = % b.thickness
244 | if b.width > 0:
245 | fields["width"] = % b.width
246 | case b.kind
247 | of ebkConstantSym:
248 | fields["symmetric"] = % true
249 | fields["type"] = % "constant"
250 | fields["value"] = % b.value
251 | of ebkConstantAsym:
252 | fields["symmetric"] = % false
253 | fields["type"] = % "constant"
254 | fields["valueminus"] = % b.valueMinus
255 | fields["value"] = % b.valuePlus
256 | of ebkPercentSym:
257 | fields["symmetric"] = % true
258 | fields["type"] = % "percent"
259 | fields["value"] = % b.percent
260 | of ebkPercentAsym:
261 | fields["symmetric"] = % false
262 | fields["type"] = % "percent"
263 | fields["valueminus"] = % b.percentMinus
264 | fields["value"] = % b.percentPlus
265 | of ebkSqrt:
266 | fields["type"] = % "sqrt"
267 | of ebkArraySym:
268 | fields["symmetric"] = % true
269 | fields["type"] = % "data"
270 | fields["array"] = % b.errors
271 | of ebkArrayAsym:
272 | fields["symmetric"] = % false
273 | fields["type"] = % "data"
274 | fields["arrayminus"] = % b.errorsMinus
275 | fields["array"] = % b.errorsPlus
276 | result = JsonNode(kind: JObject, fields: fields)
277 |
278 | func `%`*(t: Trace): JsonNode =
279 | var fields = initOrderedTable[string, JsonNode](8)
280 | if t.xs.len == 0:
281 | if t.text.len > 0 and t.`type` != PlotType.Histogram:
282 | fields["x"] = % t.text
283 | else:
284 | fields["x"] = % t.xs
285 |
286 | if t.ys.len > 0:
287 | fields["y"] = % t.ys
288 |
289 | if t.xaxis != "":
290 | fields["xaxis"] = % t.xaxis
291 |
292 | if t.yaxis != "":
293 | fields["yaxis"] = % t.yaxis
294 |
295 | if t.opacity != 0:
296 | fields["opacity"] = % t.opacity
297 |
298 | if $t.fill != "":
299 | fields["fill"] = % t.fill
300 |
301 | # now check variant object to fill correct fields
302 | case t.`type`
303 | of PlotType.HeatMap, PlotType.HeatMapGL:
304 | # heatmap stores data in z only
305 | if t.zs.len > 0:
306 | fields["z"] = % t.zs
307 |
308 | fields["colorscale"] = serializeColormap(t.colormap, t.customColormap)
309 | if t.zmin != t.zmax:
310 | # set `zauto` to false and use `zmin`, `zmax` instead, otherwise `zauto` not set
311 | fields["zauto"] = % false
312 | fields["zmin"] = % t.zmin
313 | fields["zmax"] = % t.zmax
314 | of PlotType.Contour:
315 | if t.zs.len > 0: fields["z"] = % t.zs
316 | fields["colorscale"] = serializeColormap(t.colorscale, t.customColorscale)
317 | if t.contours.start != t.contours.stop:
318 | fields["autocontour"] = % false
319 | fields["contours"] = %* {
320 | "start" : % t.contours.start,
321 | "end" : % t.contours.stop,
322 | "size" : % t.contours.size
323 | }
324 | else:
325 | fields["autocontour"] = % true
326 | fields["contours"] = %* {}
327 | if t.heatmap:
328 | fields["contours"]["coloring"] = % "heatmap"
329 | if t.smoothing > 0:
330 | fields["line"] = %* {
331 | "smoothing": % t.smoothing
332 | }
333 | of PlotType.Candlestick:
334 | fields["open"] = % t.open
335 | fields["high"] = % t.high
336 | fields["low"] = % t.low
337 | fields["close"] = % t.close
338 | of PlotType.Histogram:
339 | fields.parseHistogramFields(t)
340 | of PlotType.Bar:
341 | # if `xs` not given, user wants `string` named bars
342 | if t.xs.len > 0:
343 | fields.parseBarFields(t)
344 | of PlotType.Scatter, PlotType.ScatterGL:
345 | if not t.hideLine and t.lineWidth > 0:
346 | fields["line"] = %* {"width": t.lineWidth}
347 | elif t.hideLine:
348 | fields["line"] = %* {"width": 0}
349 | else:
350 | discard
351 |
352 | if t.xs_err != nil:
353 | fields["error_x"] = % t.xs_err
354 | if t.ys_err != nil:
355 | fields["error_y"] = % t.ys_err
356 |
357 | fields["mode"] = % t.mode
358 | fields["type"] = % t.`type`
359 | if t.name.len > 0:
360 | fields["name"] = % t.name
361 | if t.text.len > 0:
362 | fields["text"] = % t.text
363 | if t.marker != nil:
364 | fields["marker"] = % t.marker
365 |
366 | result = JsonNode(kind: JObject, fields: fields)
367 |
368 | func `%`*(m: Marker): JsonNode =
369 | var fields = initOrderedTable[string, JsonNode](8)
370 | if m.size.len > 0:
371 | if m.size.len == 1:
372 | fields["size"] = % m.size[0]
373 | else:
374 | fields["size"] = % m.size
375 | if m.color.len > 0:
376 | if m.color.len == 1:
377 | fields["color"] = % m.color[0]
378 | else:
379 | fields["color"] = % m.color
380 | elif m.colorVals.len > 0:
381 | fields["color"] = % m.colorVals
382 | fields["colorscale"] = serializeColormap(m.colormap, m.customColormap)
383 | fields["showscale"] = % true
384 |
385 | result = JsonNode(kind: JObject, fields: fields)
386 |
387 | func `$`*(d: Trace): string =
388 | var j = % d
389 | result = $j
390 |
391 | func json*(d: Trace, as_pretty=false): string =
392 | var j = % d
393 | if as_pretty:
394 | result = pretty(j)
395 | else:
396 | result = $d
397 |
--------------------------------------------------------------------------------
/src/plotly/color.nim:
--------------------------------------------------------------------------------
1 | import chroma
2 | import json, strformat, sequtils
3 | from plotly_types import CustomColorMap, PredefinedCustomMaps
4 | # defines raw data for viridis, plasma, magma, inferno
5 | import predefined_colormaps
6 |
7 | type
8 | # TODO: make `ColorRange` work as types. Apparently cannot create
9 | # (r: 0.5, g: 0.4, b: 0.1) as tuple w/ ColorRange fields. Implicit
10 | # conversion only works for individual values, not tuples?
11 | # ColorRange = range[0.0 .. 1.0]
12 | CmapData = seq[tuple[r, g, b: float64]]
13 |
14 | # this module contains utility functions used in other modules of plotly
15 | # related to the chroma module as well as custom color maps
16 | func empty*(): Color =
17 | ## returns completely black
18 | result = Color(r: 0, g: 0, b: 0, a: 0)
19 |
20 | func isEmpty*(c: Color): bool =
21 | ## checks whether given color is black according to above
22 | # TODO: this is also black, but should never need black with alpha == 0
23 | result = c == empty()
24 |
25 | func toHtmlHex*(colors: seq[Color]): seq[string] =
26 | result = newSeq[string](len(colors))
27 | for i, c in colors:
28 | result[i] = c.toHtmlHex
29 |
30 | proc makeZeroWhite*(cmap: CmapData): CmapData =
31 | result = @[(r: 1.0, g: 1.0, b: 1.0)]
32 | result.add cmap[1 .. ^1]
33 |
34 | proc makePlotlyCustomMap*(map: CustomColorMap): JsonNode =
35 | result = newJArray()
36 | for i, row in map.rawColors:
37 | let rowJarray = % [% (i.float / (map.rawColors.len - 1).float),
38 | % &"rgb({row[0] * 256.0}, {row[1] * 256.0}, {row[2] * 256.0})"]
39 | result.add rowJarray
40 |
41 | proc getCustomMap*(customMap: PredefinedCustomMaps): CustomColorMap =
42 | var data: CmapData
43 | case customMap
44 | of ViridisZeroWhite:
45 | data = makeZeroWhite(ViridisRaw)
46 | of Plasma:
47 | data = PlasmaRaw
48 | of PlasmaZeroWhite:
49 | data = makeZeroWhite(PlasmaRaw)
50 | of Magma:
51 | data = MagmaRaw
52 | of MagmaZeroWHite:
53 | data = makeZeroWhite(MagmaRaw)
54 | of Inferno:
55 | data = InfernoRaw
56 | of InfernoZeroWHite:
57 | data = makeZeroWhite(InfernoRaw)
58 | of WhiteToBlack:
59 | data = toSeq(0 .. 255).mapIt((r: 1.0 - it.float / 255.0,
60 | g: 1.0 - it.float / 255.0,
61 | b: 1.0 - it.float / 255.0))
62 | of Other:
63 | discard
64 | result = CustomColorMap(rawColors: data,
65 | name: $customMap)
66 |
--------------------------------------------------------------------------------
/src/plotly/errorbar.nim:
--------------------------------------------------------------------------------
1 | import chroma
2 |
3 | # plotly internal modules
4 | import plotly_types
5 | import color
6 |
7 | # this module contains all procedures related to the `ErrorBar` class
8 | # e.g. convenience functions to create a new `ErrorBar` object
9 |
10 | func newErrorBar*[T: SomeNumber](err: T, color: Color = empty(), thickness = 0.0,
11 | width = 0.0, visible = true, percent = false):
12 | ErrorBar[T] =
13 | ## creates an `ErrorBar` object of type `ebkConstantSym` or `ebkPercentSym`, if the `percent` flag
14 | ## is set to `true`
15 | # NOTE: there is a lot of visual noise in the creation here... change how?
16 | if percent == false:
17 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
18 | width: width, kind: ebkConstantSym)
19 | result.value = err
20 | else:
21 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
22 | width: width, kind: ebkPercentSym)
23 | result.percent = err
24 |
25 | func newErrorBar*[T: SomeNumber](err: tuple[m, p: T], color: Color = empty(),
26 | thickness = 0.0, width = 0.0, visible = true,
27 | percent = false): ErrorBar[T] =
28 | ## creates an `ErrorBar` object of type `ebkConstantAsym`, constant plus and
29 | ## minus errors given as tuple or `ebkPercentAsym` of `percent` flag is set to true
30 | ## Note: the first element of the `err` tuple is the `negative` size, the second
31 | ## the positive!
32 | if percent == false:
33 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
34 | width: width, kind: ebkConstantAsym)
35 | result.valuePlus = err.p
36 | result.valueMinus = err.m
37 | else:
38 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
39 | width: width, kind: ebkPercentAsym)
40 | result.percentPlus = err.p
41 | result.percentMinus = err.m
42 |
43 | func newErrorBar*[T: SomeNumber](color: Color = empty(), thickness = 0.0,
44 | width = 0.0, visible = true): ErrorBar[T] =
45 | ## creates an `ErrorBar` object of type `ebkSqrt`
46 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
47 | width: width, kind: ebkSqrt)
48 |
49 | func newErrorBar*[T](err: seq[T], color: Color = empty(), thickness = 0.0,
50 | width = 0.0, visible = true): ErrorBar[T] =
51 | ## creates an `ErrorBar` object of type `ebkArraySym`
52 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
53 | width: width, kind: ebkArraySym)
54 | result.errors = err
55 |
56 | func newErrorBar*[T: SomeNumber](err: tuple[m, p: seq[T]], color: Color = empty(),
57 | thickness = 0.0, width = 0.0, visible = true): ErrorBar[T] =
58 | ## creates an `ErrorBar` object of type `ebkArrayAsym`, where the first
59 | ## Note: the first seq of the `err` tuple is the `negative` error seq, the second
60 | ## the positive!
61 | result = ErrorBar[T](visible: visible, color: color, thickness: thickness,
62 | width: width, kind: ebkArrayAsym)
63 | result.errorsPlus = err.p
64 | result.errorsMinus = err.m
65 |
--------------------------------------------------------------------------------
/src/plotly/image_retrieve.nim:
--------------------------------------------------------------------------------
1 | import ws
2 | import asynchttpserver, asyncnet, asyncdispatch
3 | import strutils, strformat
4 | import re
5 | import os
6 | # to decode SVG data
7 | import uri
8 | # to decode jpeg, png and webp data
9 | import base64
10 |
11 | type
12 | # simple type for clarity, ``Connected`` package first sent after
13 | # connection to websocket server has been established
14 | Message {.pure.} = enum
15 | Connected = "connected"
16 |
17 | template withDebug(actions: untyped) =
18 | # use this template to echo statements, if the
19 | # -d:DEBUG compile flag is set
20 | when defined(DEBUG):
21 | actions
22 |
23 | proc parseImageType*(filename: string): string =
24 | let
25 | (dir, file, ext) = filename.splitFile
26 | filetype = ext.strip(chars = {'.'})
27 | # now check for the given type
28 | case filetype
29 | of "jpg":
30 | # plotly expects the filetype to be given as ".jpeg"
31 | result = "jpeg"
32 | of "jpeg", "png", "svg", "webp":
33 | result = filetype
34 | else:
35 | echo "Warning: Only the following filetypes are allowed:"
36 | echo "\t jpeg, svg, png, webp"
37 | echo "will save file as png"
38 | result = "png"
39 |
40 | # data header which we use to insert the filetype and then strip
41 | # it from the data package
42 | # jpeg header = data:image/jpeg;base64,
43 | # png header = data:image/png;base64,
44 | # webp header = data:image/webp;base64,
45 | # svg header = data:image/svg+xml,
46 | # template for jpg, png and webp
47 | const base64Tmpl = r"data:image/$#;base64,"
48 | # template for svg
49 | const urlTmpl = r"data:image/$#+xml,"
50 |
51 | # use a channel to hand the filename to the callback function
52 | var
53 | filenameChannel: Channel[string]
54 | stopServerChannel: Channel[bool]
55 | filenameChannel.open(1)
56 | # used by the callback function to stop the server when the file has
57 | # been written or an error occured
58 | stopServerChannel.open(1)
59 |
60 | template parseFileType(header: string, regex: Regex): string =
61 | # template due to GC safety. Just have it replaced in code below
62 | var result = ""
63 | if header =~ regex:
64 | # pipe output through ``parseImageType``
65 | result = matches[0]
66 | else:
67 | # default to png
68 | result = "png"
69 | result
70 |
71 | proc cb(req: Request) {.async.} =
72 | # compile regex to parse the data header
73 | let regex = re(r"data:image\/(\w+)[;+].*")
74 | # receive the filename from the channel
75 | let filename = filenameChannel.recv()
76 |
77 | # now await the connection of the websocket client
78 | #let (ws, error) = await verifyWebsocketRequest(req)
79 | var ws = await newWebSocket(req)
80 | if ws.isNil:
81 | echo "WS negotiation failed: ", ws.repr
82 | await req.respond(Http400, "Websocket negotiation failed: " & $ws.repr)
83 | req.client.close()
84 | return
85 | else:
86 | # receive connection successful package
87 | let (opcodeConnect, dataConnect) = await ws.receivePacket()
88 | if dataConnect == $Message.Connected:
89 | withDebug:
90 | debugEcho "Plotly connected successfully!"
91 | else:
92 | echo "Connection broken :/"
93 | return
94 |
95 | # now await the actual data package
96 | let (opcode, data) = await ws.receivePacket()
97 | # get header to parse the actual filetype and remove the header from the data
98 | # determine header length from data, first appearance of `,`
99 | let headerLength = data.find(',')
100 | let
101 | header = (data.strip)[0 .. headerLength]
102 | filetype = header.parseFileType(regex)
103 | var
104 | onlyData = ""
105 | image = ""
106 | case filetype
107 | of "jpeg", "png", "webp":
108 | onlyData = data.replace(base64Tmpl % filetype, "")
109 | # decode the base64 decoded data packet
110 | image = (onlyData).decode
111 | of "svg":
112 | onlyData = data.replace(urlTmpl % filetype, "")
113 | # decode the URI decoded data packet
114 | image = onlyData.decodeUrl
115 | else:
116 | echo "Warning: Unsupported filetype :", filetype
117 | try:
118 | # try to write the filename the user requested
119 | echo "Saving plot to file ", filename
120 | writeFile(filename, image)
121 | except IOError:
122 | echo "Warning: file could not be written to ", filename
123 |
124 | # write ``true`` to the channel to let the server know it can be closed
125 | stopServerChannel.send(true)
126 |
127 | proc listenForImage*(filename: string) =
128 | withDebug:
129 | debugEcho "Starting server"
130 | let server = newAsyncHttpServer()
131 | filenameChannel.send(filename)
132 | # start the async server
133 | asyncCheck server.serve(Port(8080), cb)
134 | # two booleans to keep track of whether we should poll or
135 | # can close the server. The callback writes to a channel once
136 | # its
137 | var
138 | stopAvailable = false
139 | stop = false
140 | while not stopAvailable:
141 | # we try to receive data from the stop channel. Once we do
142 | # check it's actually ``true`` and stop the server
143 | (stopAvailable, stop) = stopServerChannel.tryRecv
144 | if stop:
145 | withDebug:
146 | debugEcho "Closing server"
147 | server.close()
148 | break
149 | #break
150 | # else poll for events, i.e. let the callback work
151 | poll(500)
152 |
--------------------------------------------------------------------------------
/src/plotly/plotly_display.nim:
--------------------------------------------------------------------------------
1 | import std / [strutils, os, osproc, json, sequtils, times]
2 |
3 | # we now import the plotly modules and export them so that
4 | # the user sees them as a single module
5 | import api, plotly_types, plotly_subplots
6 |
7 | when defined(webview) or defined(testCI):
8 | import webview
9 |
10 | # normally just import browsers module. Howver, in case we run
11 | # tests on testCI, we need a way to open a browser, which is
12 | # non-blocking. For some reason `xdg-open` does not return immediately
13 | # on testCI.
14 | when not defined(testCI):
15 | import browsers
16 |
17 | # check whether user is compiling with thread support. We can only compile
18 | # `saveImage` if the user compiles with it!
19 | const hasThreadSupport* = compileOption("threads")
20 | when hasThreadSupport:
21 | import threadpool
22 | import plotly/image_retrieve
23 |
24 | when defined(posix):
25 | import posix_utils
26 |
27 | template openBrowser(): untyped {.dirty.} =
28 | # default normal browser
29 | when defined(posix):
30 | # check if running under WSL, if so convert to full path
31 | let release = uname().release
32 | if "microsoft" in release or "Microsoft" in release:
33 | let res = execCmdEx("wslpath -m " & file)
34 | openDefaultBrowser("file://" & res[0].strip)
35 | else:
36 | openDefaultBrowser(file)
37 | else:
38 | openDefaultBrowser(file)
39 |
40 | when hasThreadSupport:
41 | proc showPlotThreaded(file: string, thr: Thread[string], onlySave: static bool = false) =
42 | when defined(webview) or defined(testCI):
43 | # on testCI we use webview when saving files. We run the webview loop
44 | # until the image saving thread is finished
45 | let w = newWebView("Nim Plotly", "file://" & file)
46 | when onlySave or defined(testCI):
47 | while thr.running:
48 | if not w.isNil:
49 | discard w.loop(1)
50 | else:
51 | break
52 | thr.joinThread
53 | else:
54 | w.run()
55 | w.exit()
56 | else:
57 | # WARNING: dirty template, see above!
58 | openBrowser()
59 | else:
60 | proc showPlot(file: string) =
61 | when defined(webview):
62 | let w = newWebView("Nim Plotly", "file://" & file)
63 | w.run()
64 | w.exit()
65 | elif defined(testCI):
66 | # patched version of Nim's `openDefaultBrowser` which always
67 | # returns immediately
68 | var u = quoteShell(file)
69 | const osOpenCmd =
70 | when defined(macos) or defined(macosx) or defined(windows): "open" else: "xdg-open" ## \
71 | ## Alias for the operating system specific *"open"* command,
72 | ## `"open"` on OSX, MacOS and Windows, `"xdg-open"` on Linux, BSD, etc.
73 | ## NOTE: from Nim stdlib
74 | let cmd = osOpenCmd
75 | discard startProcess(command = cmd, args = [file], options = {poUsePath})
76 | else:
77 | # WARNING: dirty template, see above!
78 | openBrowser()
79 |
80 | include plotly/tmpl_html
81 |
82 | proc parseTraces*[T](traces: seq[Trace[T]]): string =
83 | ## parses the traces of a Plot object to strings suitable for
84 | ## plotly by creating a JsonNode and converting to string repr
85 | result.toUgly(% traces)
86 |
87 | # `show` and `save` are only used for the C target
88 | proc fillImageInjectTemplate(filetype, width, height: string): string =
89 | ## fill the image injection code with the correct fields
90 | ## Here we use numbering of elements to replace in the template.
91 | # Named replacements don't seem to work because of the characters
92 | # around the `$` calls
93 | result = injectImageCode % [filetype,
94 | filetype,
95 | width,
96 | height,
97 | filetype,
98 | width,
99 | height]
100 |
101 | proc fillHtmlTemplate(htmlTemplate,
102 | data_string: string,
103 | p: SomePlot,
104 | filename = "",
105 | autoResize = true): string =
106 | ## fills the HTML template with the correct strings and, if compiled with
107 | ## ``--threads:on``, inject the save image HTML code and fills that
108 | var
109 | slayout = "{}"
110 | title = ""
111 | if p.layout != nil:
112 | when type(p) is Plot:
113 | slayout = $(%p.layout)
114 | title = p.layout.title
115 | else:
116 | slayout = $p.layout
117 | title = p.layout{"title"}.getStr
118 |
119 | # read the HTML template and insert data, layout and title strings
120 | # imageInject is will be filled iff the user compiles with ``--threads:on``
121 | # and a filename is given
122 | var imageInject = ""
123 | when hasThreadSupport:
124 | if filename.len > 0:
125 | # prepare save image code
126 | let filetype = parseImageType(filename)
127 | when type(p) is Plot:
128 | let swidth = $p.layout.width
129 | let sheight = $p.layout.height
130 | else:
131 | let swidth = $p.layout{"width"}
132 | let sheight = $p.layout{"height"}
133 | imageInject = fillImageInjectTemplate(filetype, swidth, sheight)
134 |
135 | let scriptTag = if autoResize: resizeScript()
136 | else: staticScript()
137 | let scriptFilled = scriptTag % [ "data", data_string,
138 | "layout", slayout ]
139 |
140 | # now fill all values into the html template
141 | result = htmlTemplate % [ "title", title,
142 | "scriptTag", scriptFilled,
143 | "saveImage", imageInject]
144 |
145 | proc genPlotDirname(filename, outdir: string): string =
146 | ## generates unique name for the given input file based on its name and
147 | ## the current time
148 | const defaultName = "nim_plotly"
149 | let filename = if filename.len == 0: defaultName # default to give some sane human readable idea
150 | else: splitFile(filename)[1]
151 | let timeStr = format(now(), "yyyy-MM-dd'_'HH-mm-ss'.'fff")
152 | let dir = outdir / defaultName
153 | createDir(dir)
154 | let outfile = filename & "_" & timeStr & ".html"
155 | result = dir / outfile
156 |
157 | proc save*(p: SomePlot,
158 | htmlPath = "",
159 | htmlTemplate = defaultTmplString,
160 | filename = "",
161 | autoResize = true
162 | ): string =
163 | result = if htmlPath.len > 0: htmlPath
164 | else: genPlotDirname(filename, getTempDir())
165 |
166 | when type(p) is Plot:
167 | # convert traces to data suitable for plotly and fill Html template
168 | let data_string = parseTraces(p.traces)
169 | else:
170 | let data_string = $p.traces
171 | let html = htmlTemplate.fillHtmlTemplate(data_string, p, filename, autoResize)
172 |
173 | writeFile(result, html)
174 |
175 | when not hasThreadSupport:
176 | # some violation of DRY for the sake of better error messages at
177 | # compile time
178 | proc show*(p: SomePlot,
179 | filename: string,
180 | htmlPath = "",
181 | htmlTemplate = defaultTmplString,
182 | removeTempFile = false,
183 | autoResize = true)
184 | {.error: "`filename` argument to `show` only supported if compiled " &
185 | "with --threads:on!".}
186 |
187 | proc show*(p: SomePlot,
188 | htmlPath = "",
189 | htmlTemplate = defaultTmplString,
190 | removeTempFile = false,
191 | autoResize = true) =
192 | ## Creates the temporary Html file in using `save`, and opens the user's
193 | ## default browser.
194 | ##
195 | ## If `htmlPath` is given the file is stored in the given path and name.
196 | ## Else a suitable name will be generated based on the current time.
197 | ##
198 | ## `htmlTemplate` allows to overwrite the default HTML template.
199 | ##
200 | ## If `removeTempFile` is true, the temporary file will be deleted after
201 | ## a short while (not recommended).
202 | ##
203 | ## If `autoResize` is true, the shown plot will automatically resize according
204 | ## to the browser window size. This overrides any possible custom sizes for
205 | ## the plot. By default it is disabled for plots that should be saved.
206 | let tmpfile = p.save(htmlPath = htmlPath,
207 | htmlTemplate = htmlTemplate,
208 | autoResize = autoResize)
209 | showPlot(tmpfile)
210 | if removeTempFile:
211 | sleep(2000)
212 | ## remove file after thread is finished
213 | removeFile(tmpfile)
214 |
215 | proc saveImage*(p: SomePlot, filename: string,
216 | htmlPath = "",
217 | htmlTemplate = defaultTmplString,
218 | removeTempFile = false,
219 | autoResize = false)
220 | {.error: "`saveImage` only supported if compiled with --threads:on!".}
221 |
222 | when not defined(js):
223 | proc show*(grid: Grid, filename: string,
224 | htmlPath = "",
225 | htmlTemplate = defaultTmplString,
226 | removeTempFile = false,
227 | autoResize = true)
228 | {.error: "`filename` argument to `show` only supported if compiled " &
229 | "with --threads:on!".}
230 |
231 | proc show*(grid: Grid,
232 | htmlPath = "",
233 | htmlTemplate = defaultTmplString,
234 | removeTempFile = false,
235 | autoResize = true) =
236 | ## Displays the `Grid` plot. Converts the `grid` to a call to
237 | ## `combine` and calls `show` on it.
238 | ##
239 | ## If `htmlPath` is given the file is stored in the given path and name.
240 | ## Else a suitable name will be generated based on the current time.
241 | ##
242 | ## `htmlTemplate` allows to overwrite the default HTML template.
243 | ##
244 | ## If `removeTempFile` is true, the temporary file will be deleted after
245 | ## a short while (not recommended).
246 | ##
247 | ## If `autoResize` is true, the shown plot will automatically resize according
248 | ## to the browser window size. This overrides any possible custom sizes for
249 | ## the plot. By default it is disabled for plots that should be saved.
250 | grid.toPlotJson.show(htmlPath = htmlPath,
251 | htmlTemplate = defaultTmplString,
252 | removeTempFile = removeTempFile,
253 | autoResize = autoResize)
254 | else:
255 | # if compiled with --threads:on
256 | proc show*(p: SomePlot,
257 | filename = "",
258 | htmlPath = "",
259 | htmlTemplate = defaultTmplString,
260 | onlySave: static bool = false,
261 | removeTempFile = false,
262 | autoResize = true) =
263 | ## Creates the temporary Html file using `save`, and opens the user's
264 | ## default browser.
265 | ##
266 | ## If `onlySave` is true, the plot is only saved and "not shown". However
267 | ## this only works on the `webview` target. And a webview window has to
268 | ## be opened, but will be closed automatically the moment the plot is saved.
269 | ##
270 | ## If `htmlPath` is given the file is stored in the given path and name.
271 | ## Else a suitable name will be generated based on the current time.
272 | ##
273 | ## `htmlTemplate` allows to overwrite the default HTML template.
274 | ##
275 | ## If `removeTempFile` is true, the temporary file will be deleted after
276 | ## a short while (not recommended).
277 | ##
278 | ## If `autoResize` is true, the shown plot will automatically resize according
279 | ## to the browser window size. This overrides any possible custom sizes for
280 | ## the plot. By default it is disabled for plots that should be saved.
281 | var thr: Thread[string]
282 | if filename.len > 0:
283 | # start a second thread with a webview server to capture the image
284 | thr.createThread(listenForImage, filename)
285 |
286 | let tmpfile = p.save(htmlPath = htmlPath,
287 | filename = filename,
288 | htmlTemplate = htmlTemplate,
289 | autoResize = autoResize)
290 | showPlotThreaded(tmpfile, thr, onlySave)
291 | if filename.len > 0:
292 | # wait for thread to join
293 | thr.joinThread
294 | if removeTempFile:
295 | sleep(2000)
296 | removeFile(tmpfile)
297 |
298 | proc saveImage*(p: SomePlot, filename: string,
299 | htmlPath = "",
300 | htmlTemplate = defaultTmplString,
301 | removeTempFile = false,
302 | autoResize = false) =
303 | ## Saves the image under the given filename
304 | ## supported filetypes:
305 | ##
306 | ## - jpg, png, svg, webp
307 | ##
308 | ## Note: only supported if compiled with --threads:on!
309 | ##
310 | ## If the `webview` target is used, the plot is ``only`` saved and not
311 | ## shown (for long; webview closed after image saved correctly).
312 | ##
313 | ## If `htmlPath` is given the file is stored in the given path and name.
314 | ## Else a suitable name will be generated based on the current time.
315 | ##
316 | ## `htmlTemplate` allows to overwrite the default HTML template.
317 | ##
318 | ## If `removeTempFile` is true, the temporary file will be deleted after
319 | ## a short while (not recommended).
320 | ##
321 | ## If `autoResize` is true, the shown plot will automatically resize according
322 | ## to the browser window size. This overrides any possible custom sizes for
323 | ## the plot. By default it is disabled for plots that should be saved.
324 | p.show(filename = filename,
325 | htmlPath = htmlPath,
326 | htmlTemplate = htmlTemplate,
327 | onlySave = true,
328 | removeTempFile = removeTempFile,
329 | autoResize = autoResize)
330 |
331 | when not defined(js):
332 | proc show*(grid: Grid,
333 | filename = "",
334 | htmlPath = "",
335 | htmlTemplate = defaultTmplString,
336 | removeTempFile = false,
337 | autoResize = true) =
338 | ## Displays the `Grid` plot. Converts the `grid` to a call to
339 | ## `combine` and calls `show` on it.
340 | ##
341 | ## If `htmlPath` is given the file is stored in the given path and name.
342 | ## Else a suitable name will be generated based on the current time.
343 | ##
344 | ## `htmlTemplate` allows to overwrite the default HTML template.
345 | ##
346 | ## If `removeTempFile` is true, the temporary file will be deleted after
347 | ## a short while (not recommended).
348 | ##
349 | ## If `autoResize` is true, the shown plot will automatically resize according
350 | ## to the browser window size. This overrides any possible custom sizes for
351 | ## the plot. By default it is disabled for plots that should be saved.
352 | grid.toPlotJson.show(filename,
353 | htmlPath = htmlPath,
354 | htmlTemplate = htmlTemplate,
355 | removeTempFile = removeTempFile,
356 | autoResize = autoResize)
357 |
--------------------------------------------------------------------------------
/src/plotly/plotly_js.nim:
--------------------------------------------------------------------------------
1 | import jsbind
2 | import jsffi
3 | import dom
4 | import plotly_types
5 | import api
6 | # defines some functions and types used for the JS target. In this case
7 | # we call the plotly.js functions directly.
8 |
9 | type PlotlyObj = ref object of JsObject
10 | # create a new plotly object
11 | proc newPlotly*(): PlotlyObj {.jsimportgWithName: "function(){return (Plotly)}" .}
12 | proc newPlot*(p: PlotlyObj; divname: cstring; data: JsObject; layout: JsObject) {.jsimport.}
13 | # `react` has the same signature as `newPlot` but is used to quickly update a given
14 | # plot
15 | proc react*(p: PlotlyObj; divname: cstring; data: JsObject; layout: JsObject) {.jsimport.}
16 | proc restyle*(p: PlotlyObj; divname: cstring, update: JsObject) {.jsimport.}
17 | # parseJsonToJs is used to parse stringified JSON to a `JsObject`.
18 | # NOTE: in principle there is `toJs` in the jsffi module, but that
19 | # seems to behave differently
20 | proc parseJsonToJs*(json: cstring): JsObject {.jsimportgWithName: "JSON.parse".}
21 |
22 | proc parseTraces*[T](traces: seq[Trace[T]]): string =
23 | ## parses the traces of a Plot object to strings suitable for
24 | ## plotly by creating a JsonNode and converting to string repr
25 | result.toUgly(% traces)
26 |
--------------------------------------------------------------------------------
/src/plotly/plotly_subplots.nim:
--------------------------------------------------------------------------------
1 | import json, macros, math
2 | import plotly_types, plotly_sugar, api
3 |
4 | type
5 | # subplot specific object, which stores intermediate information about
6 | # the grid layout to use for multiple plots
7 | GridLayout = object
8 | useGrid: bool
9 | rows: int
10 | columns: int
11 |
12 | Grid* = object
13 | # layout of the plot itself
14 | layout*: Layout
15 | numPlotsPerRow*: int
16 | plots: seq[PlotJson]
17 |
18 | proc convertDomain*(d: Domain | DomainAlt): Domain =
19 | ## proc to get a `Domain` from either a `Domain` or `DomainAlt` tuple.
20 | ## That is a tuple of:
21 | ## left, bottom, right, top
22 | ## notation to:
23 | ## left, bottom, width, height
24 | when type(d) is Domain:
25 | result = d
26 | else:
27 | result = (left: d.left,
28 | bottom: d.bottom,
29 | width: d.right - d.left,
30 | height: d.top - d.bottom)
31 |
32 | proc assignDomain(plt: PlotJson, xaxis, yaxis: string, domain: Domain) =
33 | ## assigns the `domain` to the plot described by `xaxis`, `yaxis`
34 | let xdomain = @[domain.left, domain.left + domain.width]
35 | let ydomain = @[domain.bottom, domain.bottom + domain.height]
36 | plt.layout[xaxis]["domain"] = % xdomain
37 | plt.layout[yaxis]["domain"] = % ydomain
38 |
39 | proc calcRowsColumns(rows, columns: int, nPlots: int): (int, int) =
40 | ## Calculates the desired rows and columns for # of `nPlots` given the user's
41 | ## input for `rows` and `columns`.
42 | ## - If no input is given, calculate the next possible rectangle of plots
43 | ## that favors columns over rows.
44 | ## - If either row or column is 0, sets this dimension to 1
45 | ## - If either row or column is -1, calculate square of nPlots for rows / cols
46 | ## - If both row and column is -1 or either -1 and the other 0, default back
47 | ## to the next possible square.
48 | if rows <= 0 and columns <= 0:
49 | # calc square of plots
50 | let sqPlt = sqrt(nPlots.float)
51 | result[1] = sqPlt.ceil.int
52 | result[0] = sqPlt.round.int
53 | elif rows == -1 and columns > 0:
54 | result[0] = (nPlots.float / columns.float).ceil.int
55 | result[1] = columns
56 | elif rows > 0 and columns == -1:
57 | result[0] = rows
58 | result[1] = (nPlots.float / rows.float).ceil.int
59 | elif rows == 0 and columns > 0:
60 | # 1 row, user desired # cols
61 | result = (1, columns)
62 | elif rows > 0 and columns == 0:
63 | # user desired # row, 1 col
64 | result = (rows, 1)
65 | else:
66 | result = (rows, columns)
67 |
68 | proc assignGrid(plt: PlotJson, grid: GridLayout) =
69 | ## assigns the `grid` to the layout of `plt`
70 | ## If a grid is desired, but the user does not specify rows and columns,
71 | ## plots are aranged in a rectangular grid automatically.
72 | ## If only either rows or columns is specified, the other is set to 1.
73 | plt.layout["grid"] = newJObject()
74 | plt.layout["grid"]["pattern"] = % "independent"
75 | let (rows, columns) = calcRowsColumns(grid.rows, grid.columns, plt.traces.len)
76 | plt.layout["grid"]["rows"] = % rows
77 | plt.layout["grid"]["columns"] = % columns
78 |
79 | proc combine(baseLayout: Layout,
80 | plts: openArray[PlotJson],
81 | domains: openArray[Domain],
82 | grid: GridLayout): PlotJson =
83 | # we need to combine the plots on a JsonNode level to avoid problems with
84 | # different plot types!
85 | var res = newPlot()
86 | var useGrid = grid.useGrid
87 | result = res.toPlotJson
88 | result.layout = % baseLayout
89 | if not grid.useGrid and domains.len == 0:
90 | useGrid = true
91 | for i, p in plts:
92 | #doAssert p.traces.len == 1
93 | # first add traces of `*each Plot*`, only afterwards flatten them!
94 | if not p.isNil:
95 | result.traces.add p.traces
96 | # first plot needs to be treated differently than all others
97 | let idx = result.traces.len
98 | var
99 | xaxisStr = "xaxis"
100 | yaxisStr = "yaxis"
101 | if i > 0:
102 | xaxisStr &= $idx
103 | yaxisStr &= $idx
104 |
105 | result.layout[xaxisStr] = p.layout["xaxis"]
106 | result.layout[yaxisStr] = p.layout["yaxis"]
107 |
108 | if not useGrid:
109 | result.assignDomain(xaxisStr, yaxisStr, domains[i])
110 |
111 | if i > 0:
112 | # anchor xaxis to y data and vice versa
113 | result.layout[xaxisStr]["anchor"] = % ("y" & $idx)
114 | result.layout[yaxisStr]["anchor"] = % ("x" & $idx)
115 |
116 | var i = 0
117 | # flatten traces and set correct axis for correct original plots
118 | var traces = newJArray()
119 | if useGrid:
120 | result.assignGrid(grid)
121 |
122 | for tr in mitems(result.traces):
123 | if i > 0:
124 | for t in tr:
125 | t["xaxis"] = % ("x" & $(i + 1))
126 | t["yaxis"] = % ("y" & $(i + 1))
127 | traces.add t
128 | else:
129 | for t in tr:
130 | traces.add t
131 | inc i
132 | result.traces = traces
133 |
134 | proc handleDomain(field, value: NimNode): NimNode =
135 | ## receives a field of the domain description and the corresponding
136 | ## element and returns an element for a named tuple of the domain for the plot
137 | case field.strVal
138 | of "left", "l":
139 | result = nnkExprColonExpr.newTree(ident"left", value)
140 | of "right", "r":
141 | result = nnkExprColonExpr.newTree(ident"right", value)
142 | of "bottom", "b":
143 | result = nnkExprColonExpr.newTree(ident"bottom", value)
144 | of "top", "t":
145 | result = nnkExprColonExpr.newTree(ident"top", value)
146 | of "width", "w":
147 | result = nnkExprColonExpr.newTree(ident"width", value)
148 | of "height", "h":
149 | result = nnkExprColonExpr.newTree(ident"height", value)
150 | else:
151 | error("Plot domain needs to be described by:\n" &
152 | "\t{`left`, `right`, `bottom`, `top`, `width`, `height`}\n" &
153 | "Field: " & field.repr & ", Value: " & value.repr)
154 |
155 | proc handlePlotStmt(plt: NimNode): (NimNode, NimNode) =
156 | ## handle Plot description.
157 | ## First line needs to be identifier of the `Plot[T]` object
158 | ## Second line either a (nameless) tuple of
159 | ## (left: float, bottom: float, width: float, height: float)
160 | ## or several lines with either of the following keys:
161 | ## left = left end of this plot
162 | ## bottom = bottom end of this plot
163 | ## and:
164 | ## width = width of this plot
165 | ## height = width of this plot
166 | ## ``or``:
167 | ## right = right end of this plot
168 | ## top = top end of this plot
169 | ## These can either be done as an assignment, i.e. via `=` or
170 | ## as a call, i.e. via `:`
171 | result[0] = plt[0]
172 | var domain = newNimNode(kind = nnkPar)
173 | # flag to differentiate user handing field of object containing
174 | # `Domain` vs. user leaves out elements of tuple specification
175 | var isSymbol = false
176 | for i in 1 ..< plt.len:
177 | case plt[i].kind
178 | of nnkPar, nnkTupleConstr:
179 | # is nameless tuple
180 | doAssert plt[i].len == 4, "Domain needs to consist of 4 elements!"
181 | domain.add handleDomain(ident"left", plt[i][0])
182 | domain.add handleDomain(ident"bottom", plt[i][1])
183 | domain.add handleDomain(ident"width", plt[i][2])
184 | domain.add handleDomain(ident"height", plt[i][3])
185 | # ignore what comes after
186 | break
187 | of nnkCall:
188 | # for call RHS is StmtList
189 | domain.add handleDomain(plt[i][0], plt[i][1][0])
190 | of nnkAsgn:
191 | # for assignment RHS is single expr
192 | domain.add handleDomain(plt[i][0], plt[i][1])
193 | of nnkDotExpr, nnkBracketExpr, nnkIdent:
194 | # assume the user accesses some object, array or identifier
195 | # storing a domain of either type `Domain` or `DomainAlt`
196 | domain = plt[i]
197 | isSymbol = true
198 | else:
199 | error("Domain description needs to be of node kind nnkIdent, nnkCall, " &
200 | "nnkDotExpr, nnkBracketExpr or nnkAsgn. Line is " & plt[i].repr &
201 | " of kind " & $plt[i].kind)
202 | if domain.len == 4:
203 | # have a full domain, stop
204 | break
205 | if domain.len != 4 and not isSymbol:
206 | # replace by empty node, since user didn't specify domain
207 | domain = newEmptyNode()
208 |
209 | result[1] = domain
210 |
211 | proc handleRowsCols(field, value: NimNode): NimNode =
212 | ## handling of individual assignments for rows / columns for the
213 | ## grid layout
214 | case field.strVal
215 | of "rows", "r":
216 | result = nnkExprColonExpr.newTree(ident"rows", value)
217 | of "columns", "cols", "c":
218 | result = nnkExprColonExpr.newTree(ident"columns", value)
219 | else:
220 | error("Invalid field for grid layout description: " & $field &
221 | "! Use only elements of {\"rows\", \"r\"} and {\"columns\", \"cols\", \"c\"}.")
222 |
223 | proc handleGrid(stmt: NimNode): NimNode =
224 | ## handles parsing of the grid layout description.
225 | ## It looks like the following for example:
226 | ## grid:
227 | ## rows: 2
228 | ## columns: 3
229 | ## which is rewritten to an object constructor for a
230 | ## `GridLayout` object storing the information.
231 | let gridIdent = ident"gridImpl"
232 | var gridVar = quote do:
233 | var `gridIdent` = GridLayout()
234 | var gridObj = nnkObjConstr.newTree(
235 | bindSym"GridLayout",
236 | nnkExprColonExpr.newTree(
237 | ident"useGrid",
238 | ident"true")
239 | )
240 | for el in stmt[1]:
241 | case el.kind
242 | of nnkCall, nnkAsgn:
243 | gridObj.add handleRowsCols(el[0], el[1])
244 | else:
245 | error("Invalid statement in grid layout description: " & el.repr &
246 | " of kind " & $el.kind)
247 | # replace object constructor tree in `gridVar`
248 | gridVar[0][2] = gridObj
249 | result = gridVar
250 |
251 | macro subplots*(stmts: untyped): untyped =
252 | ## macro to create subplots from several `Plot[T]` objects
253 | ## the macro needs to contain the blocks `baseLayout`
254 | ## and one or more `plot` blocks. A plot block has the
255 | ## `Plot[T]` object in line 1, followed by the domain description
256 | ## of the subplot, i.e. the location within the whole canvas.
257 | ##
258 | ## .. code-block:: nim
259 | ## let plt1 = scatterPlot(x, y) # x, y some seq[T]
260 | ## let plt2 = scatterPlot(x2, y2) # x2, y2 some other seq[T]
261 | ## let layout = Layout(...) # some layout for the whole canvas
262 | ## let subplt = subplots:
263 | ## baseLayout: layout
264 | ## plot:
265 | ## plt1
266 | ## left: 0.0
267 | ## bottom: 0.0
268 | ## width: 0.45
269 | ## height: 1.0
270 | ## # alternatively use right, top instead of width, height
271 | ## # single letters also supported, e.g. l == left
272 | ## plot:
273 | ## plt2
274 | ## # or just write a concise tuple, here the
275 | ## (0.55, 0.0, 0.45, 1.0)
276 | ##
277 | ## will create a subplot of `plt1` on the left and `plt2` on the
278 | ## right.
279 | ## This simply creates the following call to `combine`.
280 | ## let subplt = combine(layout,
281 | ## [plt1.toPlotJson, plt2.toPlotJson],
282 | ## [(left: 0.0, bottom: 0.0, width: 0.45, height: 1.0),
283 | ## (left: 0.55, bottom: 0.0, width: 0.45, height: 1.0)])
284 | var
285 | layout: NimNode
286 | # plots contain `Plot[T]` identifier and `domain`
287 | plots: seq[(NimNode, NimNode)]
288 | grid: NimNode
289 | let gridIdent = ident"gridImpl"
290 | grid = quote do:
291 | var `gridIdent` = GridLayout(useGrid: false)
292 |
293 | for stmt in stmts:
294 | case stmt.kind
295 | of nnkCall:
296 | case stmt[0].strVal
297 | of "baseLayout":
298 | layout = stmt[1][0]
299 | of "plot":
300 | # only interested in content of `plot:`, hence [1]
301 | plots.add handlePlotStmt(stmt[1])
302 | of "grid":
303 | grid = handleGrid(stmt)
304 | of nnkIdent:
305 | case stmt.strVal
306 | of "grid":
307 | grid = quote do:
308 | var `gridIdent` = GridLayout(useGrid: true)
309 |
310 | else:
311 | error("Statement needs to be `baseLayout`, `plot`, `grid`! " &
312 | "Line `" & stmt.repr & "` is " & $stmt.kind)
313 |
314 | var
315 | pltArray = nnkBracket.newTree()
316 | domainArray = nnkBracket.newTree()
317 | # split the plot tuples and apply conversions
318 | # `Plot` -> `PlotJson`
319 | # `DomainAlt` | `Domain` -> `Domain`
320 | for i, plt in plots:
321 | let pltIdent = plt[0]
322 | let domainIdent = plt[1]
323 | pltArray.add quote do:
324 | `pltIdent`.toPlotJson
325 | if domainIdent.kind != nnkEmpty:
326 | domainArray.add quote do:
327 | `domainIdent`.convertDomain
328 |
329 | # call combine proc
330 | result = quote do:
331 | block:
332 | `grid`
333 | combine(`layout`, `pltArray`, `domainArray`, `gridIdent`)
334 |
335 | proc createGrid*(numPlots: int, numPlotsPerRow = 0, layout = Layout()): Grid =
336 | ## creates a `Grid` object with `numPlots` to which one can assign plots
337 | ## at runtime. Optionally the number of desired plots per row of the grid
338 | ## may be given. If left empty, the grid will attempt to produce a square,
339 | ## resorting to more columns than rows if not possible.
340 | ## Optionally a base layout can be given for the grid.
341 | result = Grid(layout: layout,
342 | numPlotsPerRow: numPlotsPerRow,
343 | plots: newSeq[PlotJson](numPlots))
344 |
345 | proc createGrid*(size: tuple[rows, cols: int], layout = Layout()): Grid =
346 | ## creates a `Grid` object with `rows` x `cols` plots to which one can assign
347 | ## plots at runtime.
348 | ## Optionally a base layout can be given for the grid.
349 | let nPlots = size.rows * size.cols
350 | result = createGrid(nPlots, size.cols, layout)
351 |
352 | proc add*[T](grid: var Grid, plt: Plot[T]) =
353 | ## add a new plot to the grid. Extends the number of plots stored in the
354 | ## `Grid` by one.
355 | ## NOTE: the given `Plot[T]` object is converted to a `PlotJson` object
356 | ## upon assignment!
357 | grid.plots.add plt.toPlotJson
358 |
359 | proc `[]=`*[T](grid: var Grid, idx: int, plt: Plot[T]) =
360 | ## converts the given `Plot[T]` to a `PlotJson` and assigns to the given
361 | ## index.
362 | if idx > grid.plots.high:
363 | raise newException(IndexError, "Index position " & $idx & " is out of " &
364 | "bounds for grid with " & $grid.plots.len & " plots.")
365 | grid.plots[idx] = plt.toPlotJson
366 |
367 | proc `[]=`*[T](grid: var Grid, coord: tuple[row, col: int], plt: Plot[T]) =
368 | ## converts the given `Plot[T]` to a `PlotJson` and assigns to specified
369 | ## (row, column) coordinate of the grid.
370 | let idx = grid.numPlotsPerRow * coord.row + coord.col
371 | if coord.col > grid.numPlotsPerRow:
372 | raise newException(IndexError, "Column " & $coord.col & " is out of " &
373 | "bounds for grid with " & $grid.numPlotsPerRow & " columns!")
374 | if idx > grid.plots.high:
375 | raise newException(IndexError, "Position (" & $coord.row & ", " & $coord.col &
376 | ") is out of bounds for grid with " & $grid.plots.len & " plots.")
377 | grid.plots[idx] = plt.toPlotJson
378 |
379 | proc `[]`*(grid: Grid, idx: int): PlotJson =
380 | ## returns the plot at index `idx`.
381 | ## NOTE: the plot is returned as a `PlotJson` object, not as the `Plot[T]`
382 | ## originally put in!
383 | result = grid.plots[idx]
384 |
385 | proc `[]`*(grid: Grid, coord: tuple[row, col: int]): PlotJson =
386 | ## returns the plot at (row, column) coordinate `coord`.
387 | ## NOTE: the plot is returned as a `PlotJson` object, not as the `Plot[T]`
388 | ## originally put in!
389 | let idx = grid.numPlotsPerRow * coord.row + coord.col
390 | result = grid.plots[idx]
391 |
392 | proc toPlotJson*(grid: Grid): PlotJson =
393 | ## converts the `Grid` object to a `PlotJson` object ready to be plotted
394 | ## via the normal `show` procedure.
395 | let
396 | (rows, cols) = calcRowsColumns(rows = -1,
397 | columns = grid.numPlotsPerRow,
398 | nPlots = grid.plots.len)
399 | gridLayout = GridLayout(useGrid: true, rows: rows, columns: cols)
400 | result = combine(grid.layout, grid.plots, [], gridLayout)
401 |
402 | when isMainModule:
403 | # test the calculation of rows and columns
404 | doAssert calcRowsColumns(2, 0, 4) == (2, 1)
405 | doAssert calcRowsColumns(0, 2, 4) == (1, 2)
406 | doAssert calcRowsColumns(7, 3, 1) == (7, 3)
407 | doAssert calcRowsColumns(0, 0, 1) == (1, 1)
408 | doAssert calcRowsColumns(0, 0, 2) == (1, 2)
409 | doAssert calcRowsColumns(0, 0, 3) == (2, 2)
410 | doAssert calcRowsColumns(0, 0, 4) == (2, 2)
411 | doAssert calcRowsColumns(0, 0, 5) == (2, 3)
412 | doAssert calcRowsColumns(0, 0, 6) == (2, 3)
413 | doAssert calcRowsColumns(0, 0, 7) == (3, 3)
414 | doAssert calcRowsColumns(0, 0, 8) == (3, 3)
415 | doAssert calcRowsColumns(0, 0, 9) == (3, 3)
416 | doAssert calcRowsColumns(-1, 2, 4) == (2, 2)
417 | doAssert calcRowsColumns(-1, 0, 4) == (2, 2)
418 | doAssert calcRowsColumns(2, -1, 4) == (2, 2)
419 |
--------------------------------------------------------------------------------
/src/plotly/plotly_sugar.nim:
--------------------------------------------------------------------------------
1 | import plotly_types
2 | import sugar
3 | import sequtils
4 | import chroma
5 |
6 | proc newPlot*(xlabel = "", ylabel = "", title = ""): Plot[float64] =
7 | ## create a plot with sane default layout.
8 | result = Plot[float64]()
9 | result.traces = newSeq[Trace[float64]]()
10 | result.layout = Layout(title: title, width: 600, height: 600,
11 | xaxis: Axis(title: xlabel),
12 | yaxis: Axis(title: ylabel),
13 | autosize: false)
14 |
15 | proc roundOrIdent*[T: SomeNumber](x: T): T =
16 | when T is SomeInteger:
17 | x
18 | else:
19 | x.round
20 |
21 | template barPlot*(x, y: untyped): untyped =
22 | type xType = type(x[0])
23 | type yType = type(y[0])
24 | when xType is string:
25 | let xData = x
26 | else:
27 | # make sure x and y are same type
28 | let xData = x.mapIt(yType(it))
29 | let title = "Bar plot of " & astToStr(x) & " vs. " & astToStr(y)
30 | let plLayout = Layout(title: title,
31 | width: 800, height: 600,
32 | xaxis: Axis(title: astToStr(x)),
33 | yaxis: Axis(title: astToStr(y)),
34 | autosize: false)
35 | var tr = Trace[yType](`type`: PlotType.Bar,
36 | ys: y)
37 | when xType is string:
38 | tr.text = xData
39 | else:
40 | tr.xs = xData
41 | let plt = Plot[yType](traces: @[tr], layout: plLayout)
42 | plt
43 |
44 | proc histTrace*[T](hist: seq[T]): Trace[T] =
45 | type hType = type(hist[0])
46 | result = Trace[hType](`type`: PlotType.Histogram,
47 | xs: hist)
48 |
49 | template histPlot*(hist: untyped): untyped =
50 | type hType = type(hist[0])
51 | let title = "Histogram of " & astToStr(hist)
52 | let plLayout = Layout(title: title,
53 | width: 800, height: 600,
54 | xaxis: Axis(title: astToStr(hist)),
55 | yaxis: Axis(title: "Counts"),
56 | autosize: false)
57 | let tr = histTrace(hist)
58 | var plt = Plot[hType](traces: @[tr], layout: plLayout)
59 | plt
60 |
61 | template heatmap*(x, y, z: untyped): untyped =
62 | type xType = type(x[0])
63 | let xData = x
64 | let yData = y.mapIt(xType(it))
65 | let zData = z.mapIt(xType(it))
66 | var zs = newSeqWith(max(xData).roundOrIdent.int + 1,
67 | newSeq[xType](max(yData).roundOrIdent.int + 1))
68 | for i in 0 .. xData.high:
69 | let xIdx = xData[i].roundOrIdent.int
70 | let yIdx = yData[i].roundOrIdent.int
71 | zs[xIdx][yIdx] += zData[i]
72 | let title = "Heatmap of " & astToStr(x) & " vs. " & astToStr(y) & " on " & astToStr(z)
73 | let plLayout = Layout(title: title,
74 | width: 800, height: 800,
75 | xaxis: Axis(title: astToStr(x)),
76 | yaxis: Axis(title: astToStr(y)),
77 | autosize: true)
78 | let tr = Trace[xType](`type`: PlotType.Heatmap,
79 | colormap: ColorMap.Viridis,
80 | zs: zs)
81 | var plt = Plot[xType](traces: @[tr], layout: plLayout)
82 | plt
83 |
84 | proc heatmapTrace*[T](z: seq[seq[T]]): Trace[T] =
85 | type hType = type(z[0])
86 | result = Trace[hType](`type`: PlotType.Heatmap,
87 | colorMap: ColorMap.Viridis,
88 | xs: z)
89 |
90 | template heatmap*[T](z: seq[seq[T]]): untyped =
91 | type zType = type(z[0][0])
92 | var zs = z
93 | let title = "Heatmap of " & astToStr(z)
94 | let plLayout = Layout(title: title,
95 | width: 800, height: 800,
96 | xaxis: Axis(title: "x"),
97 | yaxis: Axis(title: "y"),
98 | autosize: true)
99 | let tr = Trace[zType](`type`: PlotType.Heatmap,
100 | colormap: ColorMap.Viridis,
101 | zs: zs)
102 | var plt = Plot[zType](traces: @[tr], layout: plLayout)
103 | plt
104 |
105 | proc scatterTrace*[T, U](x: seq[T], y: seq[U]): Trace[T] =
106 | type xType = type(x[0])
107 | let xData = x
108 | # make sure y has same dtype
109 | let yData = y.mapIt(xType(it))
110 | result = Trace[xType](mode: PlotMode.Markers,
111 | marker: Marker[xType](),
112 | `type`: PlotType.Scatter,
113 | xs: xData,
114 | ys: yData)
115 |
116 | template scatterPlot*(x, y: untyped): untyped =
117 | type xType = type(x[0])
118 | let title = "Scatter plot of " & astToStr(x) & " vs. " & astToStr(y)
119 | let plLayout = Layout(title: title,
120 | width: 800, height: 600,
121 | xaxis: Axis(title: astToStr(x)),
122 | yaxis: Axis(title: astToStr(y)),
123 | autosize: false)
124 | let tr = scatterTrace(x, y)
125 | var plt = Plot[xType](traces: @[tr], layout: plLayout)
126 | plt
127 |
128 | template scatterColor*(x, y, z: untyped): untyped =
129 | ## adds a color dimension to the scatter plot in addition
130 | type xType = type(x[0])
131 | let zData = z.mapIt(xType(it))
132 | let zText = zData.mapIt((astToStr(z) & ": " & $it))
133 | let title = "Scatter plot of " & astToStr(x) & " vs. " & astToStr(y) &
134 | " with colorscale of " & astToStr(z)
135 | let plt = scatterPlot(x, y)
136 | .title(title)
137 | .text(zText)
138 | .markercolor(colors = zData,
139 | map = ColorMap.Viridis)
140 | plt
141 |
142 | proc addTrace*[T](plt: Plot[T], t: Trace[T]): Plot[T] =
143 | result = plt
144 | result.traces.add t
145 |
146 | proc title*[T](plt: Plot[T], t: string): Plot[T] =
147 | result = plt
148 | result.layout.title = t
149 |
150 | proc width*[T, U: SomeNumber](plt: Plot[T], width: U): Plot[T] =
151 | result = plt
152 | result.layout.width = width.roundOrIdent.int
153 |
154 | proc height*[T, U: SomeNumber](plt: Plot[T], height: U): Plot[T] =
155 | result = plt
156 | result.layout.height = height.roundOrIdent.int
157 |
158 | proc text*[T; U: string | seq[string]](plt: Plot[T],
159 | val: U,
160 | idx = 0): Plot[T] =
161 | result = plt
162 | when type(val) is string:
163 | result.traces[idx].text = @[val]
164 | else:
165 | result.traces[idx].text = val
166 |
167 | proc markerSize*[T](plt: Plot[T],
168 | val: SomeNumber,
169 | idx = 0): Plot[T] =
170 | result = plt
171 | if result.traces[idx].marker.isNil:
172 | result.traces[idx].marker = Marker[T]()
173 | result.traces[idx].marker.size = @[T(val)]
174 |
175 | proc markerSizes*[T](plt: Plot[T],
176 | sizes: seq[T],
177 | idx = 0): Plot[T] =
178 | result = plt
179 | if result.traces[idx].marker.isNil:
180 | result.traces[idx].marker = Marker[T]()
181 | result.traces[idx].marker.size = sizes
182 |
183 | proc markerColor*[T](plt: Plot[T],
184 | colors: seq[Color] | seq[T] = @[],
185 | map: ColorMap = ColorMap.None,
186 | idx = 0): Plot[T] =
187 | result = plt
188 | if result.traces[idx].marker.isNil:
189 | result.traces[idx].marker = Marker[T]()
190 | if colors.len > 0:
191 | when type(colors[idx]) is Color:
192 | result.traces[idx].marker.color = colors
193 | else:
194 | result.traces[idx].marker.colorVals = colors
195 | if map != ColorMap.None:
196 | result.traces[idx].marker.colormap = map
197 |
198 | proc mode*[T](plt: Plot[T], m: PlotMode, idx = 0): Plot[T] =
199 | result = plt
200 | result.traces[idx].mode = m
201 |
202 | proc lineWidth*[T](plt: Plot[T], val: SomeNumber, idx = 0): Plot[T] =
203 | result = plt
204 | doAssert plt.traces[idx].`type` in {Scatter, ScatterGL}
205 | result.traces[idx].lineWidth = val.roundOrIdent.int
206 |
207 | template pltLabel*(plt: untyped,
208 | axis: untyped,
209 | label: string): untyped =
210 | if plt.layout.axis == nil:
211 | plt.layout.axis = Axis()
212 | plt.layout.axis.title = label
213 |
214 | proc xlabel*[T](plt: Plot[T], label: string): Plot[T] =
215 | result = plt
216 | result.pltLabel(xaxis, label)
217 |
218 | proc ylabel*[T](plt: Plot[T], label: string): Plot[T] =
219 | result = plt
220 | result.pltLabel(yaxis, label)
221 |
222 | proc name*[T](plt: Plot[T], name: string, idx = 0): Plot[T] =
223 | result = plt
224 | result.traces[idx].name = name
225 |
226 | proc nbins*[T](plt: Plot[T], nbins: int, idx = 0): Plot[T] =
227 | result = plt
228 | doAssert result.traces[idx].`type` == PlotType.Histogram
229 | result.traces[idx].nbins = nbins
230 |
231 | proc binSize*[T](plt: Plot[T], size: float, idx = 0): Plot[T] =
232 | result = plt
233 | doAssert result.traces[idx].`type` == PlotType.Histogram
234 | result.traces[idx].binSize = size
235 |
236 | proc binRange*[T](plt: Plot[T], start, stop: float, idx = 0): Plot[T] =
237 | result = plt
238 | doAssert result.traces[idx].`type` == PlotType.Histogram
239 | result.traces[idx].bins = (start, stop)
240 |
241 | proc legend*[T](plt: Plot[T], legend: Legend): Plot[T] =
242 | result = plt
243 | result.layout.legend = legend
244 | result.layout.showLegend = true
245 |
246 | proc legendLocation*[T](plt: Plot[T], x, y: float): Plot[T] =
247 | result = plt
248 | if result.layout.legend == nil:
249 | result.layout.legend = Legend()
250 | result.layout.legend.x = x
251 | result.layout.legend.y = y
252 |
253 | proc legendBgColor*[T](plt: Plot[T], color: Color): Plot[T] =
254 | result = plt
255 | if result.layout.legend == nil:
256 | result.layout.legend = Legend()
257 | result.layout.legend.backgroundColor = color
258 |
259 | proc legendBorderColor*[T](plt: Plot[T], color: Color): Plot[T] =
260 | result = plt
261 | if result.layout.legend == nil:
262 | result.layout.legend = Legend()
263 | result.layout.legend.borderColor = color
264 |
265 | proc legendBorderWidth*[T](plt: Plot[T], width: int): Plot[T] =
266 | result = plt
267 | if result.layout.legend == nil:
268 | result.layout.legend = Legend()
269 | result.layout.legend.borderWidth = width
270 |
271 | proc legendOrientation*[T](plt: Plot[T], orientation: Orientation): Plot[T] =
272 | result = plt
273 | if result.layout.legend == nil:
274 | result.layout.legend = Legend()
275 | result.layout.legend.orientation = orientation
276 |
277 | proc gridWidthX*[T](plt: Plot[T], width: int): Plot[T] =
278 | result = plt
279 | if result.layout.xaxis == nil:
280 | result.layout.xaxis = Axis()
281 | result.layout.xaxis.gridWidth = width
282 |
283 | proc gridWidthY*[T](plt: Plot[T], width: int): Plot[T] =
284 | result = plt
285 | if result.layout.xaxis == nil:
286 | result.layout.xaxis = Axis()
287 | result.layout.yaxis.gridWidth = width
288 |
289 | proc gridWidth*[T](plt: Plot[T], width: int): Plot[T] =
290 | result = plt.gridWidthX(width).gridWidthY(width)
291 |
292 | proc gridColorX*[T](plt: Plot[T], color: Color): Plot[T] =
293 | result = plt
294 | if result.layout.xaxis == nil:
295 | result.layout.xaxis = Axis()
296 | result.layout.xaxis.gridColor = color
297 |
298 | proc gridColorY*[T](plt: Plot[T], color: Color): Plot[T] =
299 | result = plt
300 | if result.layout.yaxis == nil:
301 | result.layout.yaxis = Axis()
302 | result.layout.yaxis.gridColor = color
303 |
304 | proc gridColor*[T](plt: Plot[T], color: Color): Plot[T] =
305 | result = plt.gridColorX(color).gridColorY(color)
306 |
307 | proc backgroundColor*[T](plt: Plot[T], color: Color): Plot[T] =
308 | result = plt
309 | result.layout.backgroundColor = color
310 |
311 | proc paperColor*[T](plt: Plot[T], color: Color): Plot[T] =
312 | result = plt
313 | result.layout.paperColor = color
314 |
315 | type
316 | AllowedColorMap = ColorMap | PredefinedCustomMaps |
317 | CustomColorMap | seq[tuple[r, g, b: float64]]
318 | proc colormap*[T; U: AllowedColorMap](plt: Plot[T], colormap: U, idx = 0): Plot[T] =
319 | ## assigns the given colormap to the trace of index `idx`
320 | ## A colormap can be given as one of the following:
321 | ## - `ColorMap: enum`
322 | ## - `PredefinedCustomMaps: enum`
323 | ## - `CustomColorMap: ref object`
324 | ## - `colormapData: seq[tuple[r, g, b: float64]]`
325 | result = plt
326 | doAssert idx < plt.traces.len, "Invalid trace index!"
327 | var cmapEnum: ColorMap
328 | var customCmap: CustomColorMap = nil
329 | when U is ColorMap:
330 | cmapEnum = colormap
331 | elif U is PredefinedCustomMaps:
332 | cmapEnum = ColorMap.Custom
333 | customCmap = getCustomMap(colormap)
334 | elif U is CustomColorMap:
335 | cmapEnum = ColorMap.Custom
336 | customCmap = colorMap
337 | else:
338 | cmapEnum = ColorMap.Custom
339 | customCmap = CustomColorMap(rawColors: colormap, name: "Non-predefined")
340 | case result.traces[idx].`type`
341 | of Heatmap, HeatmapGL:
342 | result.traces[idx].colormap = cmapEnum
343 | result.traces[idx].customColormap = customCmap
344 | of Contour:
345 | result.traces[idx].colorscale = cmapEnum
346 | result.traces[idx].customColorscale = customCmap
347 | else: discard
348 |
349 | proc zmin*[T](plt: Plot[T], val: float, idx = 0): Plot[T] =
350 | ## Allows to set the minimum value of the colormap for a heatmap
351 | ## for trace of index `idx`
352 | doAssert plt.traces[idx].`type` in {Heatmap, HeatmapGL}
353 | result = plt
354 | result.traces[idx].zmin = val
355 |
356 | proc zmax*[T](plt: Plot[T], val: float, idx = 0): Plot[T] =
357 | ## Allows to set the maximum value of the colormap for a heatmap
358 | ## for trace of index `idx`
359 | doAssert plt.traces[idx].`type` in {Heatmap, HeatmapGL}
360 | result = plt
361 | result.traces[idx].zmax = val
362 |
--------------------------------------------------------------------------------
/src/plotly/plotly_types.nim:
--------------------------------------------------------------------------------
1 | import chroma, json
2 |
3 | # this module contains all types used in the plotly module
4 |
5 | type
6 | Plot*[T: SomeNumber] = ref object
7 | traces* : seq[Trace[T]]
8 | layout*: Layout
9 |
10 | PlotJson* = ref object
11 | traces* : JsonNode
12 | layout*: JsonNode
13 |
14 | SomePlot* = Plot | PlotJson
15 |
16 | PlotType* {.pure.} = enum
17 | Scatter = "scatter"
18 | ScatterGL = "scattergl"
19 | Bar = "bar"
20 | Histogram = "histogram"
21 | Box = "box"
22 | HeatMap = "heatmap"
23 | HeatMapGL = "heatmapgl"
24 | Candlestick = "candlestick"
25 | Contour = "contour"
26 |
27 | HistFunc* {.pure.} = enum
28 | # count is plotly.js default
29 | Count = "count"
30 | Sum = "sum"
31 | Avg = "avg"
32 | Min = "min"
33 | Max = "max"
34 |
35 | HistNorm* {.pure.} = enum
36 | None = ""
37 | Percent = "percent"
38 | Probability = "probability"
39 | Density = "density"
40 | ProbabilityDensity = "probability density"
41 |
42 | PlotFill* {.pure.} = enum
43 | Unset = ""
44 | ToNextY = "tonexty"
45 | ToZeroY = "tozeroy"
46 | ToSelf = "toself"
47 |
48 | PlotMode* {.pure.} = enum
49 | Lines = "lines"
50 | Markers = "markers"
51 | LinesMarkers = "lines+markers"
52 |
53 | BarMode* {.pure.} = enum
54 | Unset = ""
55 | Stack = "stack"
56 | Overlay = "overlay"
57 |
58 | BarAlign* {.pure.} = enum
59 | None,
60 | Edge,
61 | Center
62 |
63 | Orientation* {.pure.} = enum
64 | None = ""
65 | Vertical = "v"
66 | Horizontal = "h"
67 |
68 | HoverMode* {.pure.} = enum
69 | Closest = "closest"
70 | X = "x"
71 | Y = "y"
72 | False = "false"
73 |
74 | PlotSide* {.pure.} = enum
75 | Unset = ""
76 | Left = "left"
77 | Right = "right"
78 |
79 | ColorMap* {.pure.} = enum
80 | None = ""
81 | Custom = "Custom"
82 | Greys = "Greys"
83 | YlGnBu = "YlGnBu"
84 | Greens = "Greens"
85 | YlOrRd = "YlOrRd"
86 | Bluered = "Bluered"
87 | RdBu = "RdBu"
88 | Reds = "Reds"
89 | Blues = "Blues"
90 | Picnic = "Picnic"
91 | Rainbow = "Rainbow"
92 | Portland = "Portland"
93 | Jet = "Jet"
94 | Hot = "Hot"
95 | Blackbody = "Blackbody"
96 | Earth = "Earth"
97 | Electric = "Electric"
98 | Viridis = "Viridis"
99 | Cividis = "Cividis"
100 |
101 | PredefinedCustomMaps* = enum
102 | Other, # non predefined custom colormap
103 | ViridisZeroWhite, # Viridis w/ value of 0 set to white; Viridis is part of Plotly!
104 | Plasma, PlasmaZeroWhite,
105 | Magma, MagmaZeroWhite,
106 | Inferno, InfernoZeroWhite,
107 | WhiteToBlack # 0 = White, 1 = Black
108 |
109 | CustomColorMap* = ref object
110 | # raw color values in range [0.0, 1.0]
111 | # using range[0.0 .. 1.0]]] doesn't work properly
112 | rawColors*: seq[tuple[r, g, b: float64]]
113 | name*: string
114 |
115 | AxisType* {.pure.} = enum
116 | Default = "-"
117 | Log = "log"
118 |
119 | ErrorBarKind* = enum # different error bar kinds (from constant value, array,...)
120 | ebkConstantSym, # constant symmetric error
121 | ebkConstantAsym, # constant asymmetric error
122 | ebkPercentSym, # symmetric error on percent of value
123 | ebkPercentAsym, # asymmetric error on percent of value
124 | ebkSqrt, # error based on sqrt of value
125 | ebkArraySym, # symmetric error based on array of length data.len
126 | ebkArrayAsym # assymmetric error based on array of length data.len
127 |
128 | ErrorBar*[T: SomeNumber] = ref object
129 | visible*: bool
130 | color*: Color # color of bars (including alpha channel)
131 | thickness*: float # thickness of bar
132 | width*: float # width of bar
133 | case kind*: ErrorBarKind
134 | of ebkConstantSym:
135 | value*: T
136 | of ebkConstantAsym:
137 | valueMinus*: T
138 | valuePlus*: T
139 | of ebkPercentSym:
140 | percent*: T
141 | of ebkPercentAsym:
142 | percentMinus*: T
143 | percentPlus*: T
144 | of ebkSqrt:
145 | # NOTE: the fact that we technically have not type T in the `ErrorBar` for
146 | # this variant means we have to hand it to the `newErrorBar` proc manually!
147 | discard
148 | of ebkArraySym:
149 | errors*: seq[T]
150 | of ebkArrayAsym:
151 | errorsMinus*: seq[T]
152 | errorsPlus*: seq[T]
153 |
154 | Marker*[T: SomeNumber] = ref object
155 | size*: seq[T]
156 | color*: seq[Color]
157 | # alternatively use sequence of values defining color based on one of
158 | # the color maps
159 | colorVals*: seq[T]
160 | colormap*: ColorMap
161 | customColormap*: CustomColorMap
162 |
163 | Trace*[T: SomeNumber] = ref object
164 | xs*: seq[T]
165 | ys*: seq[T]
166 | zs*: seq[seq[T]]
167 | xs_err*: ErrorBar[T]
168 | ys_err*: ErrorBar[T]
169 | marker*: Marker[T]
170 | text*: seq[string]
171 | opacity*: float
172 | mode*: PlotMode
173 | fill*: PlotFill
174 | name*: string
175 | xaxis*: string
176 | yaxis*: string
177 | # case on `type`, since we only need ColorMap for
178 | # PlotType.HeatMap
179 | case `type`*: PlotType
180 | of HeatMap, HeatMapGL:
181 | colormap*: ColorMap
182 | customColormap*: CustomColorMap
183 | zmin*: float # can be used to override calculation of color ranges based on data
184 | zmax*: float # `zmin` minimum and `zmax` maximum value of color range
185 | of Contour:
186 | colorscale*: ColorMap
187 | customColorscale*: CustomColorMap
188 | # setting no contours implies `autocontour` true
189 | contours*: tuple[start, stop, size: float]
190 | heatmap*: bool
191 | smoothing*: float
192 | # case on `type`, since we only need Close,High,Low,Open for
193 | # PlotType.Candlestick
194 | of Candlestick:
195 | open*: seq[T]
196 | high*: seq[T]
197 | low*: seq[T]
198 | close*: seq[T]
199 | of Histogram:
200 | histFunc*: HistFunc
201 | histNorm*: HistNorm
202 | # TODO: include increasing and decreasing distinction?
203 | cumulative*: bool
204 | # if `nBins` is set, the `bins` tuple and `binSize` will be ignored
205 | nBins*: int
206 | bins*: tuple[start, stop: float]
207 | # `binSize` is optional, even if `bins` is given.
208 | binSize*: float
209 | of Bar:
210 | # manually set bin width via scalar
211 | width*: T
212 | # or seq (widths.len == xs.len), overwritten by `width`
213 | widths*: seq[T]
214 | # calculate bin widths automaticlly to leave no space between
215 | # overwritten by `width`, `widths`
216 | autoWidth*: bool
217 | # align bins left or center (default)
218 | align*: BarAlign
219 | # orientation of bars, vertical or horizontal
220 | orientation*: Orientation
221 | of Scatter, ScatterGL:
222 | lineWidth*: int
223 | hideLine*: bool # can be used to force line to be width 0 (for backwards compatiblity)
224 | else:
225 | discard
226 |
227 | Font* = ref object
228 | family*: string
229 | size*: int
230 | color*: Color
231 |
232 | RangeSlider* = ref object
233 | visible*: bool
234 |
235 | # tuple types to set location of subplots within a plot
236 | # given in relative coordinates of the plot [0, 1] canvas
237 | Domain* = tuple
238 | left, bottom, width, height: float
239 | # alternative notation for a `Domain`. Instead of using width and height,
240 | # directly set right and top edge of plot.
241 | DomainAlt* = tuple
242 | left, bottom, right, top: float
243 |
244 | Axis* = ref object
245 | title*: string
246 | font*: Font
247 | domain*: seq[float64]
248 | side*: PlotSide
249 | rangeslider*: RangeSlider
250 | # setting no range implies plotly's `autorange` true
251 | range*: tuple[start, stop: float]
252 | # oposite of showticklabels
253 | hideticklabels*: bool
254 | gridColor*: Color
255 | gridWidth*: int
256 | ty*: AxisType
257 |
258 | Annotation* = ref object
259 | x*: float
260 | xshift*: float
261 | y*: float
262 | yshift*: float
263 | text*: string
264 | showarrow*: bool
265 |
266 | Legend* = ref object
267 | # location in x, y in relative coordinates of the layout in [-2, 3]
268 | x*: float
269 | y*: float
270 | font*: Font
271 | backgroundColor*: Color
272 | borderColor*: Color
273 | borderWidth*: int # border width in pixels
274 | orientation*: Orientation
275 |
276 | Layout* = ref object
277 | title*: string
278 | width*: int
279 | height*: int
280 | hovermode*: HoverMode
281 | annotations*: seq[Annotation]
282 | autosize*: bool
283 | showlegend*: bool
284 | legend*: Legend
285 | font*: Font
286 | xaxis*: Axis
287 | yaxis*: Axis
288 | yaxis2*: Axis
289 | barmode*: BarMode
290 | backgroundColor*: Color # background of plot
291 | paperColor*: Color # background of paper / canvas
292 |
--------------------------------------------------------------------------------
/src/plotly/tmpl_html.nim:
--------------------------------------------------------------------------------
1 | template resizeScript(): untyped =
2 | # TODO: this currently overrides size settings given in plots;
3 | # need to expose whether to autoresize or not
4 | # Note: this didn't seem to work: Plotly.Plots.resize('plot0');
5 | # Consider to add: `{responsive: true}`
6 | """
7 | runRelayout = function() {
8 | var margin = 50; // if 0, would introduce scrolling
9 | Plotly.relayout('plot0', {width: window.innerWidth - margin, height: window.innerHeight - margin } );
10 | };
11 | window.onresize = runRelayout;
12 | Plotly.newPlot('plot0', $data, $layout).then(runRelayout);
13 | """
14 |
15 | template staticScript(): untyped =
16 | ## the default script to get a static plot used if the user wishes to save a file as well
17 | ## as view it
18 | """Plotly.newPlot('plot0', $data, $layout)"""
19 |
20 | const defaultTmplString = """
21 |
22 |
23 |
24 |
25 | $title
26 |
27 |
28 |
29 |
30 |
33 | $saveImage
34 |
35 |
36 | """
37 |
38 | # type needs to be inserted!
39 | # either
40 | # - png
41 | # - svg
42 | # - jpg
43 | const injectImageCode = """
44 |
61 | """
62 |
--------------------------------------------------------------------------------
/tests/config.nims:
--------------------------------------------------------------------------------
1 | switch("path", "$projectDir/src")
2 |
--------------------------------------------------------------------------------
/tests/plotly/test_api.nim:
--------------------------------------------------------------------------------
1 | import ../../src/plotly
2 | import ../../src/plotly/color
3 | import chroma
4 | import unittest
5 | import json, sequtils
6 | import random
7 |
8 | suite "Miscellaneous":
9 | test "Color checks":
10 | let c = empty()
11 | check c.isEmpty
12 | test "Default AxisType":
13 | var ty: AxisType
14 | check ty == AxisType.Default
15 |
16 | suite "API serialization":
17 | test "Color":
18 | let
19 | c1 = % color(1.0, 1.0, 1.0)
20 | c2 = % color(0.5, 0.5, 0.5)
21 | c3 = % empty()
22 | check c1 == % "#FFFFFF"
23 | check c2 == % "#7F7F7F"
24 | check c3 == % "#000000"
25 |
26 | test "Marker":
27 | test "make Markers, scalar size":
28 | let
29 | mk = Marker[float](size: @[1.0])
30 | expected = %*{ "size": 1.0 }
31 | let r = %mk
32 | check r == expected
33 |
34 | test "make Markers, seq of sizes":
35 | let
36 | mk = Marker[float](size: @[1.0, 2.0, 3.0])
37 | expected = %*{ "size": [1.0, 2.0, 3.0] }
38 | let r = %mk
39 | check r == expected
40 |
41 | test "make Markers, scalar color":
42 | let
43 | mk = Marker[float](size: @[1.0],
44 | color: @[color(0.5, 0.5, 0.5)])
45 | expected = %*{ "size": 1.0,
46 | "color" : "#7F7F7F"
47 | }
48 | let r = %mk
49 | check r == expected
50 |
51 | test "make Markers, seq of colors":
52 | let
53 | mk = Marker[float](size: @[1.0],
54 | color: @[color(0.5, 0.5, 0.5), color(1.0, 1.0, 1.0), empty()])
55 | expected = %*{ "size": 1.0,
56 | "color" : ["#7F7F7F", "#FFFFFF", "#000000"]
57 | }
58 | let r = %mk
59 | check r == expected
60 |
61 | test "make Markers, seq of color based on values; no color map":
62 | let
63 | mk = Marker[float](size: @[1.0],
64 | colorVals: @[0.25, 0.5, 0.75, 1.0])
65 | expected = %*{ "size": 1.0,
66 | "color" : [0.25, 0.5, 0.75, 1.0],
67 | "colorscale" : "",
68 | "showscale" : true
69 | }
70 | let r = %mk
71 | check r == expected
72 |
73 | test "make Markers, seq of color based on values; w/ color map":
74 | let
75 | mk = Marker[float](size: @[1.0],
76 | colorVals: @[0.25, 0.5, 0.75, 1.0],
77 | colormap: ColorMap.Viridis
78 | )
79 | expected = %*{ "size": 1.0,
80 | "color" : [0.25, 0.5, 0.75, 1.0],
81 | "colorscale" : "Viridis",
82 | "showscale" : true
83 | }
84 | let r = %mk
85 | check r == expected
86 |
87 | test "make Markers, color takes precedent over colorVals":
88 | let
89 | mk = Marker[float](size: @[1.0],
90 | color: @[color(0.5, 0.5, 0.5)],
91 | colorVals: @[0.25, 0.5, 0.75, 1.0],
92 | colormap: ColorMap.Viridis
93 | )
94 | expected = %*{ "size": 1.0,
95 | "color" : "#7F7F7F"
96 | }
97 | let r = %mk
98 | check r == expected
99 |
100 | test "ErrorBar":
101 | test "make ConstantSym ErrorBar, manual":
102 | let
103 | eb = ErrorBar[float](visible: true,
104 | color: color(0.0, 1.0, 1.0),
105 | thickness: 1.0,
106 | width: 1.0,
107 | kind: ErrorBarKind.ebkConstantSym,
108 | value: 2.0)
109 | expected = %*{ "visible": true,
110 | "color": "#00FFFF",
111 | "thickness": 1.0,
112 | "width" : 1.0,
113 | "symmetric" : true,
114 | "type": "constant",
115 | "value": 2.0
116 | }
117 | let r = %eb
118 | check r == expected
119 | test "make ConstantSym ErrorBar, newErrorBar":
120 | let
121 | eb = newErrorBar[float](2.0) # fields not given won't be serialized
122 | expected = %*{ "visible": true,
123 | "symmetric" : true,
124 | "type": "constant",
125 | "value": 2.0
126 | }
127 | let r = %eb
128 | check r == expected
129 |
130 | test "make PercentSym ErrorBar, manual":
131 | let
132 | eb = ErrorBar[float](visible: true,
133 | color: color(0.0, 1.0, 1.0),
134 | thickness: 1.0,
135 | width: 1.0,
136 | kind: ErrorBarKind.ebkPercentSym,
137 | percent: 5.0)
138 | expected = %*{ "visible": true,
139 | "color": "#00FFFF",
140 | "thickness": 1.0,
141 | "width" : 1.0,
142 | "symmetric" : true,
143 | "type": "percent",
144 | "value": 5.0
145 | }
146 | let r = %eb
147 | check r == expected
148 |
149 | test "make PercentSym ErrorBar, newErrorBar":
150 | let
151 | eb = newErrorBar[float](2.0, percent = true) # fields not given won't be serialized
152 | expected = %*{ "visible": true,
153 | "symmetric" : true,
154 | "type": "percent",
155 | "value": 2.0
156 | }
157 | let r = %eb
158 | check r == expected
159 |
160 | test "make ConstantAsym ErrorBar, manual":
161 | let
162 | eb = ErrorBar[float](visible: true,
163 | color: color(0.0, 1.0, 1.0),
164 | thickness: 1.0,
165 | width: 1.0,
166 | kind: ErrorBarKind.ebkConstantAsym,
167 | valuePlus: 2.0,
168 | valueMinus: 1.0)
169 | expected = %*{ "visible": true,
170 | "color": "#00FFFF",
171 | "thickness": 1.0,
172 | "width" : 1.0,
173 | "symmetric" : false,
174 | "type": "constant",
175 | "value": 2.0,
176 | "valueminus" : 1.0
177 | }
178 | let r = %eb
179 | check r == expected
180 | test "make ConstantAsym ErrorBar, newErrorBar":
181 | let
182 | eb = newErrorBar[float]((m: 1.0, p: 2.0)) # fields not given won't be serialized
183 | expected = %*{ "visible": true,
184 | "symmetric" : false,
185 | "type": "constant",
186 | "value": 2.0,
187 | "valueminus" : 1.0
188 | }
189 | let r = %eb
190 | check r == expected
191 |
192 | test "make PercentAsym ErrorBar, manual":
193 | let
194 | eb = ErrorBar[float](visible: true,
195 | color: color(0.0, 1.0, 1.0),
196 | thickness: 1.0,
197 | width: 1.0,
198 | kind: ErrorBarKind.ebkPercentAsym,
199 | percentPlus: 2.0,
200 | percentMinus: 1.0)
201 | expected = %*{ "visible": true,
202 | "color": "#00FFFF",
203 | "thickness": 1.0,
204 | "width" : 1.0,
205 | "symmetric" : false,
206 | "type": "percent",
207 | "value": 2.0,
208 | "valueminus" : 1.0
209 | }
210 | let r = %eb
211 | check r == expected
212 | test "make ConstantAsym ErrorBar, newErrorBar":
213 | let
214 | eb = newErrorBar[float]((m: 1.0, p: 2.0), percent = true) # fields not given won't be serialized
215 | expected = %*{ "visible": true,
216 | "symmetric" : false,
217 | "type": "percent",
218 | "value": 2.0,
219 | "valueminus" : 1.0
220 | }
221 | let r = %eb
222 | check r == expected
223 |
224 | test "make Sqrt ErrorBar, manual":
225 | let
226 | eb = ErrorBar[float](visible: true,
227 | color: color(0.0, 1.0, 1.0),
228 | thickness: 1.0,
229 | width: 1.0,
230 | kind: ErrorBarKind.ebkSqrt)
231 | expected = %*{ "visible": true,
232 | "color": "#00FFFF",
233 | "thickness": 1.0,
234 | "width" : 1.0,
235 | "type": "sqrt",
236 | }
237 | let r = %eb
238 | check r == expected
239 | test "make Sqrt ErrorBar, newErrorBar":
240 | let
241 | eb = newErrorBar[float]() # TODO: this proc should really be renamed!
242 | expected = %*{ "visible": true,
243 | "type": "sqrt",
244 | }
245 | let r = %eb
246 | check r == expected
247 |
248 | test "make ArraySym ErrorBar, manual":
249 | let
250 | eb = ErrorBar[float](visible: true,
251 | color: color(0.0, 1.0, 1.0),
252 | thickness: 1.0,
253 | width: 1.0,
254 | kind: ErrorBarKind.ebkArraySym,
255 | errors: @[1.0, 2.0, 3.0])
256 | expected = %*{ "visible": true,
257 | "color": "#00FFFF",
258 | "thickness": 1.0,
259 | "width" : 1.0,
260 | "symmetric": true,
261 | "type": "data",
262 | "array" : [1.0, 2.0, 3.0]
263 | }
264 | let r = %eb
265 | check r == expected
266 | test "make ArraySym ErrorBar, newErrorBar":
267 | let
268 | eb = newErrorBar[float](@[1.0, 2.0, 3.0]) # TODO: this proc should really be renamed!
269 | expected = %*{ "visible": true,
270 | "symmetric": true,
271 | "type": "data",
272 | "array": [1.0, 2.0, 3.0]
273 | }
274 | let r = %eb
275 | check r == expected
276 |
277 | test "make ArrayAsym ErrorBar, manual":
278 | let
279 | eb = ErrorBar[float](visible: true,
280 | color: color(0.0, 1.0, 1.0),
281 | thickness: 1.0,
282 | width: 1.0,
283 | kind: ErrorBarKind.ebkArrayAsym,
284 | errorsPlus: @[1.0, 2.0, 3.0],
285 | errorsMinus: @[2.0, 3.0, 4.0])
286 | expected = %*{ "visible": true,
287 | "color": "#00FFFF",
288 | "thickness": 1.0,
289 | "width" : 1.0,
290 | "symmetric": false,
291 | "type": "data",
292 | "array" : [1.0, 2.0, 3.0],
293 | "arrayminus" : [2.0, 3.0, 4.0]
294 | }
295 | let r = %eb
296 | check r == expected
297 | test "make ArrayAsym ErrorBar, newErrorBar":
298 | let
299 | eb = newErrorBar[float]((m: @[2.0, 3.0, 4.0], p: @[1.0, 2.0, 3.0]))
300 | expected = %*{ "visible": true,
301 | "symmetric": false,
302 | "type": "data",
303 | "array": [1.0, 2.0, 3.0],
304 | "arrayminus": [2.0, 3.0, 4.0]
305 | }
306 | let r = %eb
307 | check r == expected
308 |
309 | test "Annotation":
310 | test "make Json object":
311 | let
312 | a = Annotation(x:1, xshift:10, y:2, yshift:20, text:"text")
313 | expected = %*{ "x": 1.0
314 | , "xshift": 10.0
315 | , "y": 2.0
316 | , "yshift": 20.0
317 | , "text": "text"
318 | , "showarrow": false
319 | }
320 | let r = %a
321 | check r == expected
322 | test "make Json object less parameters":
323 | let
324 | a = Annotation(x:1,y:2,text:"text")
325 | expected = %*{ "x": 1.0
326 | , "xshift": 0.0
327 | , "y": 2.0
328 | , "yshift": 0.0
329 | , "text": "text"
330 | , "showarrow": false
331 | }
332 | let r = %a
333 | check r == expected
334 |
335 | test "Layout":
336 | test "Layout with Annotations":
337 | let
338 | a = Annotation(x:1, xshift:10, y:2, yshift:20, text:"text")
339 | layout = Layout(title: "title", width: 10, height: 10,
340 | xaxis: Axis(title: "x"),
341 | yaxis: Axis(title: "y"),
342 | annotations: @[a],
343 | autosize: true)
344 | expected = %*{ "title": "title"
345 | , "width": 10
346 | , "height": 10
347 | , "xaxis": { "title": "x"
348 | , "autorange": true
349 | }
350 | , "yaxis": { "title": "y"
351 | , "autorange": true
352 | }
353 | , "hovermode": "closest"
354 | , "annotations": [ { "x": 1.0
355 | , "xshift": 10.0
356 | , "y": 2.0
357 | , "yshift": 20.0
358 | , "text": "text"
359 | , "showarrow": false
360 | }
361 | ]
362 | }
363 | let r = %layout
364 | check r == expected
365 | test "Layout without Annotations":
366 | let
367 | layout = Layout(title: "title", width: 10, height: 10,
368 | xaxis: Axis(title: "x"),
369 | yaxis: Axis(title: "y"),
370 | autosize: true)
371 | expected = %*{ "title": "title"
372 | , "width": 10
373 | , "height": 10
374 | , "xaxis": { "title": "x"
375 | , "autorange": true
376 | }
377 | , "yaxis": { "title": "y"
378 | , "autorange": true
379 | }
380 | , "hovermode": "closest"
381 | }
382 | let r = %layout
383 | check r == expected
384 | test "Layout with log axis":
385 | let
386 | a = Annotation(x:1, xshift:10, y:2, yshift:20, text:"text")
387 | layout = Layout(title: "title", width: 10, height: 10,
388 | xaxis: Axis(title: "x"),
389 | yaxis: Axis(title: "y", ty: AxisType.Log),
390 | annotations: @[a],
391 | autosize: true)
392 | expected = %*{ "title": "title"
393 | , "width": 10
394 | , "height": 10
395 | , "xaxis": { "title": "x"
396 | , "autorange": true
397 | }
398 | , "yaxis": { "title": "y"
399 | , "type": "log"
400 | , "autorange": true
401 | }
402 | , "hovermode": "closest"
403 | , "annotations": [ { "x": 1.0
404 | , "xshift": 10.0
405 | , "y": 2.0
406 | , "yshift": 20.0
407 | , "text": "text"
408 | , "showarrow": false
409 | }
410 | ]
411 | }
412 | let r = %layout
413 | check r == expected
414 |
415 | suite "Sugar":
416 | test "Custom colormap comparisons":
417 | var data = newSeqWith(1, newSeq[float](1))
418 | data[0][0] = 1.5
419 | let d = Trace[float](mode: PlotMode.Lines, `type`: PlotType.HeatMap)
420 | d.zs = data
421 | proc customHeatmap(name: PredefinedCustomMaps): Plot[float] =
422 | d.customColormap = getCustomMap(name)
423 | d.colorMap = Custom
424 | let
425 | layout = Layout(title: $name, width: 800, height: 800,
426 | xaxis: Axis(title: "x"),
427 | yaxis: Axis(title: "y"), autosize: false)
428 | result = Plot[float](layout: layout, traces: @[d])
429 | proc customSugar(name: PredefinedCustomMaps): Plot[float] =
430 | result = heatmap(data)
431 | .title($name)
432 | .width(800)
433 | .height(800)
434 | .colormap(name)
435 |
436 | for map in PredefinedCustomMaps:
437 | let m1 = customHeatmap(map)
438 | let m2 = customSugar(map)
439 | check m1.layout.width == m2.layout.width
440 | check m1.layout.height == m2.layout.height
441 | check m1.layout.xaxis.title == m2.layout.xaxis.title
442 | check m1.layout.yaxis.title == m2.layout.yaxis.title
443 | check m1.traces[0].`type` == m1.traces[0].`type`
444 | check m1.traces[0].colormap == m1.traces[0].colormap
445 | check m1.traces[0].customColormap == m1.traces[0].customColormap
446 | check m1.traces[0].zs == data
447 | check m1.traces[0].zs == m2.traces[0].zs
448 | check m1.traces[0].customColormap.name == $map
449 | check m2.traces[0].customColormap.name == $map
450 |
451 | test "Limit colormap range":
452 | var data = newSeqWith(28, newSeq[float](28))
453 | for x in 0 ..< 28:
454 | for y in 0 ..< 28:
455 | data[x][y] = max(rand(30.0), 0.1)
456 | let
457 | layout = Layout()
458 | block:
459 | let d = Trace[float](mode: PlotMode.Lines, `type`: PlotType.HeatMap,
460 | zmin: 0.0, zmax: 10.0,
461 | zs: data)
462 | let plt = Plot[float](layout: layout, traces: @[d])
463 | let pltJson = % plt
464 | check pltJson["traces"][0]["zmin"] == % 0.0
465 | check pltJson["traces"][0]["zmax"] == % 10.0
466 | check pltJson["traces"][0]["zauto"] == % false
467 | block:
468 | let d = Trace[float](mode: PlotMode.Lines, `type`: PlotType.HeatMap,
469 | zs: data)
470 | let plt = Plot[float](layout: layout, traces: @[d])
471 | let pltJson = % plt
472 | check not hasKey(pltJson["traces"][0], "zmin")
473 | check not hasKey(pltJson["traces"][0], "zmax")
474 | check not hasKey(pltJson["traces"][0], "zauto")
475 |
476 | block:
477 | let pltJson = % heatmap(data)
478 | .zmin(0.0)
479 | .zmax(10.0)
480 | check pltJson["traces"][0]["zmin"] == % 0.0
481 | check pltJson["traces"][0]["zmax"] == % 10.0
482 | check pltJson["traces"][0]["zauto"] == % false
483 |
484 | suite "show w/ filename without threads fails compilation":
485 | template compileFails(body: untyped): untyped =
486 | when not compiles(body):
487 | true
488 | else:
489 | false
490 |
491 | let xs = toSeq(0 ..< 100).mapIt(it.float)
492 | let ys = xs.mapIt(it * it * it)
493 | let layout = Layout()
494 | let d = Trace[float](mode: PlotMode.Lines, `type`: PlotType.Scatter,
495 | xs: xs, ys: ys)
496 | let plt = Plot[float](layout: layout, traces: @[d])
497 |
498 | when not compileOption("threads"):
499 | ## NOTE: the following tests assume the test is compiled without `--threads:on`!
500 | test "Plot - saveImage fails":
501 | check compileFails(plt.saveImage("test.svg"))
502 |
503 | test "PlotJson - saveImage fails":
504 | check compileFails(plt.toPlotJson.saveImage("test.svg"))
505 |
506 | test "Plot - show w/ filename w/o threads:on fails":
507 | check compileFails(plt.show("test.svg"))
508 |
509 | test "PlotJson - show w/ filename w/o threads:on fails":
510 | check compileFails(plt.toPlotJson.show("test.svg"))
511 |
512 | test "Grid - show w/ filename w/o threads:on fails":
513 | var grid = createGrid(1)
514 | grid[0] = plt
515 | check compileFails(grid.show("test.svg"))
516 |
--------------------------------------------------------------------------------