svelte-plots-basic 3.4.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/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,14 @@ 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
+
18
26
  ### 3.4.0
19
27
 
20
28
  * Added the `plotActions` property to `Axes` for adding custom buttons to the download panel.
@@ -220,7 +228,7 @@ You can also define tick positions and corresponding labels manually. Tick posit
220
228
 
221
229
  ```svelte
222
230
  <Axes limX={[2019, 2026]} limY={[0, 60000]}>
223
- <Xaxis ticks={[2023, 2024, 2025]} tickLabels={['Past', 'Present', 'Future']} />
231
+ <XAxis ticks={[2023, 2024, 2025]} tickLabels={['Past', 'Present', 'Future']} />
224
232
  </Axes>
225
233
  ```
226
234
 
@@ -620,7 +628,7 @@ Here is an example:
620
628
 
621
629
  ```svelte
622
630
  <script>
623
- import { Axes, Xaxis, YAxis, Box, Area } from 'svelte-plots-basic/2d';
631
+ import { Axes, XAxis, YAxis, Box, Area } from 'svelte-plots-basic/2d';
624
632
 
625
633
  const xValues = [3, 2, 2.5, 4, 5.5, 6, 5];
626
634
  const yValues = [1, 3, 4, 5, 4, 3, 1];
@@ -677,7 +685,7 @@ Here is an example:
677
685
 
678
686
  ```svelte
679
687
  <script>
680
- import { Axes, Xaxis, YAxis, Box, Heatmap } from 'svelte-plots-basic/2d';
688
+ import { Axes, XAxis, YAxis, Box, Heatmap } from 'svelte-plots-basic/2d';
681
689
  import { Matrix } from 'mdatools/arrays';
682
690
 
683
691
  // create 5 x 10 matrix filled with normally distributed random values
@@ -729,7 +737,7 @@ Colormap legend can be used together with `<Heatmap>` or any other color groupin
729
737
 
