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.
@@ -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
 
@@ -27,13 +27,12 @@
27
27
  import { getContext } from 'svelte';
28
28
  import { ismatrix, isvector } from 'mdatools/arrays';
29
29
  import { expandgrid } from 'mdatools/misc';
30
- import { split } from 'mdatools/stat';
30
+ import { range, split } from 'mdatools/stat';
31
31
 
32
32
  import { getcolmap, transformObjects, transformCoords, seq } from '../methods';
33
33
  import { HEATMAP_NUM_SPLITS } from '../constants';
34
34
 
35
35
 
36
- /** @type {Props} */
37
36
  let {
38
37
  values, // Matrix with values to show heatmap for
39
38
  breaks, // vector with breaks to distribute the values in
@@ -42,26 +41,101 @@
42
41
  } = $props();
43
42
 
44
43
 
44
+ function validateBreaks(values) {
45
+ if (!values || values.length < 2) {
46
+ console.error('Heatmap: "breaks" must contain at least two values.');
47
+ return null;
48
+ }
49
+
50
+ const items = Array.from(values);
51
+
52
+ if (items.some(value =>
53
+ typeof value !== 'number' || Number.isNaN(value)
54
+ )) {
55
+ console.error('Heatmap: values in "breaks" must be numeric.');
56
+ return null;
57
+ }
58
+
59
+ if (items.some((value, index) =>
60
+ index > 0 && value <= items[index - 1]
61
+ )) {
62
+ console.error('Heatmap: values in "breaks" must be strictly increasing.');
63
+ return null;
64
+ }
65
+
66
+ return values;
67
+ }
68
+
69
+
70
+ function findInterval(value, breaks) {
71
+ let low = 1;
72
+ let high = breaks.length - 1;
73
+
74
+ while (low < high) {
75
+ const middle = Math.floor((low + high) / 2);
76
+
77
+ if (value <= breaks[middle]) {
78
+ high = middle;
79
+ } else {
80
+ low = middle + 1;
81
+ }
82
+ }
83
+
84
+ const index = low - 1;
85
+
86
+ return value > breaks[index] && value <= breaks[index + 1]
87
+ ? index
88
+ : -1;
89
+ }
90
+
91
+
45
92
 
46
93
  // check that values are provided as a matrix
47
- 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
+ });
48
102
 
49
103
  // check and process left values for breaks
50
104
  let lb = $derived.by(() => {
51
105
  if (!v) return null;
52
- if (breaks && Array.isArray(breaks)) return breaks;
53
- if (breaks && isvector(breaks)) return breaks.v;
54
- return split(v.v, HEATMAP_NUM_SPLITS).v;
106
+
107
+ if (breaks !== undefined && breaks !== null) {
108
+ if (Array.isArray(breaks)) return validateBreaks(breaks);
109
+ if (isvector(breaks)) return validateBreaks(breaks.v);
110
+
111
+ console.error('Heatmap: "breaks" must be an array or Vector.');
112
+ return null;
113
+ }
114
+
115
+ const valueRange = range(v.v);
116
+ if (valueRange[0] === valueRange[1]) {
117
+ const value = valueRange[0];
118
+ const delta = Math.abs(value) > 0 ? Math.abs(value) * 0.01 : 1;
119
+ const lower = Number.isFinite(value - delta) ? value - delta : value;
120
+ const upper = Number.isFinite(value + delta) ? value + delta : value;
121
+ return validateBreaks([lower, upper]);
122
+ }
123
+
124
+ return validateBreaks(split(v.v, HEATMAP_NUM_SPLITS).v);
55
125
  });
56
126
 
57
127
  // check and process colormap values
