svelte-plots-basic 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2026 Sergey Kucheryavskiy
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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `svelte-plots-basic` is a [Svelte](https://svelte.dev) component library for making simple responsive 2D and 3D plots/charts. The plots are created by generating [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) inside HTML document.
4
4
 
5
- One can think of this library as "Lego" bricks for plots. It has two groups of components: for 2D and for 3D plots. The 2D components are more developed while 3D components remain quite basic. In addition to `svelte`, the package has one direct dependence, [mdatools-js](https://github.com/svkucheryavski/mdatools-js) library, which is used for vector/matrix operations, statistics, and other manipulations with data values.
5
+ One can think of this library as "Lego" bricks for plots. It has two groups of components: for 2D and for 3D plots. The 2D components are more developed while 3D components remain quite basic. In addition to `svelte`, the package requires the [mdatools-js](https://github.com/svkucheryavski/mdatools-js) library as a peer dependency. It is used for vector/matrix operations, statistics, and other manipulations with data values.
6
6
 
7
7
 
8
8
  ## Showcase
@@ -15,6 +15,32 @@ These websites and web-applications use `svelte-plots-basic` library:
15
15
 
16
16
  ## News
17
17
 
18
+ ### 4.0.0
19
+
20
+ * Added horizontal legend layout and improved legend sizing and alignment.
21
+ * Improved PNG/SVG export reliability, export-dialog accessibility, and clipboard handling.
22
+ * Strengthened validation, automatic axis ticks, marker rendering, and component cleanup.
23
+ * Made subscripts, superscripts, and tick-factor labels consistent across modern browsers.
24
+ * Updated to `mdatools ^1.5.0`, now declared as a peer dependency to prevent incompatible duplicate instances.
25
+
26
+ ### 3.4.0
27
+
28
+ * Added the `plotActions` property to `Axes` for adding custom buttons to the download panel.
29
+ * Added advanced validation for axis limits, text-label positions, heatmap breaks, and custom plot actions.
30
+ * Fixed text-label positioning and rotation when individual positions are provided.
31
+ * Fixed heatmaps with automatic, custom, or constant-value breaks.
32
+ * Added support for rectangular and constant-height 3D meshes.
33
+ * Improved PNG, SVG, and clipboard export by serializing a clone instead of modifying the visible plot.
34
+ * Fixed the advanced PNG export dialog so a default resolution is always selected, falling back to 300 dpi when necessary.
35
+ * Prevented plot controls from submitting an enclosing HTML form.
36
+ * Added package validation before publishing and synchronized Svelte 5 peer-dependency metadata.
37
+ * Fixed conditionally rendered axes, boxes, and legends so their parent state is cleared when they are removed.
38
+ * Improved heatmap interval processing and added automatically generated colormaps with more than 16 colors.
39
+ * Fixed `TextLegend` positioning at zero and strengthened finite-range validation for 3D axes.
40
+ * Reduced reactive and repeated allocation overhead for axis configuration, text measurement, PNG export, and colormap generation.
41
+ * Added dependency-free regression tests and made them run automatically before packaging.
42
+ * Documented the trust requirements for labels rendered as SVG/HTML markup.
43
+
18
44
  ### 3.3.0
19
45
 
20
46
  * Added advanced "save as PNG" option where user can select size and resolution of the image as well as change the filename.
@@ -57,6 +83,13 @@ In addition to Svelte 5 syntax and functionality, this release also introduces a
57
83
 
58
84
  * **Properites** — from 3.x.x the naming of the properties is more consistent. For example, before the library used properties `borderColor` and `borderWidth` for areas, rectangels, bars and markers, while for lines, segments and multilines the similar properties were named as `lineColor` and `lineWidth`. Now they all have prefix `line*` if it is something about lines or segments and `face*` if it is somthing inside a closed contour. So no more `border*` properties.
59
85
 
86
+
87
+ ## Security and label markup
88
+
89
+ Several title and label properties support SVG/HTML markup and are rendered as raw markup using Svelte's `{@html}` mechanism. This enables formatted labels, HTML entities, subscripts, and superscripts.
90
+
91
+ Only pass trusted content to these properties. Do not pass unsanitized user input or external data directly as a plot title, axis label, tick label, legend label, or text-label value. Escape untrusted text, or sanitize it with an allow-list appropriate for SVG markup, before passing it to a component.
92
+
60
93
  ## Installation
61
94
 
62
95
  The set up process is similar to any other Svelte component library. Just use:
@@ -140,6 +173,7 @@ Here is a table with properties of the component related to saving plot to a fil
140
173
  `pngRes` | `300` | resolution of PNG image (pixels per inch)
141
174
  `clipboardWidth` | `1200` | width of image in clipboard in pixels
142
175
  `clipboardHeight` | `800` | height of image in clipboard in pixels
176
+ `plotActions` | `[]` | array of `{ text, callback }` objects for adding custom buttons to the download panel.
143
177
 
144
178
  Please pay attention that most of the browsers enable clipboard functionality only if website is available via HTTPS.
145
179
 
@@ -194,7 +228,7 @@ You can also define tick positions and corresponding labels manually. Tick posit
194
228
 
195
229
  ```svelte
196
230
  <Axes limX={[2019, 2026]} limY={[0, 60000]}>
197
- <Xaxis ticks={[2023, 2024, 2025]} tickLabels={['Past', 'Present', 'Future']} />
231
+ <XAxis ticks={[2023, 2024, 2025]} tickLabels={['Past', 'Present', 'Future']} />
198
232
  </Axes>
199
233
  ```
200
234
 
@@ -594,7 +628,7 @@ Here is an example:
594
628
 
595
629
  ```svelte
596
630
  <script>
597
- import { Axes, Xaxis, YAxis, Box, Area } from 'svelte-plots-basic/2d';
631
+ import { Axes, XAxis, YAxis, Box, Area } from 'svelte-plots-basic/2d';
598
632
 
599
633
  const xValues = [3, 2, 2.5, 4, 5.5, 6, 5];
600
634
  const yValues = [1, 3, 4, 5, 4, 3, 1];
@@ -651,7 +685,7 @@ Here is an example:
651
685
 
652
686
  ```svelte
653
687
  <script>
654
- import { Axes, Xaxis, YAxis, Box, Heatmap } from 'svelte-plots-basic/2d';
688
+ import { Axes, XAxis, YAxis, Box, Heatmap } from 'svelte-plots-basic/2d';
655
689
  import { Matrix } from 'mdatools/arrays';
656
690
 
657
691
  // create 5 x 10 matrix filled with normally distributed random values
@@ -703,7 +737,7 @@ Colormap legend can be used together with `<Heatmap>` or any other color groupin
703
737
 
704
738
  ```svelte
705
739
  <script>
706
- import { Axes, Xaxis, YAxis, Box, Heatmap, ColormapLegend } from 'svelte-plots-basic/2d';
740
+ import { Axes, XAxis, YAxis, Box, Heatmap, ColormapLegend } from 'svelte-plots-basic/2d';
707
741
  import { Matrix } from 'mdatools/arrays';
708
742
 
709
743
  // create 5 x 10 matrix filled with normally distributed random values
@@ -763,16 +797,16 @@ Here is an example:
763
797
  const y3 = x.apply(v => Math.pow(v, 4));
764
798
 
765
799
  // define properties of each series
766
- line1Props = {lineType: 3, lineColor: 'red'};
767
- line2Props = {lineColor: 'blue', lineType: 1};
768
- point2Props = {lineColor: 'blue', faceColor: 'white'};
769
- point3Props = {marker: 5, lineColor: 'green', faceColor: 'yellow'};
800
+ const line1Props = {lineType: 3, lineColor: 'red'};
801
+ const line2Props = {lineColor: 'blue', lineType: 1};
802
+ const point2Props = {lineColor: 'blue', faceColor: 'white'};
803
+ const point3Props = {marker: 5, lineColor: 'green', faceColor: 'yellow'};
770
804
 
771
805
  // legend items
772
- items = [
773
- {label: 'y=x^2', line: lines1Props },
774
- {label: "y=x^3", line: lines2Props, point: points2Props },
775
- {label: "y=x^4", point: points2Props },
806
+ const items = [
807
+ {label: 'y=x^2', line: line1Props },
808
+ {label: "y=x^3", line: line2Props, point: point2Props },
809
+ {label: "y=x^4", point: point3Props },
776
810
  ];
777
811
  </script>
778
812
 
@@ -786,7 +820,7 @@ Here is an example:
786
820
  <Points xValues={x} yValues={y2} {...point2Props} />
787
821
 
788
822
  <!-- series 3: markers in form of diamonds with green stroke and yellow fill -->
789
- <Points xValues={x} yValues={y3} {...points3Props} />
823
+ <Points xValues={x} yValues={y3} {...point3Props} />
790
824
 
791
825
  <!-- legend with one JSON for each series -->
792
826
  <Legend position="right" {items} />
@@ -800,12 +834,21 @@ Check this example in Svelte REPL: [plots-legend](https://svelte.dev/playground/
800
834
 
801
835
  The `position` parameter can be one of the follows: `'topleft'`, `'top'`, `'topright'`, `'right'`, `'bottomright'` and so on.
802
836
 
837
+ By default, legend items are arranged vertically. Set `orientation="horizontal"` to arrange them in a single row, which is especially useful with the `top` and `bottom` positions:
838
+
839
+ ```svelte
840
+ <Legend position="top" orientation="horizontal" {items} />
841
+ ```
842
+
843
+ The horizontal layout does not wrap items onto additional rows, so a large number of items or long labels may require more plot width.
844
+
803
845
  The component has also properties which changes it apearance, here is the full list:
804
846
 
805
847
  Property name | Default value | Description
806
848
  --|--|--
807
849
  `items` | | array with JSON properties of legend items
808
850
  `position` | `'topleft'` | position of the legend element inside plotting area
851
+ `orientation` | `'vertical'` | arrangement of legend items: `'vertical'` or `'horizontal'`
809
852
  `lineColor` | `'#303030'` | color of the legend box line
810
853
  `lineWidth` | `1` | width (thickness) of the legend box line
811
854
  `faceColor` | `'#fff'` | background color of the legend box
@@ -841,15 +884,12 @@ Here is an example of simple 3D scatter plot:
841
884
  <Axes limX={[-10, 10]} limY={[-10, 10]} limZ={[-10, 10]} {zoom} {phi} {theta}>
842
885
  <Points {xValues} {yValues} {zValues} />
843
886
 
844
- <XAxis showGrid={true} title="X" />
845
- <YAxis showGrid={true} title="Y" />
846
- <ZAxis showGrid={true} title="Z" />
887
+ <XAxis showGrid={true} label="X" />
888
+ <YAxis showGrid={true} label="Y" />
889
+ <ZAxis showGrid={true} label="Z" />
847
890
  </Axes>
848
891
  ```
849
892
 
850
893
  You can also add `Lines`, `Segments` and `Mesh` series to 3D plots.
851
894
 
852
895
  Check more advanced example in Svelte REPL with mouse and keyboard support, and more: [plots-3d](https://svelte.dev/playground/2294eecba7d7477ab9a09c1734d32ac2)
853
-
854
-
855
-
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "svelte-plots-basic",
3
3
  "description": "Svelte components for creating simple plots",
4
- "version": "3.3.0",
4
+ "version": "4.0.0",
5
5
  "author": "Sergey Kucheryavskiy",
6
6
  "license": "MIT",
7
7
  "private": false,
8
+ "type": "module",
9
+ "scripts": {
10
+ "check": "node scripts/check.cjs",
11
+ "test": "node --test",
12
+ "prepack": "npm run check && npm test"
13
+ },
8
14
  "exports": {
9
15
  "./constants": "./src/constants.js",
10
16
  "./utils": "./src/methods.js",
@@ -14,10 +20,12 @@
14
20
  "./3d/*.svelte": "./src/3d/*.svelte"
15
21
  },
16
22
  "devDependencies": {
17
- "svelte": "^5.x.x"
23
+ "mdatools": "^1.5.0",
24
+ "svelte": "^5.56.8"
18
25
  },
19
- "dependencies": {
20
- "mdatools": "^1.x.x"
26
+ "peerDependencies": {
27
+ "mdatools": "^1.5.0",
28
+ "svelte": "^5.0.0"
21
29
  },
22
30
  "keywords": [
23
31
  "svelte",
@@ -2,12 +2,12 @@
2
2
  @component Adds a closed polygon (area).
3
3
 
4
4
  Main properties:
5
- - `xValues` - array of vector with x-coordinates of the polygon's points.
6
- - `yValues` - array of vector with y-coordinates of the polygon's points.
7
- - `lineWidth` - width (thickness) of the polygon's line in pixels, defailt: `1`.
5
+ - `xValues` - array or vector with x-coordinates of the polygon's points.
6
+ - `yValues` - array or vector with y-coordinates of the polygon's points.
7
+ - `lineWidth` - width (thickness) of the polygon's line in pixels, default: `1`.
8
8
  - `lineColor` - line color, default: `Colors.PRIMARY`.
9
9
  - `lineType` - type of line (`1` - solid, `2` - dashed, `3` - dotted, `4` - dashdot).
10
- - `facColor` - face (fill) color of the polygon, default: `'transparent'`.
10
+ - `faceColor` - face (fill) color of the polygon, default: `'transparent'`.
11
11
  - `opacity` - opacity of the face color, default: `1`.
12
12
  - `onclick` - function (callback) to be called when user clicks on a polygon.
13
13
 
@@ -29,17 +29,16 @@
29
29
  <script>
30
30
  import { getContext } from 'svelte';
31
31
  import { Colors } from '../constants';
32
- import { val2p, checkCoords, handleClick, transformCoords } from '../methods';
32
+ import { val2p, checkCoords, handleClick, transformCoords, normalizeLineType } from '../methods';
33
33
  import { LINE_STYLES } from '../constants';
34
34
 
35
- /** @type {Props} */
36
35
  let {
37
36
  xValues, // vector or array with x-coordinates of the points
38
37
  yValues, // vector or array with y-coordinates of the points
39
38
  lineColor = Colors.PRIMARY, // color of segment lines
40
39
  lineType = 1, // type of segment lines (1 - solid, 2 - dashed, 3 - dotted, 4 - dashdot)
41
40
  lineWidth = 1, // width (thickness) of segment lines
42
- faceColor = 'transparent', // color of segment lines
41
+ faceColor = 'transparent', // face (fill) color of the polygon
43
42
  opacity = 1, // opacity of the area color
44
43
  onclick, // function to be called if onclick event fires
45
44
  } = $props();
@@ -47,6 +46,7 @@
47
46
  // check user provided coordinates
48
47
  const xv = $derived(checkCoords(xValues, 'Area'));
49
48
  const yv = $derived(xv ? checkCoords(yValues, 'Area', xv.length) : null);
49
+ const validLineType = $derived(normalizeLineType(lineType, 'Area'));
50
50
 
51
51
  // get axes context and compute coordinates of polyline
52
52
  const axes = getContext('axes');
@@ -56,7 +56,7 @@
56
56
  const xs = $derived(xv ? transformCoords(xv.subset(1), axes.tX()) : null);
57
57
  const pa = $derived(p && xs && ys ? xs + ',' + ys + ' ' + p + ' ' + xs + ',' + ys : null);
58
58
 
59
- const areaStyleStr = $derived(`opacity:${opacity};fill:${faceColor};stroke:${lineColor};stroke-width: ${lineWidth}px;stroke-dasharray:${LINE_STYLES[axes.scales().plot][lineType - 1]}`);
59
+ const areaStyleStr = $derived(`opacity:${opacity};fill:${faceColor};stroke:${lineColor};stroke-width: ${lineWidth}px;stroke-dasharray:${LINE_STYLES[axes.scales().plot][validLineType - 1]}`);
60
60
  </script>
61
61
 
62
62
  {#if pa}
@@ -66,4 +66,3 @@
66
66
  <polygon points={pa}/>
67
67
  </g>
68
68
  {/if}
69
-
@@ -5,7 +5,7 @@
5
5
  - `limX` - array with limits for x-axis (in plot units), default: `[0, 1]`.
6
6
  - `limY` - array with limits for y-axis (in plot units), default: `[0, 1]`.
7
7
  - `title` - title of the plot (string).
8
- - `margins` - array with relative margins (bottom, left, top, right), default `[1.0, 0.85, 0.5, 0.5]`.
8
+ - `margins` - array with relative margins (bottom, left, top, right), default `[1.0, 1.1, 0.55, 0.60]`.
9
9
 
10
10
  Properties for downloading the plot as PNG or SVG file or copying it to clipboard:
11
11
  - `downloadLinks` - how to show download links panel (`'none'` - default, `'hover'`, `'fixed'`).
@@ -13,8 +13,8 @@
13
13
  - `pngWidth` - width of PNG image in cm, default `8`
14
14
  - `pngHeight` - height of PNG image in cm, default `8`.
15
15
  - `pngRes` - resolution of PNG image (pixels per inch), default `300`.
16
- - `clipboardWidth` - width of plot image to copy to clipboard in pixels.
17
- - `clipboardHeight` - height of plot image to copy to clipboard in pixels.Axes
16
+ - `clipboardWidth` - width of plot image to copy to clipboard in pixels, default `1200`.
17
+ - `clipboardHeight` - height of plot image to copy to clipboard in pixels, default `800`.
18
18
 
19
19
  Example:
20
20
 
@@ -23,14 +23,14 @@
23
23
  import {Axes} from 'svelte-plots-basic/2d';
24
24
  </script>
25
25
 
26
- <Axes limX={[2000, 2050]} limY={[0, 100]} title="GDP of contries" >
26
+ <Axes limX={[2000, 2050]} limY={[0, 100]} title="GDP of countries" >
27
27
  </Axes>
28
28
  ```
29
29
  -->
30
30
  <script>
31
31
  import { setContext } from 'svelte';
32
32
  import { Colors, PLOT_FONT_SIZE, AXES_MARGIN_FACTORS, TICK_SIZE, LINE_STYLES, MARKER_SYMBOLS } from '../constants.js';
33
- import { downloadSVG, downloadPNG, copyToClipboard, checkArray, getScale, getAxisScale, getXAxisParams, getYAxisParams,
33
+ import { downloadSVG, downloadPNG, copyToClipboard, getScale, getAxisScale, getXAxisParams, getYAxisParams,
34
34
  transformCoords, getColormapLegendParams, getColormapLegendCoords, getTickFactorLabel,
35
35
  getGroupLegendCoords, text2svg,
36
36
  invTransformCoords } from '../methods.js';
@@ -46,6 +46,7 @@
46
46
  limY = [0, 1],
47
47
  margins = [1.0, 1.1, 0.55, 0.60],
48
48
  downloadLinks = 'none',
49
+ plotActions = [],
49
50
  fileName = 'plot',
50
51
  pngWidth = 8,
51
52
  pngHeight = 8,
@@ -60,9 +61,47 @@
60
61
  } = $props();
61
62
 
62
63
 
64
+ const actions = $derived.by(() => {
65
+ if (plotActions == null) return [];
66
+
67
+ if (!Array.isArray(plotActions)) {
68
+ console.error('Axes: "plotActions" must be an array.');
69
+ return [];
70
+ }
71
+
72
+ return plotActions.filter((action, index) => {
73
+ const isValid =
74
+ action &&
75
+ typeof action.text === 'string' &&
76
+ typeof action.callback === 'function';
77
+
78
+ if (!isValid) {
79
+ console.error(
80
+ `Axes: plot action ${index + 1} must contain a text string and callback function.`
81
+ );
82
+ }
83
+
84
+ return isValid;
85
+ });
86
+ });
87
+
88
+
89
+ // Check that axis limits contain a finite, meaningful numeric range.
90
+ function hasUsableRange(lim) {
91
+ if (!Array.isArray(lim) || lim.length !== 2) return false;
92
+ if (!lim.every(v => typeof v === 'number' && Number.isFinite(v))) return false;
93
+
94
+ const span = Math.abs(lim[1] - lim[0]);
95
+ const scale = Math.max(Math.abs(lim[0]), Math.abs(lim[1]));
96
+
97
+ return Number.isFinite(1 / span) &&
98
+ span > Number.EPSILON * scale * 16;
99
+ }
100
+
101
+
63
102
  /* handler for mouse events */
64
103
  function handleMouse(e, f) {
65
- if (f && e.target.id == "axes-box") {
104
+ if (f && e.target.id === "axes-box") {
66
105
  const x = invTransformCoords([e.offsetX - left], tX)[0];
67
106
  const y = invTransformCoords([e.offsetY - top], tY)[0];
68
107
  f(x, y);
@@ -138,8 +177,8 @@
138
177
 
139
178
  // plot status
140
179
  const isOk = $derived(
141
- checkArray(limX, 2) &&
142
- checkArray(limY, 2) &&
180
+ hasUsableRange(limX) &&
181
+ hasUsableRange(limY) &&
143
182
  width > (pxMargins[1] + pxMargins[3]) &&
144
183
  height > (pxMargins[0] + pxMargins[2])
145
184
  );
@@ -175,11 +214,11 @@
175
214
  const titleHeight = $derived(plotTitle ? (plotTitle.length === title.length ? 1.0 : 1.4) : 0);
176
215
 
177
216
  // default parameters of box, axis elements, colomap legend and group legend
178
- let box = $state({show: false});
179
- let xaxis = $state({show: false, error: ''});
180
- let yaxis = $state({show: false, error: ''});
181
- let clg = $state({show: false, colmap: null, breaks: null});
182
- let glg = $state({show: false, items: null, position: null});
217
+ let box = $state.raw({show: false});
218
+ let xaxis = $state.raw({show: false, error: ''});
219
+ let yaxis = $state.raw({show: false, error: ''});
220
+ let clg = $state.raw({show: false, colmap: null, breaks: null});
221
+ let glg = $state.raw({show: false, items: null, position: null});
183
222
 
184
223
  // set colormap legend parameters if requested
185
224
  const clgParams = $derived(isOk && clg && clg.show ? getColormapLegendParams(clg) : null);
@@ -268,7 +307,7 @@
268
307
  <text {x} y={coords.ltop} dx="1.2em" dy="-0.25em" dominant-baseline="hanging"
269
308
  fill={params.labelColor}
270
309
  font-size="{params.fontSize}em"
271
- text-anchor="end"
310
+ text-anchor="start"
272
311
  >{@html getTickFactorLabel(params.labelsFactor)}</text>
273
312
  {/if}
274
313
  {/snippet}
@@ -277,14 +316,16 @@
277
316
  {#snippet glgSnippet(params, coords)}
278
317
  {@const elw = coords.elw + 2 * coords.elp}
279
318
  {@const elp = coords.elp}
319
+ {@const elx = coords.elx}
280
320
  {@const elh = coords.elh}
281
321
  {@const ely = coords.ely}
322
+ {@const eliw = coords.eliw}
282
323
 
283
324
  <svg x={coords.lgl} y={coords.lgt} height={coords.lgh} width={coords.lgw}>
284
325
  <rect height="100%" width="100%" fill={params.faceColor} stroke={params.lineColor} stroke-width={params.lineWidth} />
285
326
 
286
327
  {#each params.items as item, i}
287
- <svg x={0} y={ely[i]} width={coords.lgw} height={elh[i]}>
328
+ <svg x={elx[i]} y={ely[i]} width={eliw[i]} height={elh[i]}>
288
329
 
289
330
  <!-- line -->
290
331
  {#if item.line}
@@ -445,6 +486,8 @@
445
486
 
446
487
  <!-- box -->
447
488
  <g class="axes-box">
489
+ <!-- The plot callback requires real pointer coordinates; a keyboard-generated coordinate would be arbitrary. -->
490
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
448
491
  <rect stroke={box.show ? box.lineColor : 'transparent'}
449
492
  stroke-width={box.show ? box.lineWidth + 'px' : '0'}
450
493
  fill="transparent"
@@ -491,10 +534,16 @@
491
534
  </p>
492
535
  {:else}
493
536
  <div class="download-links">
494
- <button onclick={handleClickSVG}>⇩ svg</button>
495
- <button onclick={handleClickPNG}>⇩ png</button>
496
- <button onclick={handleClickPNGAdvanced}>⇩ png+</button>
497
- <button onclick={handleClickCopy}>⧉ copy</button>
537
+ <button type="button" onclick={handleClickSVG}>↓ svg</button>
538
+ <button type="button" onclick={handleClickPNG}>↓ png</button>
539
+ <button type="button" onclick={handleClickPNGAdvanced}>↓ png+</button>
540
+ <button type="button" onclick={handleClickCopy}>⧉ copy</button>
541
+ {#if actions.length > 0}
542
+ <span class="divider">⋮</span>
543
+ {#each actions as ac}
544
+ <button type="button" onclick={ac.callback}>{ac.text}</button>
545
+ {/each}
546
+ {/if}
498
547
  </div>
499
548
  {/if}
500
549
 
@@ -545,6 +594,9 @@
545
594
  background: #fafafa;
546
595
  border-radius: 0.35em;
547
596
  padding: 0.25em 0.5em;
597
+ margin: 0 1px;
598
+
599
+ line-height: 1.25;
548
600
  }
549
601
 
550
602
  .download-links > button:hover{
@@ -560,6 +612,12 @@
560
612
  background: #4488ee;
561
613
  }
562
614
 
615
+ .download-links > .divider {
616
+ display: inline-block;
617
+ width: 1em;
618
+ text-align: center;
619
+ color: #909090;
620
+ }
563
621
 
564
622
  /* Plot container */
565
623
  .plot-container {
@@ -617,4 +675,4 @@
617
675
  min-width: 100%;
618
676
  }
619
677
 
620
- </style>
678
+ </style>
@@ -3,21 +3,19 @@
3
3
 
4
4
  Main properties:
5
5
  - `lineCoords` - coordinates of the line segments as 2D array.
6
- - `lineColor` - color of the box line, default: `Colors.DARKGRAY`.
7
- - `lineWidth` - width of box line in pixels, default: `1`.
6
+ - `lineColor` - color of the line segments, default: `Colors.DARKGRAY`.
7
+ - `lineWidth` - width of the line segments in pixels, default: `1`.
8
8
  - `lineType` - line type (`1` - solid (default), `2` - dashed, `3` - dotted, `4` - dashdot).
9
9
  - `className` - string with classname to be added to `<g></g>` tag wrapping the lines.
10
10
 
11
11
  **Description:**
12
12
 
13
- The coordinates msut be provided in form of nested arrays. Each element of this array contains
13
+ The coordinates must be provided in form of nested arrays. Each element of this array contains
14
14
  `Vector` or `Array` with the coordinates in world (not screen) coordinate system:
15
15
  - `lineCoords[0][0]` - x-coordinates of start points of segments.
16
16
  - `lineCoords[1][0]` - x-coordinates of end points of segments.
17
17
  - `lineCoords[0][1]` - y-coordinates of start points of segments.
18
18
  - `lineCoords[1][1]` - y-coordinates of end points of segments.
19
-
20
- ```
21
19
  -->
22
20
  <script>
23
21
  import { getContext } from 'svelte';
@@ -33,7 +31,7 @@
33
31
  className = '' // CSS class name for the component
34
32
  } = $props();
35
33
 
36
- // get axes context and adjust x margins
34
+ // get axes context for coordinate transforms
37
35
  const axes = getContext('axes');
38
36
 
39
37
  // reactive variables for coordinates of axis lines
@@ -16,8 +16,6 @@
16
16
  - `tickCoords[1][0]` - x-coordinates of end points of tick segments.
17
17
  - `tickCoords[0][1]` - y-coordinates of start points of tick segments.
18
18
  - `tickCoords[1][1]` - y-coordinates of end points of tick segments.
19
-
20
- ```
21
19
  -->
22
20
  <script>
23
21
 
@@ -57,7 +55,7 @@
57
55
  {/each}
58
56
  {:else if las === 2 && pos === 1}
59
57
  {#each x as v, i}
60
- <text data-id={i} x={x[i]} y={y[i]} dx={-dy/2} dy={0} transform={`rotate(-90, ${x[i]}, ${y[i]})`} text-anchor={"end"}>{@html tickLabels[i]}</text>
58
+ <text data-id={i} x={x[i]} y={y[i]} dx={-dy/2} dy={0} transform={`rotate(-90, ${x[i]}, ${y[i]})`} text-anchor="end">{@html tickLabels[i]}</text>
61
59
  {/each}
62
60
  {:else}
63
61
  {#each x as v, i}
@@ -34,7 +34,6 @@
34
34
  import Rectangles from './Rectangles.svelte';
35
35
 
36
36
 
37
- /** @type {Props} */
38
37
  let {
39
38
  xValues, // vector with x-coordinates of middle points of the bars.
40
39
  yValues, // vector with y-coordinates of top points of the bars.
@@ -46,25 +45,46 @@
46
45
  onclick
47
46
  } = $props();
48
47
 
48
+ function toNumber(value) {
49
+ const canConvert =
50
+ typeof value === 'number' ||
51
+ (typeof value === 'string' && value.trim() !== '');
52
+
53
+ return canConvert ? Number(value) : NaN;
54
+ }
55
+
49
56
  const bw = $derived.by(() => {
50
- if (barWidth <= 0 || barWidth > 1) {
51
- console.error('Bars: parameter "barWidth" should be between 0 and 1.');
57
+ const value = toNumber(barWidth);
58
+ if (!Number.isFinite(value) || value <= 0 || value > 1) {
59
+ console.error('Bars: parameter "barWidth" must be a finite number greater than 0 and no greater than 1.');
60
+ return null;
61
+ }
62
+ return value;
63
+ });
64
+
65
+ const exactWidth = $derived.by(() => {
66
+ if (barWidthExact == null) return {provided: false, value: null};
67
+
68
+ const value = toNumber(barWidthExact);
69
+ if (!Number.isFinite(value) || value <= 0) {
70
+ console.error('Bars: parameter "barWidthExact" must be a finite positive number.');
52
71
  return null;
53
72
  }
54
- return barWidth;
73
+
74
+ return {provided: true, value};
55
75
  });
56
76
 
57
77
  const x = $derived.by(() => {
58
- if (!bw) return null;
78
+ if (bw === null || exactWidth === null) return null;
59
79
  const xv = checkCoords(xValues, 'BarSeries');
60
80
  if (!xv) return null;
61
81
  const n = xv.length;
62
82
  if (n < 1) return null;
63
83
 
64
84
  let w;
65
- if (barWidthExact) {
66
- w = barWidthExact;
67
- } else if (n == 1) {
85
+ if (exactWidth.provided) {
86
+ w = exactWidth.value;
87
+ } else if (n === 1) {
68
88
  console.error('Bars: if only one bar must be shown, value for property "barWidthExact" should be provided.');
69
89
  return null;
70
90
  } else {
@@ -75,7 +95,7 @@
75
95
  }
76
96
  }
77
97
 
78
- w = w * barWidth;
98
+ w = w * bw;
79
99
  const whalf = w / 2;
80
100
  const left = Vector.zeros(n);
81
101
  const width = Vector.zeros(n);
@@ -114,6 +134,6 @@
114
134
  </script>
115
135
 
116
136
  {#if isOk}
117
- <Rectangles className="series series-bar" left={x.left} width={x.width} top={y.top} height={y.height}
137
+ <Rectangles className="series-bar" left={x.left} width={x.width} top={y.top} height={y.height}
118
138
  lineWidth={lineWidth} {lineColor} {faceColor} {onclick} />
119
- {/if}
139
+ {/if}
package/src/2d/Box.svelte CHANGED
@@ -16,7 +16,7 @@
16
16
  ```
17
17
  -->
18
18
  <script>
19
- import { getContext } from 'svelte';
19
+ import { getContext, onDestroy } from 'svelte';
20
20
 
21
21
  let {lineWidth = 1, lineColor = '#606060'} = $props();
22
22
  const axes = getContext('axes');
@@ -24,5 +24,8 @@
24
24
  $effect(() => {
25
25
  axes.setBox({show:true, lineWidth, lineColor});
26
26
  });
27
- </script>
28
27
 
28
+ onDestroy(() => {
29
+ axes.setBox({show: false});
30
+ });
31
+ </script>
@@ -34,7 +34,7 @@
34
34
  ```
35
35
  -->
36
36
  <script>
37
- import { getContext } from 'svelte';
37
+ import { getContext, onDestroy } from 'svelte';
38
38
  import { Colors } from '../constants';
39
39
 
40
40
 
@@ -52,7 +52,10 @@
52
52
  $effect(() => {
53
53
  axes.setColmapLegend({show, breaks, colmap, labels, labelColor, fontSize})
54
54
  });
55
- </script>
56
55
 
56
+ onDestroy(() => {
57
+ axes.setColmapLegend({show: false});
58
+ });
59
+ </script>
57
60
 
58
61