730
738
  ```svelte
731
739
  <script>
732
- 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';
733
741
  import { Matrix } from 'mdatools/arrays';
734
742
 
735
743
  // create 5 x 10 matrix filled with normally distributed random values
@@ -789,16 +797,16 @@ Here is an example:
789
797
  const y3 = x.apply(v => Math.pow(v, 4));
790
798
 
791
799
  // define properties of each series
792
- line1Props = {lineType: 3, lineColor: 'red'};
793
- line2Props = {lineColor: 'blue', lineType: 1};
794
- point2Props = {lineColor: 'blue', faceColor: 'white'};
795
- 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'};
796
804
 
797
805
  // legend items
798
- items = [
799
- {label: 'y=x^2', line: lines1Props },
800
- {label: "y=x^3", line: lines2Props, point: points2Props },
801
- {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 },
802
810
  ];
803
811
  </script>
804
812
 
@@ -812,7 +820,7 @@ Here is an example:
812
820
  <Points xValues={x} yValues={y2} {...point2Props} />
813
821
 
814
822
  <!-- series 3: markers in form of diamonds with green stroke and yellow fill -->
815
- <Points xValues={x} yValues={y3} {...points3Props} />
823
+ <Points xValues={x} yValues={y3} {...point3Props} />
816
824
 
817
825
  <!-- legend with one JSON for each series -->
818
826
  <Legend position="right" {items} />
@@ -826,12 +834,21 @@ Check this example in Svelte REPL: [plots-legend](https://svelte.dev/playground/
826
834
 
827
835
  The `position` parameter can be one of the follows: `'topleft'`, `'top'`, `'topright'`, `'right'`, `'bottomright'` and so on.
828
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
+
829
845
  The component has also properties which changes it apearance, here is the full list:
830
846
 
831
847
  Property name | Default value | Description
832
848
  --|--|--
833
849
  `items` | | array with JSON properties of legend items
834
850
  `position` | `'topleft'` | position of the legend element inside plotting area
851
+ `orientation` | `'vertical'` | arrangement of legend items: `'vertical'` or `'horizontal'`
835
852
  `lineColor` | `'#303030'` | color of the legend box line
836
853
  `lineWidth` | `1` | width (thickness) of the legend box line
837
854
  `faceColor` | `'#fff'` | background color of the legend box
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-plots-basic",
3
3
  "description": "Svelte components for creating simple plots",
4
- "version": "3.4.0",
4
+ "version": "4.0.0",
5
5
  "author": "Sergey Kucheryavskiy",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -20,14 +20,13 @@
20
20
  "./3d/*.svelte": "./src/3d/*.svelte"
21
21
  },
22
22
  "devDependencies": {
23
- "svelte": "^5.x.x"
23
+ "mdatools": "^1.5.0",
24
+ "svelte": "^5.56.8"
24
25
  },
25
26
  "peerDependencies": {
27
+ "mdatools": "^1.5.0",
26
28
  "svelte": "^5.0.0"
27
29
  },
28
- "dependencies": {
29
- "mdatools": "^1.x.x"
30
- },
31
30
  "keywords": [
32
31
  "svelte",
33
32
  "plots",
@@ -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,7 +29,7 @@
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
35
  let {
@@ -38,7 +38,7 @@
38
38
  lineColor = Colors.PRIMARY, // color of segment lines
39
39
  lineType = 1, // type of segment lines (1 - solid, 2 - dashed, 3 - dotted, 4 - dashdot)
40
40
  lineWidth = 1, // width (thickness) of segment lines
41
- faceColor = 'transparent', // color of segment lines
41
+ faceColor = 'transparent', // face (fill) color of the polygon
42
42
  opacity = 1, // opacity of the area color
43
43
  onclick, // function to be called if onclick event fires
44
44
  } = $props();
@@ -46,6 +46,7 @@
46
46
  // check user provided coordinates
47
47
  const xv = $derived(checkCoords(xValues, 'Area'));
48
48
  const yv = $derived(xv ? checkCoords(yValues, 'Area', xv.length) : null);
49
+ const validLineType = $derived(normalizeLineType(lineType, 'Area'));
49
50
 
50
51
  // get axes context and compute coordinates of polyline
51
52
  const axes = getContext('axes');
@@ -55,7 +56,7 @@
55
56
  const xs = $derived(xv ? transformCoords(xv.subset(1), axes.tX()) : null);
56
57
  const pa = $derived(p && xs && ys ? xs + ',' + ys + ' ' + p + ' ' + xs + ',' + ys : null);
57
58
 
58
- 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]}`);
59
60
  </script>
60
61
 