58
128
  let lc = $derived.by(() => {
59
- if (colmap) {
60
- if (!Array.isArray(colmap)) {
61
- console.error('Heatmap: parameter "colmap" must be array with colors.');
129
+ if (!lb) return null;
130
+
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.');
62
136
  return null;
63
137
  }
64
- if (colmap.length !== breaks.length - 1) {
138
+ if (colmap.length !== lb.length - 1) {
65
139
  console.error('Heatmap: number of color values in colormap does not match number of intervals defined by breaks.');
66
140
  return null;
67
141
  }
@@ -81,10 +155,11 @@
81
155
  const [top, left] = lt;
82
156
  const nv = left.length; // number of values in matrix
83
157
  const nb = lb.length; // number of breaks
84
- const rl = Array(nb).fill().map(() => []); // coordinates of left for every break
85
- const rt = Array(nb).fill().map(() => []); // coordinates of top for every break
86
- const rr = Array(nb).fill().map(() => []); // row indices for every break
87
- const rc = Array(nb).fill().map(() => []); // column indices for every break
158
+ const intervalCount = nb - 1;
159
+ const rl = Array.from({length: intervalCount}, () => []); // coordinates of left for every interval
160
+ const rt = Array.from({length: intervalCount}, () => []); // coordinates of top for every interval
161
+ const rr = Array.from({length: intervalCount}, () => []); // row indices for every interval
162
+ const rc = Array.from({length: intervalCount}, () => []); // column indices for every interval
88
163
 
89
164
  // adjust left side for the first break and right side for the last
90
165
  const llb = lb.slice();
@@ -95,15 +170,13 @@
95
170
  // loop over all breaks
96
171
  for (let j = 0; j < nv; j++) {
97
172
  const vj = v.v[j];
98
- for (let i = 0; i < nb - 1; i++) {
99
- if (vj > llb[i] && vj <= llb[i + 1]) {
100
- rl[i].push(left.v[j] - 0.5);
101
- rt[i].push(v.nrows - top.v[j] + 1.5);
102
- rr[i].push(top.v[j]);
103
- rc[i].push(left.v[j]);
104
- break;
105
- }
106
- }
173
+ const i = findInterval(vj, llb);
174
+ if (i === -1) continue;
175
+
176
+ rl[i].push(left.v[j] - 0.5);
177
+ rt[i].push(v.nrows - top.v[j] + 1.5);
178
+ rr[i].push(top.v[j]);
179
+ rc[i].push(left.v[j]);
107
180
  };
108
181
  return {rl, rt, rr, rc};
109
182
  });
@@ -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
- import { getContext } from 'svelte';
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,15 +154,35 @@
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
  });
180
+
181
+ onDestroy(() => {
182
+ axes.setGroupLegend({
183
+ show: false,
184
+ position: null,
185
+ items: null
186
+ });
187
+ });
136
188
  </script>
