svelte-plots-basic 3.2.1 → 3.4.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 +21 -0
- package/README.md +36 -7
- package/package.json +10 -1
- package/src/2d/Area.svelte +0 -2
- package/src/2d/Axes.svelte +88 -36
- package/src/2d/Bars.svelte +4 -6
- package/src/2d/Box.svelte +5 -2
- package/src/2d/ColormapLegend.svelte +5 -2
- package/src/2d/Heatmap.svelte +91 -24
- package/src/2d/Legend.svelte +21 -8
- package/src/2d/Lines.svelte +0 -2
- package/src/2d/Multilines.svelte +1 -2
- package/src/2d/Points.svelte +0 -1
- package/src/2d/Rectangles.svelte +0 -2
- package/src/2d/TextLabels.svelte +18 -4
- package/src/2d/TextLegend.svelte +12 -3
- package/src/2d/XAxis.svelte +5 -1
- package/src/2d/YAxis.svelte +5 -1
- package/src/3d/Axes.svelte +29 -19
- package/src/3d/Mesh.svelte +62 -26
- package/src/3d/Segments.svelte +2 -2
- package/src/3d/TextLabels.svelte +2 -1
- package/src/3d/XAxis.svelte +5 -2
- package/src/3d/YAxis.svelte +5 -2
- package/src/3d/ZAxis.svelte +5 -2
- package/src/ExportDialog.svelte +361 -0
- package/src/methods.js +139 -67
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
|
@@ -15,7 +15,31 @@ These websites and web-applications use `svelte-plots-basic` library:
|
|
|
15
15
|
|
|
16
16
|
## News
|
|
17
17
|
|
|
18
|
-
### 3.
|
|
18
|
+
### 3.4.0
|
|
19
|
+
|
|
20
|
+
* Added the `plotActions` property to `Axes` for adding custom buttons to the download panel.
|
|
21
|
+
* Added advanced validation for axis limits, text-label positions, heatmap breaks, and custom plot actions.
|
|
22
|
+
* Fixed text-label positioning and rotation when individual positions are provided.
|
|
23
|
+
* Fixed heatmaps with automatic, custom, or constant-value breaks.
|
|
24
|
+
* Added support for rectangular and constant-height 3D meshes.
|
|
25
|
+
* Improved PNG, SVG, and clipboard export by serializing a clone instead of modifying the visible plot.
|
|
26
|
+
* Fixed the advanced PNG export dialog so a default resolution is always selected, falling back to 300 dpi when necessary.
|
|
27
|
+
* Prevented plot controls from submitting an enclosing HTML form.
|
|
28
|
+
* Added package validation before publishing and synchronized Svelte 5 peer-dependency metadata.
|
|
29
|
+
* Fixed conditionally rendered axes, boxes, and legends so their parent state is cleared when they are removed.
|
|
30
|
+
* Improved heatmap interval processing and added automatically generated colormaps with more than 16 colors.
|
|
31
|
+
* Fixed `TextLegend` positioning at zero and strengthened finite-range validation for 3D axes.
|
|
32
|
+
* Reduced reactive and repeated allocation overhead for axis configuration, text measurement, PNG export, and colormap generation.
|
|
33
|
+
* Added dependency-free regression tests and made them run automatically before packaging.
|
|
34
|
+
* Documented the trust requirements for labels rendered as SVG/HTML markup.
|
|
35
|
+
|
|
36
|
+
### 3.3.0
|
|
37
|
+
|
|
38
|
+
* Added advanced "save as PNG" option where user can select size and resolution of the image as well as change the filename.
|
|
39
|
+
* several small improvements and bug fixes.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### 3.2.1
|
|
19
43
|
|
|
20
44
|
* Added mouse click handler option for `Heatmap.svelte` component (returns row and column index of the element).
|
|
21
45
|
|
|
@@ -51,6 +75,13 @@ In addition to Svelte 5 syntax and functionality, this release also introduces a
|
|
|
51
75
|
|
|
52
76
|
* **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.
|
|
53
77
|
|
|
78
|
+
|
|
79
|
+
## Security and label markup
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
54
85
|
## Installation
|
|
55
86
|
|
|
56
87
|
The set up process is similar to any other Svelte component library. Just use:
|
|
@@ -134,6 +165,7 @@ Here is a table with properties of the component related to saving plot to a fil
|
|
|
134
165
|
`pngRes` | `300` | resolution of PNG image (pixels per inch)
|
|
135
166
|
`clipboardWidth` | `1200` | width of image in clipboard in pixels
|
|
136
167
|
`clipboardHeight` | `800` | height of image in clipboard in pixels
|
|
168
|
+
`plotActions` | `[]` | array of `{ text, callback }` objects for adding custom buttons to the download panel.
|
|
137
169
|
|
|
138
170
|
Please pay attention that most of the browsers enable clipboard functionality only if website is available via HTTPS.
|
|
139
171
|
|
|
@@ -835,15 +867,12 @@ Here is an example of simple 3D scatter plot:
|
|
|
835
867
|
<Axes limX={[-10, 10]} limY={[-10, 10]} limZ={[-10, 10]} {zoom} {phi} {theta}>
|
|
836
868
|
<Points {xValues} {yValues} {zValues} />
|
|
837
869
|
|
|
838
|
-
<XAxis showGrid={true}
|
|
839
|
-
<YAxis showGrid={true}
|
|
840
|
-
<ZAxis showGrid={true}
|
|
870
|
+
<XAxis showGrid={true} label="X" />
|
|
871
|
+
<YAxis showGrid={true} label="Y" />
|
|
872
|
+
<ZAxis showGrid={true} label="Z" />
|
|
841
873
|
</Axes>
|
|
842
874
|
```
|
|
843
875
|
|
|
844
876
|
You can also add `Lines`, `Segments` and `Mesh` series to 3D plots.
|
|
845
877
|
|
|
846
878
|
Check more advanced example in Svelte REPL with mouse and keyboard support, and more: [plots-3d](https://svelte.dev/playground/2294eecba7d7477ab9a09c1734d32ac2)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
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.
|
|
4
|
+
"version": "3.4.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",
|
|
@@ -16,6 +22,9 @@
|
|
|
16
22
|
"devDependencies": {
|
|
17
23
|
"svelte": "^5.x.x"
|
|
18
24
|
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"svelte": "^5.0.0"
|
|
27
|
+
},
|
|
19
28
|
"dependencies": {
|
|
20
29
|
"mdatools": "^1.x.x"
|
|
21
30
|
},
|
package/src/2d/Area.svelte
CHANGED
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
import { val2p, checkCoords, handleClick, transformCoords } 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
|
|
@@ -66,4 +65,3 @@
|
|
|
66
65
|
<polygon points={pa}/>
|
|
67
66
|
</g>
|
|
68
67
|
{/if}
|
|
69
|
-
|
package/src/2d/Axes.svelte
CHANGED
|
@@ -30,13 +30,14 @@
|
|
|
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,
|
|
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';
|
|
37
37
|
|
|
38
38
|
import AxisLines from './AxisLines.svelte';
|
|
39
39
|
import AxisTickLabels from './AxisTickLabels.svelte';
|
|
40
|
+
import ExportDialog from '../ExportDialog.svelte';
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
let {
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
limY = [0, 1],
|
|
46
47
|
margins = [1.0, 1.1, 0.55, 0.60],
|
|
47
48
|
downloadLinks = 'none',
|
|
49
|
+
plotActions = [],
|
|
48
50
|
fileName = 'plot',
|
|
49
51
|
pngWidth = 8,
|
|
50
52
|
pngHeight = 8,
|
|
@@ -59,6 +61,44 @@
|
|
|
59
61
|
} = $props();
|
|
60
62
|
|
|
61
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
|
+
|
|
62
102
|
/* handler for mouse events */
|
|
63
103
|
function handleMouse(e, f) {
|
|
64
104
|
if (f && e.target.id == "axes-box") {
|
|
@@ -103,9 +143,14 @@
|
|
|
103
143
|
downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes);
|
|
104
144
|
}
|
|
105
145
|
|
|
146
|
+
/* Open advanced PNG export dialog. */
|
|
147
|
+
let showExportDialog = $state(false);
|
|
148
|
+
function handleClickPNGAdvanced() { showExportDialog = true; }
|
|
149
|
+
function handleCloseExportDialog() { showExportDialog = false; }
|
|
150
|
+
|
|
106
151
|
|
|
107
152
|
// unique ID for clip path
|
|
108
|
-
const clipPathID = 'plottingArea' + Math.
|
|
153
|
+
const clipPathID = 'plottingArea' + Math.random().toString(36).slice(2);
|
|
109
154
|
|
|
110
155
|
// pointers to plot DOM element and its width and height
|
|
111
156
|
let plotElement = $state(0);
|
|
@@ -132,8 +177,8 @@
|
|
|
132
177
|
|
|
133
178
|
// plot status
|
|
134
179
|
const isOk = $derived(
|
|
135
|
-
|
|
136
|
-
|
|
180
|
+
hasUsableRange(limX) &&
|
|
181
|
+
hasUsableRange(limY) &&
|
|
137
182
|
width > (pxMargins[1] + pxMargins[3]) &&
|
|
138
183
|
height > (pxMargins[0] + pxMargins[2])
|
|
139
184
|
);
|
|
@@ -166,14 +211,14 @@
|
|
|
166
211
|
|
|
167
212
|
// parameters of plot title
|
|
168
213
|
const plotTitle = $derived(text2svg(title));
|
|
169
|
-
const titleHeight = $derived(plotTitle ? (plotTitle.length === title.
|
|
214
|
+
const titleHeight = $derived(plotTitle ? (plotTitle.length === title.length ? 1.0 : 1.4) : 0);
|
|
170
215
|
|
|
171
216
|
// default parameters of box, axis elements, colomap legend and group legend
|
|
172
|
-
let box = $state({show: false});
|
|
173
|
-
let xaxis = $state({show: false, error: ''});
|
|
174
|
-
let yaxis = $state({show: false, error: ''});
|
|
175
|
-
let clg = $state({show: false, colmap: null, breaks: null});
|
|
176
|
-
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});
|
|
177
222
|
|
|
178
223
|
// set colormap legend parameters if requested
|
|
179
224
|
const clgParams = $derived(isOk && clg && clg.show ? getColormapLegendParams(clg) : null);
|
|
@@ -220,7 +265,7 @@
|
|
|
220
265
|
|
|
221
266
|
{#if coords.tickCoords.length === 2 && coords.tickLabels.length === coords.tickCoords[1][0].length}
|
|
222
267
|
<AxisTickLabels las={params.las} pos={params.pos} tickCoords={coords.tickCoords}
|
|
223
|
-
tickLabels={coords.tickLabels}
|
|
268
|
+
tickLabels={coords.tickLabels} textColor={Colors.DARKGRAY} />
|
|
224
269
|
{/if}
|
|
225
270
|
|
|
226
271
|
<!-- tick factor -->
|
|
@@ -262,7 +307,7 @@
|
|
|
262
307
|
<text {x} y={coords.ltop} dx="1.2em" dy="-0.25em" dominant-baseline="hanging"
|
|
263
308
|
fill={params.labelColor}
|
|
264
309
|
font-size="{params.fontSize}em"
|
|
265
|
-
text-anchor="
|
|
310
|
+
text-anchor="end"
|
|
266
311
|
>{@html getTickFactorLabel(params.labelsFactor)}</text>
|
|
267
312
|
{/if}
|
|
268
313
|
{/snippet}
|
|
@@ -340,11 +385,6 @@
|
|
|
340
385
|
user-select: none;
|
|
341
386
|
}
|
|
342
387
|
|
|
343
|
-
.series-points text {
|
|
344
|
-
/* dominant-baseline: central;
|
|
345
|
-
alignment-baseline: central */
|
|
346
|
-
}
|
|
347
|
-
|
|
348
388
|
.tick-labels text,
|
|
349
389
|
.tick-labels tspan,
|
|
350
390
|
.series-textlabels text,
|
|
@@ -444,27 +484,18 @@
|
|
|
444
484
|
|
|
445
485
|
<!-- box -->
|
|
446
486
|
<g class="axes-box">
|
|
447
|
-
|
|
448
|
-
|
|
487
|
+
<!-- The plot callback requires real pointer coordinates; a keyboard-generated coordinate would be arbitrary. -->
|
|
488
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
489
|
+
<rect stroke={box.show ? box.lineColor : 'transparent'}
|
|
490
|
+
stroke-width={box.show ? box.lineWidth + 'px' : '0'}
|
|
491
|
+
fill="transparent"
|
|
449
492
|
x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
|
|
450
493
|
id="axes-box"
|
|
451
|
-
onkeydown={handleClick}
|
|
452
494
|
onclick={handleClick}
|
|
453
495
|
onmousemove={handleMove}
|
|
454
496
|
onmousedown={handleDown}
|
|
455
497
|
onmouseup={handleUp}
|
|
456
498
|
/>
|
|
457
|
-
{:else}
|
|
458
|
-
<rect stroke="transparent" fill="transparent"
|
|
459
|
-
x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
|
|
460
|
-
id="axes-box"
|
|
461
|
-
onkeydown={handleClick}
|
|
462
|
-
onclick={handleClick}
|
|
463
|
-
onmousemove={handleMove}
|
|
464
|
-
onmousedown={handleDown}
|
|
465
|
-
onmouseup={handleUp}
|
|
466
|
-
/>
|
|
467
|
-
{/if}
|
|
468
499
|
</g>
|
|
469
500
|
|
|
470
501
|
|
|
@@ -501,11 +532,24 @@
|
|
|
501
532
|
</p>
|
|
502
533
|
{:else}
|
|
503
534
|
<div class="download-links">
|
|
504
|
-
<button
|
|
505
|
-
<button
|
|
506
|
-
<button
|
|
535
|
+
<button type="button" onclick={handleClickSVG}>↓ svg</button>
|
|
536
|
+
<button type="button" onclick={handleClickPNG}>↓ png</button>
|
|
537
|
+
<button type="button" onclick={handleClickPNGAdvanced}>↓ png+</button>
|
|
538
|
+
<button type="button" onclick={handleClickCopy}>⧉ copy</button>
|
|
539
|
+
{#if actions.length > 0}
|
|
540
|
+
<span class="divider">⋮</span>
|
|
541
|
+
{#each actions as ac}
|
|
542
|
+
<button type="button" onclick={ac.callback}>{ac.text}</button>
|
|
543
|
+
{/each}
|
|
544
|
+
{/if}
|
|
507
545
|
</div>
|
|
508
546
|
{/if}
|
|
547
|
+
|
|
548
|
+
{#if showExportDialog}
|
|
549
|
+
<ExportDialog {plotElement} {fileName}
|
|
550
|
+
initialWidth={pngWidth} initialRes={pngRes}
|
|
551
|
+
onclose={handleCloseExportDialog} />
|
|
552
|
+
{/if}
|
|
509
553
|
</div>
|
|
510
554
|
|
|
511
555
|
<style>
|
|
@@ -534,7 +578,6 @@
|
|
|
534
578
|
transition: bottom 0.35s ease;
|
|
535
579
|
box-shadow: 0 0 0.75em #00000060;
|
|
536
580
|
margin: 0.6em;
|
|
537
|
-
box-sizing: border-box;
|
|
538
581
|
|
|
539
582
|
display: flex;
|
|
540
583
|
align-items: center;
|
|
@@ -549,6 +592,9 @@
|
|
|
549
592
|
background: #fafafa;
|
|
550
593
|
border-radius: 0.35em;
|
|
551
594
|
padding: 0.25em 0.5em;
|
|
595
|
+
margin: 0 1px;
|
|
596
|
+
|
|
597
|
+
line-height: 1.25;
|
|
552
598
|
}
|
|
553
599
|
|
|
554
600
|
.download-links > button:hover{
|
|
@@ -564,6 +610,12 @@
|
|
|
564
610
|
background: #4488ee;
|
|
565
611
|
}
|
|
566
612
|
|
|
613
|
+
.download-links > .divider {
|
|
614
|
+
display: inline-block;
|
|
615
|
+
width: 1em;
|
|
616
|
+
text-align: center;
|
|
617
|
+
color: #909090;
|
|
618
|
+
}
|
|
567
619
|
|
|
568
620
|
/* Plot container */
|
|
569
621
|
.plot-container {
|
|
@@ -621,4 +673,4 @@
|
|
|
621
673
|
min-width: 100%;
|
|
622
674
|
}
|
|
623
675
|
|
|
624
|
-
</style>
|
|
676
|
+
</style>
|
package/src/2d/Bars.svelte
CHANGED
|
@@ -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.
|
|
@@ -68,11 +67,10 @@
|
|
|
68
67
|
console.error('Bars: if only one bar must be shown, value for property "barWidthExact" should be provided.');
|
|
69
68
|
return null;
|
|
70
69
|
} else {
|
|
71
|
-
w =
|
|
72
|
-
let lw = 0;
|
|
70
|
+
w = Infinity;
|
|
73
71
|
for (let i = 1; i < n; i++) {
|
|
74
|
-
lw = Math.abs(xv.v[i - 1] - xv.v[i]);
|
|
75
|
-
if (lw
|
|
72
|
+
const lw = Math.abs(xv.v[i - 1] - xv.v[i]);
|
|
73
|
+
if (lw < w) w = lw;
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
@@ -117,4 +115,4 @@
|
|
|
117
115
|
{#if isOk}
|
|
118
116
|
<Rectangles className="series series-bar" left={x.left} width={x.width} top={y.top} height={y.height}
|
|
119
117
|
lineWidth={lineWidth} {lineColor} {faceColor} {onclick} />
|
|
120
|
-
{/if}
|
|
118
|
+
{/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
|
|
package/src/2d/Heatmap.svelte
CHANGED
|
@@ -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,6 +41,54 @@
|
|
|
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
94
|
let v = $derived(ismatrix(values) && values.nrows > 0 && values.ncols > 0 ? values : null);
|
|
@@ -49,19 +96,37 @@
|
|
|
49
96
|
// check and process left values for breaks
|
|
50
97
|
let lb = $derived.by(() => {
|
|
51
98
|
if (!v) return null;
|
|
52
|
-
|
|
53
|
-
if (breaks &&
|
|
54
|
-
|
|
99
|
+
|
|
100
|
+
if (breaks !== undefined && breaks !== null) {
|
|
101
|
+
if (Array.isArray(breaks)) return validateBreaks(breaks);
|
|
102
|
+
if (isvector(breaks)) return validateBreaks(breaks.v);
|
|
103
|
+
|
|
104
|
+
console.error('Heatmap: "breaks" must be an array or Vector.');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const valueRange = range(v.v);
|
|
109
|
+
if (valueRange[0] === valueRange[1]) {
|
|
110
|
+
const value = valueRange[0];
|
|
111
|
+
const delta = Math.abs(value) > 0 ? Math.abs(value) * 0.01 : 1;
|
|
112
|
+
const lower = Number.isFinite(value - delta) ? value - delta : value;
|
|
113
|
+
const upper = Number.isFinite(value + delta) ? value + delta : value;
|
|
114
|
+
return validateBreaks([lower, upper]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return validateBreaks(split(v.v, HEATMAP_NUM_SPLITS).v);
|
|
55
118
|
});
|
|
56
119
|
|
|
57
120
|
// check and process colormap values
|
|
58
121
|
let lc = $derived.by(() => {
|
|
122
|
+
if (!lb) return null;
|
|
123
|
+
|
|
59
124
|
if (colmap) {
|
|
60
125
|
if (!Array.isArray(colmap)) {
|
|
61
126
|
console.error('Heatmap: parameter "colmap" must be array with colors.');
|
|
62
127
|
return null;
|
|
63
128
|
}
|
|
64
|
-
if (colmap.length !==
|
|
129
|
+
if (colmap.length !== lb.length - 1) {
|
|
65
130
|
console.error('Heatmap: number of color values in colormap does not match number of intervals defined by breaks.');
|
|
66
131
|
return null;
|
|
67
132
|
}
|
|
@@ -81,8 +146,11 @@
|
|
|
81
146
|
const [top, left] = lt;
|
|
82
147
|
const nv = left.length; // number of values in matrix
|
|
83
148
|
const nb = lb.length; // number of breaks
|
|
84
|
-
const
|
|
85
|
-
const
|
|
149
|
+
const intervalCount = nb - 1;
|
|
150
|
+
const rl = Array.from({length: intervalCount}, () => []); // coordinates of left for every interval
|
|
151
|
+
const rt = Array.from({length: intervalCount}, () => []); // coordinates of top for every interval
|
|
152
|
+
const rr = Array.from({length: intervalCount}, () => []); // row indices for every interval
|
|
153
|
+
const rc = Array.from({length: intervalCount}, () => []); // column indices for every interval
|
|
86
154
|
|
|
87
155
|
// adjust left side for the first break and right side for the last
|
|
88
156
|
const llb = lb.slice();
|
|
@@ -93,15 +161,15 @@
|
|
|
93
161
|
// loop over all breaks
|
|
94
162
|
for (let j = 0; j < nv; j++) {
|
|
95
163
|
const vj = v.v[j];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
164
|
+
const i = findInterval(vj, llb);
|
|
165
|
+
if (i === -1) continue;
|
|
166
|
+
|
|
167
|
+
rl[i].push(left.v[j] - 0.5);
|
|
168
|
+
rt[i].push(v.nrows - top.v[j] + 1.5);
|
|
169
|
+
rr[i].push(top.v[j]);
|
|
170
|
+
rc[i].push(left.v[j]);
|
|
103
171
|
};
|
|
104
|
-
return {rl, rt};
|
|
172
|
+
return {rl, rt, rr, rc};
|
|
105
173
|
});
|
|
106
174
|
|
|
107
175
|
|
|
@@ -119,13 +187,12 @@
|
|
|
119
187
|
const el = e.target;
|
|
120
188
|
if (el.tagName !== 'rect') return;
|
|
121
189
|
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
e.stopPropagation();
|
|
190
|
+
const row = parseInt(el.getAttribute('data-row'));
|
|
191
|
+
const col = parseInt(el.getAttribute('data-col'));
|
|
192
|
+
if (!isNaN(row) && !isNaN(col)) {
|
|
193
|
+
onclick(row, col);
|
|
194
|
+
e.stopPropagation();
|
|
195
|
+
}
|
|
129
196
|
}
|
|
130
197
|
|
|
131
198
|
|
|
@@ -143,7 +210,7 @@
|
|
|
143
210
|
{#if rx[i].length > 0}
|
|
144
211
|
<!-- loop over elements which fall into the interval -->
|
|
145
212
|
{#each rx[i] as v, j}
|
|
146
|
-
<rect x={rx[i][j]} y={ry[i][j]} width={rw} height={rh}/>
|
|
213
|
+
<rect x={rx[i][j]} y={ry[i][j]} width={rw} height={rh} data-row={wc.rr[i][j]} data-col={wc.rc[i][j]}/>
|
|
147
214
|
{/each}
|
|
148
215
|
{/if}
|
|
149
216
|
</g>
|