├── .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 | [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](http://scinim.github.io/nim-plotly/) 4 | [![plotly CI](https://github.com/SciNim/nim-plotly/actions/workflows/ci.yml/badge.svg)](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 | ![simple scatter](https://user-images.githubusercontent.com/1739/39875828-e65293a8-542e-11e8-9b18-12130b8694c3.png) 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 | ![sizes and colors](https://user-images.githubusercontent.com/1739/39875826-e641acaa-542e-11e8-9c05-c936c112f36c.png) 54 | 55 | #### Multiple plot types 56 | 57 | [source](https://github.com/brentp/nim-plotly/blob/master/examples/fig3_multiple_plot_types.nim) 58 | 59 | ![multiple plot types](https://user-images.githubusercontent.com/1739/39875825-e62d5c0a-542e-11e8-83be-cdbfa18cfec9.png) 60 | 61 | #### Stacked Histogram 62 | 63 | [source](https://github.com/brentp/nim-plotly/blob/master/examples/fig7_stacked_histogram.nim) 64 | 65 | ![stacked histogram](https://user-images.githubusercontent.com/1739/40438473-66ce8a6e-5e75-11e8-8f27-79cef2752e52.png) 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 |
      1238 | 1239 |
    1240 |
  • 1241 |
  • 1242 | Funcs 1243 | 1250 |
  • 1251 | 1252 |
1253 | 1254 |
1255 |
1256 |
1257 |

1258 |
1259 |

Imports

1260 |
1261 | chroma 1262 |
1263 |
1264 |

Funcs

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