svelte-plots-basic 3.3.0 → 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 +29 -6
- package/package.json +10 -1
- package/src/2d/Area.svelte +0 -2
- package/src/2d/Axes.svelte +69 -13
- package/src/2d/Bars.svelte +1 -2
- package/src/2d/Box.svelte +5 -2
- package/src/2d/ColormapLegend.svelte +5 -2
- package/src/2d/Heatmap.svelte +83 -19
- package/src/2d/Legend.svelte +9 -1
- 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 +11 -3
- package/src/2d/XAxis.svelte +5 -1
- package/src/2d/YAxis.svelte +5 -1
- package/src/3d/Axes.svelte +26 -15
- package/src/3d/Mesh.svelte +58 -22
- 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 +8 -6
- package/src/methods.js +127 -75
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,6 +15,24 @@ These websites and web-applications use `svelte-plots-basic` library:
|
|
|
15
15
|
|
|
16
16
|
## News
|
|
17
17
|
|
|
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
|
+
|
|
18
36
|
### 3.3.0
|
|
19
37
|
|
|
20
38
|
* Added advanced "save as PNG" option where user can select size and resolution of the image as well as change the filename.
|
|
@@ -57,6 +75,13 @@ In addition to Svelte 5 syntax and functionality, this release also introduces a
|
|
|
57
75
|
|
|
58
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.
|
|
59
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
|
+
|
|
60
85
|
## Installation
|
|
61
86
|
|
|
62
87
|
The set up process is similar to any other Svelte component library. Just use:
|
|
@@ -140,6 +165,7 @@ Here is a table with properties of the component related to saving plot to a fil
|
|
|
140
165
|
`pngRes` | `300` | resolution of PNG image (pixels per inch)
|
|
141
166
|
`clipboardWidth` | `1200` | width of image in clipboard in pixels
|
|
142
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.
|
|
143
169
|
|
|
144
170
|
Please pay attention that most of the browsers enable clipboard functionality only if website is available via HTTPS.
|
|
145
171
|
|
|
@@ -841,15 +867,12 @@ Here is an example of simple 3D scatter plot:
|
|
|
841
867
|
<Axes limX={[-10, 10]} limY={[-10, 10]} limZ={[-10, 10]} {zoom} {phi} {theta}>
|
|
842
868
|
<Points {xValues} {yValues} {zValues} />
|
|
843
869
|
|
|
844
|
-
<XAxis showGrid={true}
|
|
845
|
-
<YAxis showGrid={true}
|
|
846
|
-
<ZAxis showGrid={true}
|
|
870
|
+
<XAxis showGrid={true} label="X" />
|
|
871
|
+
<YAxis showGrid={true} label="Y" />
|
|
872
|
+
<ZAxis showGrid={true} label="Z" />
|
|
847
873
|
</Axes>
|
|
848
874
|
```
|
|
849
875
|
|
|
850
876
|
You can also add `Lines`, `Segments` and `Mesh` series to 3D plots.
|
|
851
877
|
|
|
852
878
|
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.
|
|
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,7 +30,7 @@
|
|
|
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';
|
|
@@ -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,6 +61,44 @@
|
|
|
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
104
|
if (f && e.target.id == "axes-box") {
|
|
@@ -138,8 +177,8 @@
|
|
|
138
177
|
|
|
139
178
|
// plot status
|
|
140
179
|
const isOk = $derived(
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
@@ -445,6 +484,8 @@
|
|
|
445
484
|
|
|
446
485
|
<!-- box -->
|
|
447
486
|
<g class="axes-box">
|
|
487
|
+
<!-- The plot callback requires real pointer coordinates; a keyboard-generated coordinate would be arbitrary. -->
|
|
488
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
448
489
|
<rect stroke={box.show ? box.lineColor : 'transparent'}
|
|
449
490
|
stroke-width={box.show ? box.lineWidth + 'px' : '0'}
|
|
450
491
|
fill="transparent"
|
|
@@ -491,10 +532,16 @@
|
|
|
491
532
|
</p>
|
|
492
533
|
{:else}
|
|
493
534
|
<div class="download-links">
|
|
494
|
-
<button onclick={handleClickSVG}
|
|
495
|
-
<button onclick={handleClickPNG}
|
|
496
|
-
<button onclick={handleClickPNGAdvanced}
|
|
497
|
-
<button onclick={handleClickCopy}>⧉ copy</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}
|
|
498
545
|
</div>
|
|
499
546
|
{/if}
|
|
500
547
|
|
|
@@ -545,6 +592,9 @@
|
|
|
545
592
|
background: #fafafa;
|
|
546
593
|
border-radius: 0.35em;
|
|
547
594
|
padding: 0.25em 0.5em;
|
|
595
|
+
margin: 0 1px;
|
|
596
|
+
|
|
597
|
+
line-height: 1.25;
|
|
548
598
|
}
|
|
549
599
|
|
|
550
600
|
.download-links > button:hover{
|
|
@@ -560,6 +610,12 @@
|
|
|
560
610
|
background: #4488ee;
|
|
561
611
|
}
|
|
562
612
|
|
|
613
|
+
.download-links > .divider {
|
|
614
|
+
display: inline-block;
|
|
615
|
+
width: 1em;
|
|
616
|
+
text-align: center;
|
|
617
|
+
color: #909090;
|
|
618
|
+
}
|
|
563
619
|
|
|
564
620
|
/* Plot container */
|
|
565
621
|
.plot-container {
|
|
@@ -617,4 +673,4 @@
|
|
|
617
673
|
min-width: 100%;
|
|
618
674
|
}
|
|
619
675
|
|
|
620
|
-
</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.
|
|
@@ -116,4 +115,4 @@
|
|
|
116
115
|
{#if isOk}
|
|
117
116
|
<Rectangles className="series series-bar" left={x.left} width={x.width} top={y.top} height={y.height}
|
|
118
117
|
lineWidth={lineWidth} {lineColor} {faceColor} {onclick} />
|
|
119
|
-
{/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,10 +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
|
|
86
|
-
const
|
|
87
|
-
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
|
|
88
154
|
|
|
89
155
|
// adjust left side for the first break and right side for the last
|
|
90
156
|
const llb = lb.slice();
|
|
@@ -95,15 +161,13 @@
|
|
|
95
161
|
// loop over all breaks
|
|
96
162
|
for (let j = 0; j < nv; j++) {
|
|
97
163
|
const vj = v.v[j];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
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]);
|
|
107
171
|
};
|
|
108
172
|
return {rl, rt, rr, rc};
|
|
109
173
|
});
|
package/src/2d/Legend.svelte
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
</Axes>
|
|
41
41
|
-->
|
|
42
42
|
<script>
|
|
43
|
-
import { getContext } from 'svelte';
|
|
43
|
+
import { getContext, onDestroy } from 'svelte';
|
|
44
44
|
import { Colors, MARKER_SYMBOLS } from '../constants';
|
|
45
45
|
import { text2svg } from '../methods';
|
|
46
46
|
|
|
@@ -133,4 +133,12 @@
|
|
|
133
133
|
{show: false, position: null, items: null}
|
|
134
134
|
);
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
onDestroy(() => {
|
|
138
|
+
axes.setGroupLegend({
|
|
139
|
+
show: false,
|
|
140
|
+
position: null,
|
|
141
|
+
items: null
|
|
142
|
+
});
|
|
143
|
+
});
|
|
136
144
|
</script>
|
package/src/2d/Lines.svelte
CHANGED
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
import { val2p, checkCoords, handleClick } 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
|
|
@@ -60,4 +59,3 @@
|
|
|
60
59
|
<polyline class="line" points={p}/>
|
|
61
60
|
</g>
|
|
62
61
|
{/if}
|
|
63
|
-
|
package/src/2d/Multilines.svelte
CHANGED
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
import { val2p, checkCoords, handleClick } 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
43
|
yValues, // vector or array with y-coordinates of the points
|
|
@@ -61,6 +60,7 @@
|
|
|
61
60
|
|
|
62
61
|
if (xv.length !== yValues.nrows) {
|
|
63
62
|
console.error('Multilines: parameter "xValues" must have the same number of values as number of rows in "yValues".');
|
|
63
|
+
return null;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
return yValues;
|
|
@@ -91,4 +91,3 @@
|
|
|
91
91
|
{/each}
|
|
92
92
|
</g>
|
|
93
93
|
{/if}
|
|
94
|
-
|
package/src/2d/Points.svelte
CHANGED
|
@@ -31,7 +31,6 @@
|
|
|
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
35
|
xValues, // array of vector with x-coordinates of points
|
|
37
36
|
yValues, // array of vector with y-coordinates of points
|
package/src/2d/Rectangles.svelte
CHANGED
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
import { Colors } from '../constants';
|
|
37
37
|
import { checkCoords, transformCoords, transformObjects, handleClick } from '../methods';
|
|
38
38
|
|
|
39
|
-
/** @type {Props} */
|
|
40
39
|
let {
|
|
41
40
|
left, // array of vector with coordinates of left sides of the bars
|
|
42
41
|
top, // array of vector with coordinates of top sides of the bars
|
|
@@ -80,4 +79,3 @@
|
|
|
80
79
|
</g>
|
|
81
80
|
{/if}
|
|
82
81
|
|
|
83
|
-
|
package/src/2d/TextLabels.svelte
CHANGED
|
@@ -71,8 +71,22 @@
|
|
|
71
71
|
const p = $derived.by(() => {
|
|
72
72
|
if (!l) return null;
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const hasIndividualPositions =
|
|
75
|
+
Array.isArray(pos) || ArrayBuffer.isView(pos);
|
|
76
|
+
|
|
77
|
+
if (typeof labels === 'string' && hasIndividualPositions) {
|
|
78
|
+
console.error('TextLabels: when "labels" is a single string, "pos" must be a single number.');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (hasIndividualPositions && pos.length !== yv.length) {
|
|
83
|
+
console.error('TextLabels: the number of values in "pos" must match the number of coordinates.');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const positions = hasIndividualPositions ? Array.from(pos) : [pos];
|
|
88
|
+
if (positions.some(v => !Number.isInteger(v) || v < 0 || v > 4)) {
|
|
89
|
+
console.error('TextLabels: values in "pos" must be whole numbers from 0 to 4.');
|
|
76
90
|
return null;
|
|
77
91
|
}
|
|
78
92
|
|
|
@@ -106,14 +120,14 @@
|
|
|
106
120
|
{#each x as v, i}
|
|
107
121
|
<text data-id={i} x={x[i]} y={y[i]} dx={dx[pos]} dy={dy[pos]}
|
|
108
122
|
transform={rotateAngle !== undefined ? `rotate(${rotateAngle}, ${x[i] + dx[pos]}, ${y[i] + dy[pos]})` : ''}
|
|
109
|
-
text-anchor={textAnchors[pos
|
|
123
|
+
text-anchor={textAnchors[pos]}>{@html labels}</text>
|
|
110
124
|
{/each}
|
|
111
125
|
|
|
112
126
|
{:else if (Array.isArray(pos) || ArrayBuffer.isView(pos))}
|
|
113
127
|
|
|
114
128
|
{#each x as v, i}
|
|
115
129
|
<text data-id={i} x={x[i]} y={y[i]} dx={dx[pos[i]]} dy={dy[pos[i]]}
|
|
116
|
-
transform={rotateAngle !== undefined ? `rotate(${rotateAngle}, ${x[i] + dx[pos]}, ${y[i] + dy[pos]})` : ''}
|
|
130
|
+
transform={rotateAngle !== undefined ? `rotate(${rotateAngle}, ${x[i] + dx[pos[i]]}, ${y[i] + dy[pos[i]]})` : ''}
|
|
117
131
|
text-anchor={textAnchors[pos[i]]}>{@html labels[i]}</text>
|
|
118
132
|
{/each}
|
|
119
133
|
|
package/src/2d/TextLegend.svelte
CHANGED
|
@@ -16,8 +16,16 @@
|
|
|
16
16
|
} = $props()
|
|
17
17
|
|
|
18
18
|
const axes = getContext('axes');
|
|
19
|
-
const x = $derived(
|
|
20
|
-
|
|
19
|
+
const x = $derived(
|
|
20
|
+
left !== undefined && left !== null
|
|
21
|
+
? transformCoords([left], axes.tX())
|
|
22
|
+
: null
|
|
23
|
+
);
|
|
24
|
+
const y = $derived(
|
|
25
|
+
top !== undefined && top !== null
|
|
26
|
+
? transformCoords([top], axes.tY())
|
|
27
|
+
: null
|
|
28
|
+
);
|
|
21
29
|
|
|
22
30
|
// styles for bars and labels
|
|
23
31
|
const textStyleStr = $derived(`fill:${faceColor};stroke-width:${lineWidth}px;stroke:${lineColor};font-size:${textSize}em;`);
|
|
@@ -29,4 +37,4 @@
|
|
|
29
37
|
<tspan {x} {dx} dy={i === 0 ? 0 : dy}>{@html el}</tspan>
|
|
30
38
|
{/each}
|
|
31
39
|
</text>
|
|
32
|
-
{/if}
|
|
40
|
+
{/if}
|
package/src/2d/XAxis.svelte
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
-->
|
|
22
22
|
<script>
|
|
23
23
|
|
|
24
|
-
import { getContext } from 'svelte';
|
|
24
|
+
import { getContext, onDestroy } from 'svelte';
|
|
25
25
|
import { text2svg, validateTicks, validateTickLabels } from '../methods';
|
|
26
26
|
|
|
27
27
|
let {
|
|
@@ -60,4 +60,8 @@
|
|
|
60
60
|
axes.setXAxis({show, error: error, label: axisLabel, ticks: ticksProcessed,
|
|
61
61
|
labelHeight, showGrid, las, whole, tickLabels: tickLabelsProcessed});
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
onDestroy(() => {
|
|
65
|
+
axes.setXAxis({show: false, error: ''});
|
|
66
|
+
});
|
|
63
67
|
</script>
|
package/src/2d/YAxis.svelte
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
-->
|
|
22
22
|
<script>
|
|
23
23
|
|
|
24
|
-
import { getContext } from 'svelte';
|
|
24
|
+
import { getContext, onDestroy } from 'svelte';
|
|
25
25
|
import { text2svg, validateTicks, validateTickLabels } from '../methods';
|
|
26
26
|
|
|
27
27
|
|
|
@@ -63,4 +63,8 @@
|
|
|
63
63
|
axes.setYAxis({show, error: error, label: axisLabel, ticks: ticksProcessed,
|
|
64
64
|
labelHeight, showGrid, las, whole, tickLabels: tickLabelsProcessed, pos: 4});
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
onDestroy(() => {
|
|
68
|
+
axes.setYAxis({show: false, error: ''});
|
|
69
|
+
});
|
|
66
70
|
</script>
|
package/src/3d/Axes.svelte
CHANGED
|
@@ -33,14 +33,13 @@
|
|
|
33
33
|
import { matrix, Matrix } from 'mdatools/arrays';
|
|
34
34
|
import { setContext } from 'svelte';
|
|
35
35
|
import { PLOT_FONT_SIZE } from '../constants.js';
|
|
36
|
-
import {
|
|
36
|
+
import { getScale, getXAxisCoords3D, getYAxisCoords3D, getZAxisCoords3D,
|
|
37
37
|
downloadPNG, downloadSVG, copyToClipboard} from '../methods.js';
|
|
38
38
|
|
|
39
39
|
import AxisLines from './AxisLines.svelte';
|
|
40
40
|
import AxisTickLabels from './AxisTickLabels.svelte';
|
|
41
41
|
import TextLabels from './TextLabels.svelte';
|
|
42
42
|
|
|
43
|
-
/** @type {Props} */
|
|
44
43
|
let {
|
|
45
44
|
limX = [0, 1],
|
|
46
45
|
limY = [0, 1],
|
|
@@ -59,6 +58,21 @@
|
|
|
59
58
|
} = $props();
|
|
60
59
|
|
|
61
60
|
|
|
61
|
+
function hasUsableRange(lim) {
|
|
62
|
+
if (!Array.isArray(lim) || lim.length !== 2) return false;
|
|
63
|
+
if (!lim.every(value =>
|
|
64
|
+
typeof value === 'number' && Number.isFinite(value)
|
|
65
|
+
)) return false;
|
|
66
|
+
|
|
67
|
+
const span = lim[1] - lim[0];
|
|
68
|
+
const scale = Math.max(Math.abs(lim[0]), Math.abs(lim[1]));
|
|
69
|
+
|
|
70
|
+
return span > 0 &&
|
|
71
|
+
Number.isFinite(1 / span) &&
|
|
72
|
+
span > Number.EPSILON * scale * 16;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
62
76
|
|
|
63
77
|
/* Copy plot to clipboard as PNG image. */
|
|
64
78
|
async function handleClickCopy(e) {
|
|
@@ -105,12 +119,9 @@
|
|
|
105
119
|
|
|
106
120
|
// check main plot parameters and set status
|
|
107
121
|
const isOk = $derived(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
limX[0] < limX[1] &&
|
|
112
|
-
limY[0] < limY[1] &&
|
|
113
|
-
limZ[0] < limZ[1] &&
|
|
122
|
+
hasUsableRange(limX) &&
|
|
123
|
+
hasUsableRange(limY) &&
|
|
124
|
+
hasUsableRange(limZ) &&
|
|
114
125
|
width > 50 &&
|
|
115
126
|
height > 50
|
|
116
127
|
);
|
|
@@ -174,9 +185,9 @@
|
|
|
174
185
|
);
|
|
175
186
|
|
|
176
187
|
// containers for axis properties
|
|
177
|
-
let xaxis = $state({show: false, label: ""});
|
|
178
|
-
let yaxis = $state({show: false, label: ""});
|
|
179
|
-
let zaxis = $state({show: false, label: ""});
|
|
188
|
+
let xaxis = $state.raw({show: false, label: ""});
|
|
189
|
+
let yaxis = $state.raw({show: false, label: ""});
|
|
190
|
+
let zaxis = $state.raw({show: false, label: ""});
|
|
180
191
|
|
|
181
192
|
// axes context to share with children
|
|
182
193
|
setContext('axes', {
|
|
@@ -290,9 +301,9 @@
|
|
|
290
301
|
</p>
|
|
291
302
|
{:else}
|
|
292
303
|
<div class="download-links">
|
|
293
|
-
<button onclick={handleClickSVG}>⇩ svg</button>
|
|
294
|
-
<button onclick={handleClickPNG}>⇩ png</button>
|
|
295
|
-
<button onclick={handleClickCopy}>⧉ copy</button>
|
|
304
|
+
<button type="button" onclick={handleClickSVG}>⇩ svg</button>
|
|
305
|
+
<button type="button" onclick={handleClickPNG}>⇩ png</button>
|
|
306
|
+
<button type="button" onclick={handleClickCopy}>⧉ copy</button>
|
|
296
307
|
</div>
|
|
297
308
|
{/if}
|
|
298
309
|
</div>
|
|
@@ -399,4 +410,4 @@
|
|
|
399
410
|
background: #4488ee;
|
|
400
411
|
}
|
|
401
412
|
|
|
402
|
-
</style>
|
|
413
|
+
</style>
|
package/src/3d/Mesh.svelte
CHANGED
|
@@ -45,25 +45,50 @@
|
|
|
45
45
|
|
|
46
46
|
// check user defined coordinates
|
|
47
47
|
const ux = $derived(checkCoords(xValues, 'Mesh (3D)'));
|
|
48
|
-
const uz = $derived(ux ? checkCoords(zValues, 'Mesh (3D)'
|
|
48
|
+
const uz = $derived(ux ? checkCoords(zValues, 'Mesh (3D)') : null);
|
|
49
49
|
const Uy = $derived.by(() => {
|
|
50
50
|
if (!ux || !uz) return null;
|
|
51
51
|
if (!ismatrix(yValues)) {
|
|
52
52
|
console.error('Mesh: parameter "yValues" must be a matrix with height values of mesh points.');
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
|
+
if (yValues.nrows !== ux.length || yValues.ncols !== uz.length) {
|
|
56
|
+
console.error('Mesh: dimensions of "yValues" must match the lengths of "xValues" and "zValues".');
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
55
59
|
return yValues;
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
|
|
63
|
+
const colors = $derived.by(() => {
|
|
64
|
+
const isValid =
|
|
65
|
+
Array.isArray(colmap) &&
|
|
66
|
+
colmap.length > 0 &&
|
|
67
|
+
colmap.every(color =>
|
|
68
|
+
typeof color === 'string' && color.length > 0
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!isValid) {
|
|
72
|
+
console.error(
|
|
73
|
+
'Mesh: parameter "colmap" must be a non-empty array of color strings.'
|
|
74
|
+
);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return colmap;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
|
|
59
82
|
// compute coordinates of mesh elements
|
|
60
83
|
const mesh = $derived.by(() => {
|
|
61
84
|
|
|
62
|
-
if (!Uy) return null;
|
|
85
|
+
if (!Uy || !colors) return null;
|
|
63
86
|
|
|
64
|
-
const nbins =
|
|
87
|
+
const nbins = colors.length;
|
|
65
88
|
const nx = Uy.nrows;
|
|
66
89
|
const nz = Uy.ncols;
|
|
90
|
+
const nsegmentsX = (nx - 1) * nz;
|
|
91
|
+
const nsegmentsZ = nx * (nz - 1);
|
|
67
92
|
|
|
68
93
|
const nr = range(Uy.v);
|
|
69
94
|
const dnr = (nr[1] - nr[0]) / 100;
|
|
@@ -71,26 +96,36 @@
|
|
|
71
96
|
const right = nr[1] + dnr;
|
|
72
97
|
const span = right - left;
|
|
73
98
|
|
|
99
|
+
function getColorIndex(value) {
|
|
100
|
+
if (!(span > 0)) return Math.floor(nbins / 2);
|
|
101
|
+
|
|
102
|
+
const index = Math.round(
|
|
103
|
+
(value - left) / span * (nbins - 1)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return Math.max(0, Math.min(nbins - 1, index));
|
|
107
|
+
}
|
|
108
|
+
|
|
74
109
|
// compute coordinates for mesh segments lines
|
|
75
|
-
const x1s = Vector.zeros(
|
|
76
|
-
const z1s = Vector.zeros(
|
|
77
|
-
const y1s = Vector.zeros(
|
|
110
|
+
const x1s = Vector.zeros(nsegmentsX);
|
|
111
|
+
const z1s = Vector.zeros(nsegmentsX);
|
|
112
|
+
const y1s = Vector.zeros(nsegmentsX);
|
|
78
113
|
|
|
79
|
-
const x2s = Vector.zeros(
|
|
80
|
-
const z2s = Vector.zeros(
|
|
81
|
-
const y2s = Vector.zeros(
|
|
114
|
+
const x2s = Vector.zeros(nsegmentsZ);
|
|
115
|
+
const z2s = Vector.zeros(nsegmentsZ);
|
|
116
|
+
const y2s = Vector.zeros(nsegmentsZ);
|
|
82
117
|
|
|
83
|
-
const color1 = Vector.zeros(
|
|
118
|
+
const color1 = Vector.zeros(nsegmentsX);
|
|
84
119
|
|
|
85
|
-
const x1e = Vector.zeros(
|
|
86
|
-
const z1e = Vector.zeros(
|
|
87
|
-
const y1e = Vector.zeros(
|
|
120
|
+
const x1e = Vector.zeros(nsegmentsX);
|
|
121
|
+
const z1e = Vector.zeros(nsegmentsX);
|
|
122
|
+
const y1e = Vector.zeros(nsegmentsX);
|
|
88
123
|
|
|
89
|
-
const x2e = Vector.zeros(
|
|
90
|
-
const z2e = Vector.zeros(
|
|
91
|
-
const y2e = Vector.zeros(
|
|
124
|
+
const x2e = Vector.zeros(nsegmentsZ);
|
|
125
|
+
const z2e = Vector.zeros(nsegmentsZ);
|
|
126
|
+
const y2e = Vector.zeros(nsegmentsZ);
|
|
92
127
|
|
|
93
|
-
const color2 = Vector.zeros(
|
|
128
|
+
const color2 = Vector.zeros(nsegmentsZ);
|
|
94
129
|
|
|
95
130
|
let n1 = 0, n2 = 0;
|
|
96
131
|
for (let z = 0; z < nz; z++) {
|
|
@@ -108,7 +143,8 @@
|
|
|
108
143
|
z1e.v[n1] = uz.v[z];
|
|
109
144
|
y1e.v[n1] = Uy.v[z * nx + x + 1];
|
|
110
145
|
|
|
111
|
-
const
|
|
146
|
+
const meanHeight = (Uy.v[z * nx + x] + Uy.v[z * nx + x + 1]) / 2;
|
|
147
|
+
const colInd = getColorIndex(meanHeight);
|
|
112
148
|
color1.v[n1] = colInd;
|
|
113
149
|
n1 = n1 + 1;
|
|
114
150
|
}
|
|
@@ -125,7 +161,8 @@
|
|
|
125
161
|
z2e.v[n2] = uz.v[z + 1];
|
|
126
162
|
y2e.v[n2] = Uy.v[(z + 1) * nx + x];
|
|
127
163
|
|
|
128
|
-
const
|
|
164
|
+
const meanHeight = (Uy.v[z * nx + x] + Uy.v[(z + 1) * nx + x]) / 2;
|
|
165
|
+
const colInd = getColorIndex(meanHeight);
|
|
129
166
|
color2.v[n2] = colInd;
|
|
130
167
|
n2 = n2 + 1;
|
|
131
168
|
}
|
|
@@ -153,11 +190,10 @@
|
|
|
153
190
|
<g class="series series_mesh" style={lineStyleStr} data-title={title}>
|
|
154
191
|
{#if s1s && s1e && s2s && s2e }
|
|
155
192
|
{#each s1s.x as v, i}
|
|
156
|
-
<line x1={s1s.x[i]} x2={s1e.x[i]} y1={s1s.y[i]} y2={s1e.y[i]} style={`stroke:${
|
|
193
|
+
<line x1={s1s.x[i]} x2={s1e.x[i]} y1={s1s.y[i]} y2={s1e.y[i]} style={`stroke:${colors[mesh.color1.v[i]]};`} />
|
|
157
194
|
{/each}
|
|
158
195
|
{#each s2s.x as v, i}
|
|
159
|
-
<line x1={s2s.x[i]} x2={s2e.x[i]} y1={s2s.y[i]} y2={s2e.y[i]} style={`stroke:${
|
|
196
|
+
<line x1={s2s.x[i]} x2={s2e.x[i]} y1={s2s.y[i]} y2={s2e.y[i]} style={`stroke:${colors[mesh.color2.v[i]]};`} />
|
|
160
197
|
{/each}
|
|
161
198
|
{/if}
|
|
162
199
|
</g>
|
|
163
|
-
|
package/src/3d/XAxis.svelte
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
```
|
|
23
23
|
-->
|
|
24
24
|
<script>
|
|
25
|
-
import { getContext } from 'svelte';
|
|
25
|
+
import { getContext, onDestroy } from 'svelte';
|
|
26
26
|
import { Colors } from '../constants';
|
|
27
27
|
|
|
28
28
|
let {
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
$effect(() => {
|
|
42
42
|
axes.setXAxis({show: true, ticks, tickLabels, showGrid, label, lineColor, gridColor, textColor});
|
|
43
43
|
});
|
|
44
|
-
</script>
|
|
45
44
|
|
|
45
|
+
onDestroy(() => {
|
|
46
|
+
axes.setXAxis({show: false});
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
46
49
|
|
package/src/3d/YAxis.svelte
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
```
|
|
23
23
|
-->
|
|
24
24
|
<script>
|
|
25
|
-
import { getContext } from 'svelte';
|
|
25
|
+
import { getContext, onDestroy } from 'svelte';
|
|
26
26
|
import { Colors } from '../constants';
|
|
27
27
|
|
|
28
28
|
let {
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
$effect(() => {
|
|
42
42
|
axes.setYAxis({show: true, ticks, tickLabels, showGrid, label, lineColor, gridColor, textColor});
|
|
43
43
|
});
|
|
44
|
-
</script>
|
|
45
44
|
|
|
45
|
+
onDestroy(() => {
|
|
46
|
+
axes.setYAxis({show: false});
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
46
49
|
|
package/src/3d/ZAxis.svelte
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
```
|
|
23
23
|
-->
|
|
24
24
|
<script>
|
|
25
|
-
import { getContext } from 'svelte';
|
|
25
|
+
import { getContext, onDestroy } from 'svelte';
|
|
26
26
|
import { Colors } from '../constants';
|
|
27
27
|
|
|
28
28
|
let {
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
$effect(() => {
|
|
42
42
|
axes.setZAxis({show: true, ticks, tickLabels, showGrid, label, lineColor, gridColor, textColor});
|
|
43
43
|
});
|
|
44
|
-
</script>
|
|
45
44
|
|
|
45
|
+
onDestroy(() => {
|
|
46
|
+
axes.setZAxis({show: false});
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
46
49
|
|
package/src/ExportDialog.svelte
CHANGED
|
@@ -25,9 +25,13 @@
|
|
|
25
25
|
const aspectRatio = svgWidth / svgHeight;
|
|
26
26
|
|
|
27
27
|
// editable state — height always derived from width to preserve aspect ratio
|
|
28
|
+
const resOptions = [100, 150, 300, 600];
|
|
29
|
+
const parsedInitialRes = Number(initialRes);
|
|
30
|
+
const defaultRes = resOptions.includes(parsedInitialRes) ? parsedInitialRes : 300;
|
|
31
|
+
|
|
28
32
|
let width = $state(initialWidth);
|
|
29
33
|
const height = $derived(Math.max(4, Math.min(20, Math.round(width / aspectRatio * 10) / 10)));
|
|
30
|
-
let res = $state(
|
|
34
|
+
let res = $state(defaultRes);
|
|
31
35
|
let name = $state(fileName);
|
|
32
36
|
const nameValid = $derived(name.trim().length > 0);
|
|
33
37
|
|
|
@@ -73,7 +77,6 @@
|
|
|
73
77
|
width = parseFloat(e.target.value);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
const resOptions = [100, 150, 300, 600];
|
|
77
80
|
let saving = $state(false);
|
|
78
81
|
|
|
79
82
|
function handleSave() {
|
|
@@ -168,8 +171,7 @@
|
|
|
168
171
|
<div class="export-dialog-res-options">
|
|
169
172
|
{#each resOptions as opt}
|
|
170
173
|
<label class="export-dialog-res-btn" class:selected={res === opt}>
|
|
171
|
-
<input type="radio" name="export-res" value={opt}
|
|
172
|
-
onchange={() => res = opt} />
|
|
174
|
+
<input type="radio" name="export-res" value={opt} bind:group={res} />
|
|
173
175
|
{opt}
|
|
174
176
|
</label>
|
|
175
177
|
{/each}
|
|
@@ -179,8 +181,8 @@
|
|
|
179
181
|
</div>
|
|
180
182
|
|
|
181
183
|
<div class="export-dialog-actions">
|
|
182
|
-
<button class="export-dialog-cancel" onclick={onclose} disabled={saving}>Cancel</button>
|
|
183
|
-
<button class="export-dialog-save" onclick={handleSave} disabled={!nameValid}>Save</button>
|
|
184
|
+
<button type="button" class="export-dialog-cancel" onclick={onclose} disabled={saving}>Cancel</button>
|
|
185
|
+
<button type="button" class="export-dialog-save" onclick={handleSave} disabled={!nameValid}>Save</button>
|
|
184
186
|
</div>
|
|
185
187
|
</div>
|
|
186
188
|
</div>
|
package/src/methods.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cbind, vector, isvector, Vector, matrix } from 'mdatools/arrays';
|
|
2
2
|
import { min, max, diff } from 'mdatools/stat';
|
|
3
|
-
import { TICK_NUM, XTICK_NUM, YTICK_NUM, TICK_SIZE, COLMAP_WIDTH_RATIO, COLMAP_EL_HEIGHTS_PX, COLMAP_TOP_MARGIN } from './constants';
|
|
3
|
+
import { TICK_NUM, XTICK_NUM, YTICK_NUM, TICK_SIZE, COLMAP_WIDTH_RATIO, COLMAP_EL_HEIGHTS_PX, COLMAP_TOP_MARGIN } from './constants.js';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -486,13 +486,20 @@ function createPngDataTable() {
|
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
|
|
489
|
+
let pngDataTable = null;
|
|
490
|
+
|
|
491
|
+
|
|
489
492
|
/**
|
|
490
493
|
* Calculate CRC for PNG image
|
|
491
494
|
* @param buf image data
|
|
492
495
|
*/
|
|
493
496
|
function calcCrc(buf) {
|
|
494
497
|
let c = -1;
|
|
495
|
-
|
|
498
|
+
|
|
499
|
+
if (!pngDataTable) {
|
|
500
|
+
pngDataTable = createPngDataTable();
|
|
501
|
+
}
|
|
502
|
+
|
|
496
503
|
for (let n = 0; n < buf.length; n++) {
|
|
497
504
|
c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
|
|
498
505
|
}
|
|
@@ -645,11 +652,10 @@ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
|
645
652
|
canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
|
|
646
653
|
canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
|
|
647
654
|
|
|
648
|
-
//
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
svg.setAttribute('height', svgHeight * scaleFactor);
|
|
655
|
+
// clone the SVG and set export dimensions without changing the live plot
|
|
656
|
+
const exportSvg = svg.cloneNode(true);
|
|
657
|
+
exportSvg.setAttribute('width', svgWidth * scaleFactor);
|
|
658
|
+
exportSvg.setAttribute('height', svgHeight * scaleFactor);
|
|
653
659
|
|
|
654
660
|
// draw a white background
|
|
655
661
|
context.fillStyle = 'white';
|
|
@@ -657,38 +663,43 @@ export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
|
657
663
|
context.scale(scaleFactor, scaleFactor)
|
|
658
664
|
|
|
659
665
|
// draw the SVG onto the canvas
|
|
660
|
-
const svgXml = new XMLSerializer().serializeToString(
|
|
666
|
+
const svgXml = new XMLSerializer().serializeToString(exportSvg);
|
|
661
667
|
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
662
668
|
const url = URL.createObjectURL(blob);
|
|
663
669
|
|
|
664
|
-
// restore original attributes
|
|
665
|
-
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
666
|
-
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
667
|
-
|
|
668
670
|
// notify caller that serialization is done (used by ExportDialog to restore container)
|
|
669
671
|
if (onSerialize) onSerialize();
|
|
670
672
|
|
|
671
673
|
const img = new Image();
|
|
672
674
|
|
|
673
675
|
img.onload = function () {
|
|
676
|
+
try {
|
|
674
677
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
678
|
+
// downscale image if it must be smaller than the current SVG element
|
|
679
|
+
if (specialScale < 1) {
|
|
680
|
+
img.width = img.width * specialScale;
|
|
681
|
+
img.height = img.height * specialScale;
|
|
682
|
+
}
|
|
680
683
|
|
|
681
|
-
|
|
684
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
685
|
+
|
|
686
|
+
// create a data URL for the canvas and adjust its DPI
|
|
687
|
+
const dataURL = canvas.toDataURL('image/png');
|
|
688
|
+
const dataURL2 = changeDPI(dataURL, res);
|
|
682
689
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
690
|
+
// create a temporary link and trigger the download
|
|
691
|
+
const downloadLink = document.createElement('a');
|
|
692
|
+
downloadLink.href = dataURL2;
|
|
693
|
+
downloadLink.download = `${fileName}.png`;
|
|
694
|
+
downloadLink.click();
|
|
695
|
+
} finally {
|
|
696
|
+
URL.revokeObjectURL(url);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
686
699
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
downloadLink.download = `${fileName}.png`;
|
|
691
|
-
downloadLink.click();
|
|
700
|
+
img.onerror = function (error) {
|
|
701
|
+
URL.revokeObjectURL(url);
|
|
702
|
+
console.error('Failed to load the SVG for PNG export.', error);
|
|
692
703
|
};
|
|
693
704
|
|
|
694
705
|
// set the image source
|
|
@@ -718,26 +729,19 @@ export function downloadSVG(svg, fileName) {
|
|
|
718
729
|
const svgHeight = svg.clientHeight;
|
|
719
730
|
const svgWidth = svg.clientWidth;
|
|
720
731
|
|
|
721
|
-
//
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
svg.setAttribute('width', svgWidth + 'px');
|
|
727
|
-
svg.setAttribute('height', svgHeight + 'px');
|
|
728
|
-
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
732
|
+
// clone the SVG and set export dimensions without changing the live plot
|
|
733
|
+
const exportSvg = svg.cloneNode(true);
|
|
734
|
+
exportSvg.setAttribute('width', svgWidth + 'px');
|
|
735
|
+
exportSvg.setAttribute('height', svgHeight + 'px');
|
|
736
|
+
exportSvg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
729
737
|
|
|
730
|
-
const blob = new Blob([
|
|
738
|
+
const blob = new Blob([exportSvg.outerHTML], { type: 'image/svg+xml' });
|
|
731
739
|
const url = window.URL.createObjectURL(blob)
|
|
732
740
|
const a = document.createElement('a')
|
|
733
741
|
a.setAttribute('href', url)
|
|
734
742
|
a.setAttribute('download', `${fileName}.svg`);
|
|
735
743
|
a.click()
|
|
736
|
-
|
|
737
|
-
// restore original attributes
|
|
738
|
-
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
739
|
-
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
740
|
-
origViewBox ? svg.setAttribute('viewBox', origViewBox) : svg.removeAttribute('viewBox');
|
|
744
|
+
setTimeout(() => window.URL.revokeObjectURL(url), 0);
|
|
741
745
|
}
|
|
742
746
|
|
|
743
747
|
/**
|
|
@@ -813,16 +817,11 @@ export async function createPngBlob(svg, width, height) {
|
|
|
813
817
|
// scale the context
|
|
814
818
|
context.scale(scaleFactor, scaleFactor)
|
|
815
819
|
|
|
816
|
-
//
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
// serialize SVG with export dimensions, then restore original attributes
|
|
823
|
-
const svgData = new XMLSerializer().serializeToString(svg);
|
|
824
|
-
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
825
|
-
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
820
|
+
// clone and serialize the SVG with export dimensions without changing the live plot
|
|
821
|
+
const exportSvg = svg.cloneNode(true);
|
|
822
|
+
exportSvg.setAttribute('width', canvas.width);
|
|
823
|
+
exportSvg.setAttribute('height', canvas.height);
|
|
824
|
+
const svgData = new XMLSerializer().serializeToString(exportSvg);
|
|
826
825
|
|
|
827
826
|
// create promise
|
|
828
827
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -852,6 +851,7 @@ export async function createPngBlob(svg, width, height) {
|
|
|
852
851
|
|
|
853
852
|
// add error callback
|
|
854
853
|
img.onerror = function (err) {
|
|
854
|
+
DOMURL.revokeObjectURL(url);
|
|
855
855
|
reject(err);
|
|
856
856
|
};
|
|
857
857
|
|
|
@@ -864,6 +864,66 @@ export async function createPngBlob(svg, width, height) {
|
|
|
864
864
|
}
|
|
865
865
|
|
|
866
866
|
|
|
867
|
+
let colmaps = null;
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
function getColmaps() {
|
|
871
|
+
if (!colmaps) {
|
|
872
|
+
colmaps = [
|
|
873
|
+
["#2679B2"],
|
|
874
|
+
["#2679B2", "#D22C2F"],
|
|
875
|
+
["#2679B2", "#92B42A", "#D22C2F"],
|
|
876
|
+
["#2679B2", "#2E9658", "#F2B825", "#D22C2F"],
|
|
877
|
+
["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
|
|
878
|
+
["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
|
|
879
|
+
["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
|
|
880
|
+
["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
|
|
881
|
+
["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
|
|
882
|
+
["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
|
|
883
|
+
["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
|
|
884
|
+
["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
|
|
885
|
+
["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
|
|
886
|
+
["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
|
|
887
|
+
["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
|
|
888
|
+
["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
|
|
889
|
+
];
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return colmaps;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
function interpolateColor(start, end, amount) {
|
|
897
|
+
const channels = [1, 3, 5].map(offset => {
|
|
898
|
+
const from = parseInt(start.slice(offset, offset + 2), 16);
|
|
899
|
+
const to = parseInt(end.slice(offset, offset + 2), 16);
|
|
900
|
+
|
|
901
|
+
return Math.round(from + (to - from) * amount)
|
|
902
|
+
.toString(16)
|
|
903
|
+
.padStart(2, '0');
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
return `#${channels.join('')}`.toUpperCase();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
function expandColmap(colors, length) {
|
|
911
|
+
const last = colors.length - 1;
|
|
912
|
+
|
|
913
|
+
return Array.from({length}, (_, index) => {
|
|
914
|
+
const position = index * last / (length - 1);
|
|
915
|
+
const start = Math.floor(position);
|
|
916
|
+
const end = Math.min(start + 1, last);
|
|
917
|
+
|
|
918
|
+
return interpolateColor(
|
|
919
|
+
colors[start],
|
|
920
|
+
colors[end],
|
|
921
|
+
position - start
|
|
922
|
+
);
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
|
|
867
927
|
/**
|
|
868
928
|
* Returns an array with 'n' colors.
|
|
869
929
|
*
|
|
@@ -874,27 +934,14 @@ export async function createPngBlob(svg, width, height) {
|
|
|
874
934
|
*
|
|
875
935
|
*/
|
|
876
936
|
export function getcolmap(n, alpha) {
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
[
|
|
880
|
-
[
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
["#2679B2", "#22988A", "#92B42A", "#F79426", "#D22C2F"],
|
|
884
|
-
["#2679B2", "#1C9AA8", "#379531", "#EED524", "#FB7F28", "#D22C2F"],
|
|
885
|
-
["#2679B2", "#1D94A9", "#2E9658", "#92B42A", "#F2B825", "#F47129", "#D22C2F"],
|
|
886
|
-
["#2679B2", "#1E90AA", "#279775", "#519E2F", "#D3CB25", "#F5A326", "#EF672A", "#D22C2F"],
|
|
887
|
-
["#2679B2", "#1F8DAB", "#22988A", "#33953F", "#92B42A", "#EFCA24", "#F79426", "#EB5F2A", "#D22C2F"],
|
|
888
|
-
["#2679B2", "#208BAC", "#1E999A", "#2E9658", "#5FA32E", "#C5C626", "#F2B825", "#F98827", "#E85A2B", "#D22C2F"],
|
|
889
|
-
["#2679B2", "#2089AD", "#1C9AA8", "#29976C", "#379531", "#92B42A", "#EED524", "#F4AA26", "#FB7F28", "#E6552B", "#D22C2F"],
|
|
890
|
-
["#2679B2", "#2188AD", "#1C97A8", "#25987C", "#329546", "#68A62D", "#BCC327", "#F0C524", "#F69E26", "#F77728", "#E4512B", "#D22C2F"],
|
|
891
|
-
["#2679B2", "#2186AD", "#1D94A9", "#22988A", "#2E9658", "#469A2F", "#92B42A", "#DECF25", "#F2B825", "#F79426", "#F47129", "#E34E2C", "#D22C2F"],
|
|
892
|
-
["#2679B2", "#2285AE", "#1E92AA", "#209995", "#2A9767", "#34953A", "#6FA82D", "#B5C128", "#EFCE24", "#F3AD25", "#F98C27", "#F16B29", "#E14B2C", "#D22C2F"],
|
|
893
|
-
["#2679B2", "#2284AE", "#1E90AA", "#1D999F", "#279775", "#31964A", "#519E2F", "#92B42A", "#D3CB25", "#F0C224", "#F5A326", "#FA8527", "#EF672A", "#E0492C", "#D22C2F"],
|
|
894
|
-
["#2679B2", "#2283AE", "#1F8FAB", "#1C9AA8", "#249880", "#2E9658", "#379531", "#73AA2C", "#B0BF28", "#EDD424", "#F2B825", "#F69B26", "#FB7F28", "#ED632A", "#DF472C", "#D22C2F"]
|
|
895
|
-
]
|
|
937
|
+
const palettes = getColmaps();
|
|
938
|
+
const colors = n <= 16
|
|
939
|
+
? palettes[n - 1]?.slice()
|
|
940
|
+
: expandColmap(palettes[15], n);
|
|
941
|
+
|
|
942
|
+
if (!colors) return colors;
|
|
896
943
|
|
|
897
|
-
return alpha ?
|
|
944
|
+
return alpha ? colors.map(v => v + alpha) : colors;
|
|
898
945
|
}
|
|
899
946
|
|
|
900
947
|
|
|
@@ -1175,11 +1222,16 @@ export function handleClick(e, tagName, onclickCallback) {
|
|
|
1175
1222
|
*
|
|
1176
1223
|
* @return {number} size of element in pixels
|
|
1177
1224
|
*/
|
|
1225
|
+
let textMeasureContext = null;
|
|
1226
|
+
|
|
1178
1227
|
export function getTextWidth(text, font) {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1228
|
+
if (!textMeasureContext) {
|
|
1229
|
+
const canvas = document.createElement('canvas');
|
|
1230
|
+
textMeasureContext = canvas.getContext('2d');
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
textMeasureContext.font = font;
|
|
1234
|
+
return textMeasureContext.measureText(text.replace(/<(.|\n)*?>/g, '')).width;
|
|
1183
1235
|
}
|
|
1184
1236
|
|
|
1185
1237
|
|
|
@@ -1523,4 +1575,4 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
|
|
|
1523
1575
|
];
|
|
1524
1576
|
|
|
1525
1577
|
return {grid1, grid2, axisLine, tickCoords, titleCoords, tickLabels};
|
|
1526
|
-
}
|
|
1578
|
+
}
|