61
62
  {#if pa}
@@ -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,7 +23,7 @@
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
  -->
@@ -101,7 +101,7 @@
101
101
 
102
102
  /* handler for mouse events */
103
103
  function handleMouse(e, f) {
104
- if (f && e.target.id == "axes-box") {
104
+ if (f && e.target.id === "axes-box") {
105
105
  const x = invTransformCoords([e.offsetX - left], tX)[0];
106
106
  const y = invTransformCoords([e.offsetY - top], tY)[0];
107
107
  f(x, y);
@@ -307,7 +307,7 @@
307
307
  <text {x} y={coords.ltop} dx="1.2em" dy="-0.25em" dominant-baseline="hanging"
308
308
  fill={params.labelColor}
309
309
  font-size="{params.fontSize}em"
310
- text-anchor="end"
310
+ text-anchor="start"
311
311
  >{@html getTickFactorLabel(params.labelsFactor)}</text>
312
312
  {/if}
313
313
  {/snippet}
@@ -316,14 +316,16 @@
316
316
  {#snippet glgSnippet(params, coords)}
317
317
  {@const elw = coords.elw + 2 * coords.elp}
318
318
  {@const elp = coords.elp}
319
+ {@const elx = coords.elx}
319
320
  {@const elh = coords.elh}
320
321
  {@const ely = coords.ely}
322
+ {@const eliw = coords.eliw}
321
323
 
322
324
  <svg x={coords.lgl} y={coords.lgt} height={coords.lgh} width={coords.lgw}>
323
325
  <rect height="100%" width="100%" fill={params.faceColor} stroke={params.lineColor} stroke-width={params.lineWidth} />
324
326
 
325
327
  {#each params.items as item, i}
326
- <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]}>
327
329
 
328
330
  <!-- line -->
329
331
  {#if item.line}
@@ -532,7 +534,7 @@
532
534
  </p>
533
535
  {:else}
534
536
  <div class="download-links">
535
- <button type="button" onclick={handleClickSVG}>↓ svg</button>
537
+ <button type="button" onclick={handleClickSVG}>↓ svg</button>
536
538
  <button type="button" onclick={handleClickPNG}>↓ png</button>
537
539
  <button type="button" onclick={handleClickPNGAdvanced}>↓ png+</button>
538
540
  <button type="button" onclick={handleClickCopy}>⧉ copy</button>
@@ -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}
@@ -45,25 +45,46 @@
45
45
  onclick
46
46
  } = $props();
47
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
+
48
56
  const bw = $derived.by(() => {
49
- if (barWidth <= 0 || barWidth > 1) {
50
- 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.');
51
71
  return null;
52
72
  }
53
- return barWidth;
73
+
74
+ return {provided: true, value};
54
75
  });
55
76
 
56
77
  const x = $derived.by(() => {
57
- if (!bw) return null;
78
+ if (bw === null || exactWidth === null) return null;
58
79
  const xv = checkCoords(xValues, 'BarSeries');
59
80
  if (!xv) return null;
60
81
  const n = xv.length;
61
82
  if (n < 1) return null;
62
83
 
63
84
  let w;
64
- if (barWidthExact) {
65
- w = barWidthExact;
66
- } else if (n == 1) {
85
+ if (exactWidth.provided) {
86
+ w = exactWidth.value;
87
+ } else if (n === 1) {
67
88
  console.error('Bars: if only one bar must be shown, value for property "barWidthExact" should be provided.');
68
89
  return null;
69
90
  } else {
@@ -74,7 +95,7 @@
74
95
  }
75
96
  }
76
97
 
77
- w = w * barWidth;
98
+ w = w * bw;
78
99
  const whalf = w / 2;
79
100
  const left = Vector.zeros(n);
80
101
  const width = Vector.zeros(n);
@@ -113,6 +134,6 @@
113
134
  </script>
114
135
 
115
136
  {#if isOk}
116
- <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}
117
138
  lineWidth={lineWidth} {lineColor} {faceColor} {onclick} />
118
139
  {/if}
@@ -1,5 +1,5 @@
1
1
  <!--
2
- @component Adds a heatmap visializing values of a matrix.
2
+ @component Adds a heatmap visualizing values of a matrix.
3
3
 
4
4
  Main properties:
5
5
  - `values` - matrix (object of class `Matrix` from `mdatools` package) with values to visualize.
@@ -13,7 +13,7 @@
13
13
  import { Matrix } from 'mdatools/arrays';
14
14
  import { Axes, Heatmap } from 'svelte-plots-basic/2d';
15
15
 
16
- // create matrix with 5 rows and 10 columns filled with 5random values.
16
+ // create matrix with 5 rows and 10 columns filled with random values.
17
17
  const x = Matrix.rand(5, 10);
18
18
  </script>
19
19
 
@@ -91,7 +91,14 @@
91
91
 
92
92
 
93
93
  // check that values are provided as a matrix
94
- let v = $derived(ismatrix(values) && values.nrows > 0 && values.ncols > 0 ? values : null);
94
+ const v = $derived.by(() => {
95
+ if (!ismatrix(values) || values.nrows < 1 || values.ncols < 1) {
96
+ console.error('Heatmap: parameter "values" must be a non-empty Matrix.');
97
+ return null;
98
+ }
99
+
100
+ return values;
101
+ });
95
102
 
96
103
  // check and process left values for breaks
97
104
  let lb = $derived.by(() => {
@@ -121,9 +128,11 @@
121
128
  let lc = $derived.by(() => {
122
129
  if (!lb) return null;
123
130
 
124
- if (colmap) {
125
- if (!Array.isArray(colmap)) {
126
- console.error('Heatmap: parameter "colmap" must be array with colors.');
131
+ if (colmap !== null && colmap !== undefined) {
132
+ if (!Array.isArray(colmap) || colmap.some(color =>
133
+ typeof color !== 'string' || color.trim() === ''
134
+ )) {
135
+ console.error('Heatmap: parameter "colmap" must be an array of non-empty color strings.');
127
136
  return null;
128
137
  }
129
138
  if (colmap.length !== lb.length - 1) {
@@ -4,6 +4,7 @@
4
4
  Main properties:
5
5
  - `items` - array with text labels and their visual properties for each legend element.
6
6
  - `position` - position of the legend ("topleft", "top", "topright", "right", etc), default: `"topleft"`.
7
+ - `orientation` - arrangement of legend items (`"vertical"` or `"horizontal"`), default: `"vertical"`.
7
8
  - `lineColor` - color of the legend box line, default: `Colors.LEGEND`.
8
9
  - `lineWidth` - width (thickness) of the legend box line in pixels: `1`.
9
10
  - `faceColor` - color of the legend box background: `'#fff'`.
@@ -13,41 +14,47 @@
13
14
 
14
15
  ```svelte
15
16
  <script>
16
- import {Vector} from 'mdatools/array';
17
+ import { Vector } from 'mdatools/arrays';
18
+ import { Axes, Box, Legend, Lines, Points, XAxis, YAxis } from 'svelte-plots-basic/2d';
17
19
 
18
20
  const x = Vector.seq(-10, 10, 0.1);
19
21
  const y1 = x.apply(v => Math.cos(v));
20
22
  const y2 = x.apply(v => Math.sin(v));
23
+
24
+ const line1 = { lineType: 3, lineColor: 'red' };
25
+ const line2 = { lineType: 1, lineColor: 'blue' };
26
+ const point2 = { marker: 1, faceColor: 'white', lineColor: 'blue' };
27
+
28
+ const items = [
29
+ { label: 'cos(x)', line: line1 },
30
+ { label: 'sin(x)', line: line2, point: point2 }
31
+ ];
21
32
  </script>
22
33
 
23
34
  <Axes limX={[-10, 10]} limY={[-1.1, 1.1]} >
24
35
 
25
- <Lines xValues={x} yValues={y1} lineColor="red" lineType="3" />
26
- <Lines xValues={x} yValues={y2} lineColor="blue" lineType="1" />
27
- <Points xValues={x} yValues={y2} faceColor="white" borderColor="blue" />
36
+ <Lines xValues={x} yValues={y1} {...line1} />
37
+ <Lines xValues={x} yValues={y2} {...line2} />
38
+ <Points xValues={x} yValues={y2} {...point2} />
28
39
 
29
- <Legend
30
- position="topright"
31
- items = {[
32
- {"label": "cos(x)", "lineType": 1, "lineColor": "red"},
33
- {"label": "sin(x)", "lineType": 1, "lineColor": "blue", "marker": 1, "faceColor": "white", "borderColor": "blue"},
34
- ]}
35
- />
40
+ <Legend position="top" orientation="horizontal" {items} />
36
41
 
37
42
  <Box />
38
43
  <XAxis label="x" showGrid={true} />
39
44
  <YAxis label="f(x)" showGrid={true} />
40
45
  </Axes>
46
+ ```
41
47
  -->
42
48
  <script>
43
49
  import { getContext, onDestroy } from 'svelte';
44
50
  import { Colors, MARKER_SYMBOLS } from '../constants';
45
- import { text2svg } from '../methods';
51
+ import { text2svg, normalizeLineType } from '../methods';
46
52
 
47
53
  let {
48
54
  items, // array with text labels and their visual properties for each legend element.
49
55
  show = true, // logical, use to hide the element
50
- position = 'topleft', // position of the legend ("topleft", "top", "topright", "right", "bottomright", etc).
56
+ position = 'topleft', // position of the legend ("topleft", "top", "topright", "right", "bottomright", etc).
57
+ orientation = 'vertical', // arrangement of legend items ("vertical" or "horizontal")
51
58
  lineColor = Colors.LEGEND, // color of the legend box line
52
59
  lineWidth = 1, // width (thickness) of the legend box line
53
60
  faceColor = '#fff', // background color of the legend box
@@ -70,6 +77,21 @@
70
77
  return position;
71
78
  }
72
79
 
80
+ /* check if user provided orientation is correct */
81
+ function checkOrientation(orientation) {
82
+
83
+ if (typeof orientation !== 'string') {
84
+ console.error('Legend: value of "orientation" must be a string.');
85
+ return null;
86
+ }
87
+
88
+ if (!['vertical', 'horizontal'].includes(orientation)) {
89
+ console.error('Legend: wrong value of "orientation" property');
90
+ return null;
91
+ }
92
+ return orientation;
93
+ }
94
+
73
95
  /* check if user provided items are correct, transform text labels and set default values if they are absent */
74
96
  function processItems(items) {
75
97
 
@@ -96,25 +118,35 @@
96
118
  // set default values for lines
97
119
  if (item.line) {
98
120
  item.line = {...item.line};
99
- if (!item.line.lineType) item.line.lineType = 1
100
- if (!(item.line.lineType > 0 && item.line.lineType <= 4)) {
101
- console.error('Legend: parameter "lineType" for legend item ' + (i + 1) + ' is incorrect.');
102
- }
121
+ item.line.lineType = normalizeLineType(
122
+ item.line.lineType ?? 1,
123
+ `Legend item ${i + 1}`
124
+ );
103
125
  item.line.lineColor = item.line.lineColor ? item.line.lineColor : Colors.PRIMARY;
104
- item.line.lineWidth = item.line.lineWidth ? item.line.lineWidth : 1;
126
+ item.line.lineWidth = item.line.lineWidth ?? 1;
105
127
  }
106
128
 
107
129
  // set default values for markers
108
130
  if (item.point) {
109
131
  item.point = {...item.point};
110
- if (!item.point.marker) item.point.marker = 1
111
- if (!(item.point.marker > 0 && item.point.marker <= MARKER_SYMBOLS.length)) {
112
- console.error('Legend: parameter "marker" for legend item ' + (i + 1) + ' is incorrect.');
132
+ const canConvertMarker =
133
+ typeof item.point.marker === 'number' ||
134
+ (typeof item.point.marker === 'string' && item.point.marker.trim() !== '');
135
+ const marker = item.point.marker == null ? 1 :
136
+ canConvertMarker ? Number(item.point.marker) : NaN;
137
+
138
+ if (!Number.isInteger(marker) || marker < 1 || marker > MARKER_SYMBOLS.length) {
139
+ console.error(
140
+ `Legend item ${i + 1}: parameter "marker" must be a whole number from 1 to ${MARKER_SYMBOLS.length}.`
141
+ );
142
+ item.point = null;
143
+ } else {
144
+ item.point.marker = marker;
145
+ item.point.faceColor = item.point.faceColor ? item.point.faceColor : 'transparent';
146
+ item.point.lineWidth = item.point.lineWidth ?? 1;
147
+ item.point.markerSize = item.point.markerSize ?? 1;
148
+ item.point.lineColor = item.point.lineColor ? item.point.lineColor : Colors.PRIMARY;
113
149
  }
114
- item.point.faceColor = item.point.faceColor ? item.point.faceColor : 'transparent';
115
- item.point.lineWidth = item.point.lineWidth ? item.point.lineWidth : 1;
116
- item.point.markerSize = item.point.markerSize ? item.point.markerSize : 1;
117
- item.point.lineColor = item.point.lineColor ? item.point.lineColor : Colors.PRIMARY;
118
150
  }
119
151
 
120
152
  result.push(item);
@@ -122,14 +154,26 @@
122
154
  return result;
123
155
  }
124
156
 
125
- // get context and update group legend parameters reactivey
157
+ // Validate and transform only when the corresponding input changes.
158
+ const processedItems = $derived(processItems(items));
159
+ const validPosition = $derived(checkPosition(position));
160
+ const validOrientation = $derived(checkOrientation(orientation));
161
+
162
+ // get context and update group legend parameters reactively
126
163
  const axes = getContext('axes');
127
164
  $effect(() => {
128
- const newItems = processItems(items);
129
- const newPosition = checkPosition(position);
130
165
  axes.setGroupLegend(
131
- newItems && newPosition ?
132
- {show, position, items: newItems, lineColor, faceColor, lineWidth, fontSize} :
166
+ processedItems && validPosition && validOrientation ?
167
+ {
168
+ show,
169
+ position: validPosition,
170
+ orientation: validOrientation,
171
+ items: processedItems,
172
+ lineColor,
173
+ faceColor,
174
+ lineWidth,
175
+ fontSize
176
+ } :
133
177
  {show: false, position: null, items: null}
134
178
  );
135
179
  });
@@ -2,10 +2,10 @@
2
2
  @component Adds a series of lines connecting points with provided coordinates (polyline).
3
3
 
4
4
  Main properties:
5
- - `xValues` - array of vector with x-coordinates of the points.
6
- - `yValues` - array of vector with y-coordinates of the points.
5
+ - `xValues` - array or vector with x-coordinates of the points.
6
+ - `yValues` - array or vector with y-coordinates of the points.
7
7
  - `lineColor` - lines color, default: `Colors.PRIMARY`.
8
- - `lineWidth` - width (thickness) of the lines in pixels, defailt: `1`.
8
+ - `lineWidth` - width (thickness) of the lines in pixels, default: `1`.
9
9
  - `lineType` - type of lines (`1` - solid, `2` - dashed, `3` - dotted, `4` - dashdot).
10
10
  - `onclick` - function (callback) to be called when user clicks on a polyline.
11
11
 
@@ -27,7 +27,7 @@
27
27
  <script>
28
28
  import { getContext } from 'svelte';
29
29
  import { Colors } from '../constants';
30
- import { val2p, checkCoords, handleClick } from '../methods';
30
+ import { val2p, checkCoords, handleClick, normalizeLineType } from '../methods';
31
31
  import { LINE_STYLES } from '../constants';
32
32
 
33
33
  let {
@@ -43,13 +43,14 @@
43
43
  // check user provided coordinates
44
44
  const xv = $derived(checkCoords(xValues, 'Lines'));
45
45
  const yv = $derived(xv ? checkCoords(yValues, 'Lines', xv.length) : null);
46
+ const validLineType = $derived(normalizeLineType(lineType, 'Lines'));
46
47
 
47
48
  // get axes context and compute coordinates of polyline
48
49
  const axes = getContext('axes');
49
50
  const p = $derived(xv && yv ? val2p(xv, yv, axes.tX(), axes.tY()) : null);
50
51
 
51
52
  // reactive variables for coordinates of data points in pixels
52
- const lineStyleStr = $derived(`fill:transparent;stroke:${lineColor};stroke-width: ${lineWidth}px; stroke-dasharray:${LINE_STYLES[axes.scales().plot][lineType-1]}`);
53
+ const lineStyleStr = $derived(`fill:transparent;stroke:${lineColor};stroke-width: ${lineWidth}px; stroke-dasharray:${LINE_STYLES[axes.scales().plot][validLineType - 1]}`);
53
54
  </script>
54
55
 
55
56
  {#if p}