@@ -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,10 +27,9 @@
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
- /** @type {Props} */
34
33
  let {
35
34
  xValues, // vector or array with x-coordinates of the points
36
35
  yValues, // vector or array with y-coordinates of the points
@@ -44,13 +43,14 @@
44
43
  // check user provided coordinates
45
44
  const xv = $derived(checkCoords(xValues, 'Lines'));
46
45
  const yv = $derived(xv ? checkCoords(yValues, 'Lines', xv.length) : null);
46
+ const validLineType = $derived(normalizeLineType(lineType, 'Lines'));
47
47
 
48
48
  // get axes context and compute coordinates of polyline
49
49
  const axes = getContext('axes');
50
50
  const p = $derived(xv && yv ? val2p(xv, yv, axes.tX(), axes.tY()) : null);
51
51
 
52
52
  // reactive variables for coordinates of data points in pixels
53
- 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]}`);
54
54
  </script>
55
55
 
56
56
  {#if p}
@@ -60,4 +60,3 @@
60
60
  <polyline class="line" points={p}/>
61
61
  </g>
62
62
  {/if}
63
-
@@ -2,10 +2,10 @@
2
2
  @component Adds a series of polylines (like `<Lines>` but with several of those).
3
3
 
4
4
  Main properties:
5
- - `xValues` - array of vector with x-coordinates of the points.
6
- - `yValues` - Matrix (object of class `Matrix` from `mdatools/array`) with y-coordinates of the points.
5
+ - `xValues` - array or vector with x-coordinates of the points.
6
+ - `yValues` - Matrix (object of class `Matrix` from `mdatools/arrays`) 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 segment 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
 
@@ -16,8 +16,8 @@
16
16
  import { Vector, cbind } from 'mdatools/arrays';
17
17
  import { Axes, Multilines } from 'svelte-plots-basic/2d';
18
18
 
19
- const xValues = Vector.seq(0, 100, 100);
20
- const YY = cbind(
19
+ const xValues = Vector.seq(0, 10, 0.1);
20
+ const yValues = cbind(
21
21
  xValues.apply(v => Math.sin(v)),
22
22
  xValues.apply(v => Math.sin(v + 0.5) + 0.1),
23
23
  xValues.apply(v => Math.sin(v - 0.5) - 0.1),
@@ -35,13 +35,12 @@
35
35
  import { getContext } from 'svelte';
36
36
  import { ismatrix } from 'mdatools/arrays';
37
37
  import { Colors } from '../constants';
38
- import { val2p, checkCoords, handleClick } from '../methods';
38
+ import { val2p, checkCoords, handleClick, normalizeLineType } from '../methods';
39
39
  import { LINE_STYLES } from '../constants';
40
40
 
41
- /** @type {Props} */
42
41
  let {
43
42
  xValues, // vector or array with x-coordinates of the points
44
- yValues, // vector or array with y-coordinates of the points
43
+ yValues, // matrix with y-coordinates of the points
45
44
  lineColor = Colors.PRIMARY, // color of segment lines
46
45
  lineType = 1, // type of segment lines (1 - solid, 2 - dashed, 3 - dotted, 4 - dashdot)
47
46
  lineWidth = 1, // width (thickness) of segment lines
@@ -50,6 +49,7 @@
50
49
 
51
50
  // check user provided coordinates
52
51
  const xv = $derived(checkCoords(xValues, 'Multilines'));
52
+ const validLineType = $derived(normalizeLineType(lineType, 'Multilines'));
53
53
  const yv = $derived.by(() => {
54
54
 
55
55
  if (!xv) return null;
@@ -61,6 +61,7 @@
61
61
 
62
62
  if (xv.length !== yValues.nrows) {
63
63
  console.error('Multilines: parameter "xValues" must have the same number of values as number of rows in "yValues".');
64
+ return null;
64
65
  }
65
66
 
66
67
  return yValues;
@@ -79,7 +80,7 @@
79
80
  });
80
81
 
81
82
  // reactive variables for coordinates of data points in pixels
82
- const lineStyleStr = $derived(`fill:transparent;stroke:${lineColor};stroke-width: ${lineWidth}px; stroke-dasharray:${LINE_STYLES[axes.scales().plot][lineType-1]}`);
83
+ const lineStyleStr = $derived(`fill:transparent;stroke:${lineColor};stroke-width: ${lineWidth}px; stroke-dasharray:${LINE_STYLES[axes.scales().plot][validLineType - 1]}`);
83
84
  </script>
84
85
 
85
86
  {#if pp && pp.length > 0}
@@ -91,4 +92,3 @@
91
92
  {/each}
92
93
  </g>
93
94
  {/if}
94
-
@@ -2,13 +2,13 @@
2
2
  @component Adds a series of points.
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
  - `marker` - index for a point symbol (from 1 - default - to 8): `"●", "◼", "▲", "▼", "⬥", "+", "*", "✕"`.
8
8
  - `faceColor` - face (fill) color of the markers, default: `'transparent'`.
9
9
  - `lineColor` - border color of the markers, default: `Colors.PRIMARY`.
10
- - `lineWidth` - width (thickness) of the markers' border in pixels, defailt: `1`.
11
- - `markerSize` - size of the markers in em, defailt: `1`.
10
+ - `lineWidth` - width (thickness) of the markers' border in pixels, default: `1`.
11
+ - `markerSize` - size of the markers in em, default: `1`.
12
12
  - `onclick` - function (callback) to be called when user clicks on a marker.
13
13
 
14
14
  Example:
@@ -31,10 +31,9 @@
31
31
  import { Colors, MARKER_SYMBOLS } from '../constants';
32
32
  import { checkCoords, transformCoords, handleClick } from '../methods';
33
33
 
34
- /** @type {Props} */
35
34
  let {
36
- xValues, // array of vector with x-coordinates of points
37
- yValues, // array of vector with y-coordinates of points
35
+ xValues, // array or vector with x-coordinates of points
36
+ yValues, // array or vector with y-coordinates of points
38
37
  marker = 1, // index for point symbol (from 1 to 8): "●", "◼", "▲", "▼", "⬥", "+", "*", "✕"
39
38
  faceColor = 'transparent', // face (fill) color of the points
40
39
  lineColor = Colors.PRIMARY, // border color of the points
@@ -46,8 +45,8 @@
46
45
 
47
46
  // select which symbol to use as a marker
48
47
  const markerSymbol = $derived.by(() => {
49
- if (typeof(marker) !== 'number' || marker < 1 || marker > MARKER_SYMBOLS.length) {
50
- console.error('Points: parameter "marker" must be a number from 1 to ' + MARKER_SYMBOLS.length + '.');
48
+ if (!Number.isInteger(marker) || marker < 1 || marker > MARKER_SYMBOLS.length) {
49
+ console.error('Points: parameter "marker" must be a whole number from 1 to ' + MARKER_SYMBOLS.length + '.');
51
50
  return null;
52
51
  }
53
52
  return MARKER_SYMBOLS[marker - 1]
@@ -71,7 +70,7 @@
71
70
  const className = $derived(['◼', '⬥', '+', '✳', '✕'].includes(markerSymbol) ? 'series-points2' : 'series-points1');
72
71
 
73
72
  // check if all coordinates are correct
74
- const isOk = $derived(x && y && x.length === y.length);
73
+ const isOk = $derived(markerSymbol && x && y && x.length === y.length);
75
74
  </script>
76
75
 
77
76
  {#if isOk}
@@ -3,20 +3,19 @@
3
3
 
4
4
  Main properties:
5
5
  - `left` - array or vector with world coordinates of left side of the rectangles.
6
- - `top` - array or vector with world coordinates of left side of the rectangles.
6
+ - `top` - array or vector with world coordinates of top side of the rectangles.
7
7
  - `width` - width of the rectangles (one value or array/vector with individual values).
8
8
  - `height` - height of the rectangles (one value or array/vector with individual values).
9
- - `facColor` - face color of the rectangles (same for all).
9
+ - `faceColor` - face color of the rectangles (same for all).
10
10
  - `lineColor` - border color of each rectangle.
11
11
  - `lineWidth` - width (thickness) of border lines in pixels.
12
- - `className`- CSS class name for the labels group, default: `'series-rect'`.
12
+ - `className` - CSS class name for the rectangles group, default: `'series-rect'`.
13
13
  - `onclick` - function (callback) to be called when user clicks on any rectangle.
14
14
 
15
15
 
16
16
  Example:
17
- ```jsx
17
+ ```svelte
18
18
  <script>
19
- import { Matrix } from 'mdatools/arrays';
20
19
  import { Axes, Rectangles } from 'svelte-plots-basic/2d';
21
20
 
22
21
  const left = [0, 10, 20, 30];
@@ -32,28 +31,58 @@
32
31
  -->
33
32
  <script>
34
33
  import { getContext } from 'svelte';
35
- import { Vector } from 'mdatools/arrays';
34
+ import { Vector, isvector } from 'mdatools/arrays';
36
35
  import { Colors } from '../constants';
37
36
  import { checkCoords, transformCoords, transformObjects, handleClick } from '../methods';
38
37
 
39
- /** @type {Props} */
40
38
  let {
41
- left, // array of vector with coordinates of left sides of the bars
42
- top, // array of vector with coordinates of top sides of the bars
43
- width, // single value (same for all) or vector/array with bar width
44
- height, // single value (same for all) or vector/array with bar height
45
- faceColor = Colors.PRIMARY, // color of bar faces (fill)
46
- lineColor = faceColor, // color of bar borders
47
- lineWidth = 1, // width (thickness) of bar border lines
39
+ left, // array or vector with coordinates of left sides of the rectangles
40
+ top, // array or vector with coordinates of top sides of the rectangles
41
+ width, // single value (same for all) or vector/array with rectangle width
42
+ height, // single value (same for all) or vector/array with rectangle height
43
+ faceColor = Colors.PRIMARY, // color of rectangle faces (fill)
44
+ lineColor = faceColor, // color of rectangle borders
45
+ lineWidth = 1, // width (thickness) of rectangle border lines
48
46
  className = 'series-rect', // CSS class name of the SVG group
49
47
  onclick, // function to be called if onclick event fires
50
48
  } = $props();
51
49
 
50
+ function checkDimensions(value, name, len) {
51
+ const canConvert =
52
+ typeof value === 'number' ||
53
+ (typeof value === 'string' && value.trim() !== '');
54
+ let dimensions;
55
+
56
+ if (canConvert) {
57
+ const scalar = Number(value);
58
+ if (!Number.isFinite(scalar) || scalar < 0) {
59
+ console.error(`Rectangles: parameter "${name}" must contain only finite non-negative values.`);
60
+ return null;
61
+ }
62
+ dimensions = Vector.fill(scalar, len);
63
+ } else if (Array.isArray(value) || isvector(value)) {
64
+ dimensions = checkCoords(value, `Rectangles (${name})`, len);
65
+ if (!dimensions) return null;
66
+ } else {
67
+ console.error(`Rectangles: parameter "${name}" must be a number, array, or Vector.`);
68
+ return null;
69
+ }
70
+
71
+ for (let i = 0; i < dimensions.length; i++) {
72
+ if (dimensions.v[i] < 0) {
73
+ console.error(`Rectangles: parameter "${name}" must contain only finite non-negative values.`);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ return dimensions;
79
+ }
80
+
52
81
  // process provided values and compute world coordinates of rectangles
53
82
  const l = $derived(checkCoords(left, 'Rectangles (left)'));
54
83
  const t = $derived(l ? checkCoords(top, 'Rectangles (top)', l.length) : null);
55
- const w = $derived(t ? (typeof width !== 'object' ? Vector.fill(width, l.length) : width) : null);
56
- const h = $derived(t ? (typeof height !== 'object' ? Vector.fill(height, l.length) : height) : null);
84
+ const w = $derived(t ? checkDimensions(width, 'width', l.length) : null);
85
+ const h = $derived(t ? checkDimensions(height, 'height', l.length) : null);
57
86
 
58
87
  // get axes context and compute screen coordinates
59
88
  const axes = getContext('axes');
@@ -63,11 +92,11 @@
63
92
  const rw = $derived(w ? transformObjects(w, axes.tX()) : null);
64
93
  const rh = $derived(h ? transformObjects(h, axes.tY()) : null);
65
94
 
66
- // styles for bars and labels
95
+ // styles for rectangles
67
96
  const barsStyleStr = $derived(`fill:${faceColor};stroke:${lineColor};stroke-width:${lineWidth}px;`);
68
97
 
69
98
  // check if all coordinates are correct
70
- const isOk = $derived(rx && ry && rw && rh && (rw.length === rx.length) && (rh.length === rx.length));
99
+ const isOk = $derived(rx && ry && rw && rh);
71
100
  </script>
72
101
 
73
102
  {#if isOk}
@@ -79,5 +108,3 @@
79
108
  {/each}
80
109
  </g>
81
110
  {/if}
82
-
83
-