svelte-plots-basic 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/2d/Axes.svelte +23 -27
- package/src/2d/Bars.svelte +3 -4
- package/src/2d/Heatmap.svelte +12 -9
- package/src/2d/Legend.svelte +12 -7
- package/src/2d/TextLegend.svelte +1 -0
- package/src/3d/Axes.svelte +6 -7
- package/src/3d/Mesh.svelte +6 -6
- package/src/3d/Segments.svelte +2 -2
- package/src/3d/TextLabels.svelte +2 -1
- package/src/ExportDialog.svelte +359 -0
- package/src/methods.js +38 -18
package/README.md
CHANGED
|
@@ -15,7 +15,13 @@ These websites and web-applications use `svelte-plots-basic` library:
|
|
|
15
15
|
|
|
16
16
|
## News
|
|
17
17
|
|
|
18
|
-
### 3.
|
|
18
|
+
### 3.3.0
|
|
19
|
+
|
|
20
|
+
* Added advanced "save as PNG" option where user can select size and resolution of the image as well as change the filename.
|
|
21
|
+
* several small improvements and bug fixes.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### 3.2.1
|
|
19
25
|
|
|
20
26
|
* Added mouse click handler option for `Heatmap.svelte` component (returns row and column index of the element).
|
|
21
27
|
|
package/package.json
CHANGED
package/src/2d/Axes.svelte
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
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 {
|
|
@@ -103,9 +104,14 @@
|
|
|
103
104
|
downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes);
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/* Open advanced PNG export dialog. */
|
|
108
|
+
let showExportDialog = $state(false);
|
|
109
|
+
function handleClickPNGAdvanced() { showExportDialog = true; }
|
|
110
|
+
function handleCloseExportDialog() { showExportDialog = false; }
|
|
111
|
+
|
|
106
112
|
|
|
107
113
|
// unique ID for clip path
|
|
108
|
-
const clipPathID = 'plottingArea' + Math.
|
|
114
|
+
const clipPathID = 'plottingArea' + Math.random().toString(36).slice(2);
|
|
109
115
|
|
|
110
116
|
// pointers to plot DOM element and its width and height
|
|
111
117
|
let plotElement = $state(0);
|
|
@@ -166,7 +172,7 @@
|
|
|
166
172
|
|
|
167
173
|
// parameters of plot title
|
|
168
174
|
const plotTitle = $derived(text2svg(title));
|
|
169
|
-
const titleHeight = $derived(plotTitle ? (plotTitle.length === title.
|
|
175
|
+
const titleHeight = $derived(plotTitle ? (plotTitle.length === title.length ? 1.0 : 1.4) : 0);
|
|
170
176
|
|
|
171
177
|
// default parameters of box, axis elements, colomap legend and group legend
|
|
172
178
|
let box = $state({show: false});
|
|
@@ -220,7 +226,7 @@
|
|
|
220
226
|
|
|
221
227
|
{#if coords.tickCoords.length === 2 && coords.tickLabels.length === coords.tickCoords[1][0].length}
|
|
222
228
|
<AxisTickLabels las={params.las} pos={params.pos} tickCoords={coords.tickCoords}
|
|
223
|
-
tickLabels={coords.tickLabels}
|
|
229
|
+
tickLabels={coords.tickLabels} textColor={Colors.DARKGRAY} />
|
|
224
230
|
{/if}
|
|
225
231
|
|
|
226
232
|
<!-- tick factor -->
|
|
@@ -262,7 +268,7 @@
|
|
|
262
268
|
<text {x} y={coords.ltop} dx="1.2em" dy="-0.25em" dominant-baseline="hanging"
|
|
263
269
|
fill={params.labelColor}
|
|
264
270
|
font-size="{params.fontSize}em"
|
|
265
|
-
text-anchor="
|
|
271
|
+
text-anchor="end"
|
|
266
272
|
>{@html getTickFactorLabel(params.labelsFactor)}</text>
|
|
267
273
|
{/if}
|
|
268
274
|
{/snippet}
|
|
@@ -340,11 +346,6 @@
|
|
|
340
346
|
user-select: none;
|
|
341
347
|
}
|
|
342
348
|
|
|
343
|
-
.series-points text {
|
|
344
|
-
/* dominant-baseline: central;
|
|
345
|
-
alignment-baseline: central */
|
|
346
|
-
}
|
|
347
|
-
|
|
348
349
|
.tick-labels text,
|
|
349
350
|
.tick-labels tspan,
|
|
350
351
|
.series-textlabels text,
|
|
@@ -444,27 +445,16 @@
|
|
|
444
445
|
|
|
445
446
|
<!-- box -->
|
|
446
447
|
<g class="axes-box">
|
|
447
|
-
{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
id="axes-box"
|
|
451
|
-
onkeydown={handleClick}
|
|
452
|
-
onclick={handleClick}
|
|
453
|
-
onmousemove={handleMove}
|
|
454
|
-
onmousedown={handleDown}
|
|
455
|
-
onmouseup={handleUp}
|
|
456
|
-
/>
|
|
457
|
-
{:else}
|
|
458
|
-
<rect stroke="transparent" fill="transparent"
|
|
448
|
+
<rect stroke={box.show ? box.lineColor : 'transparent'}
|
|
449
|
+
stroke-width={box.show ? box.lineWidth + 'px' : '0'}
|
|
450
|
+
fill="transparent"
|
|
459
451
|
x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
|
|
460
452
|
id="axes-box"
|
|
461
|
-
onkeydown={handleClick}
|
|
462
453
|
onclick={handleClick}
|
|
463
454
|
onmousemove={handleMove}
|
|
464
455
|
onmousedown={handleDown}
|
|
465
456
|
onmouseup={handleUp}
|
|
466
457
|
/>
|
|
467
|
-
{/if}
|
|
468
458
|
</g>
|
|
469
459
|
|
|
470
460
|
|
|
@@ -501,11 +491,18 @@
|
|
|
501
491
|
</p>
|
|
502
492
|
{:else}
|
|
503
493
|
<div class="download-links">
|
|
504
|
-
<button onclick={handleClickSVG}
|
|
505
|
-
<button onclick={handleClickPNG}
|
|
506
|
-
<button onclick={
|
|
494
|
+
<button onclick={handleClickSVG}>⇩ svg</button>
|
|
495
|
+
<button onclick={handleClickPNG}>⇩ png</button>
|
|
496
|
+
<button onclick={handleClickPNGAdvanced}>⇩ png+</button>
|
|
497
|
+
<button onclick={handleClickCopy}>⧉ copy</button>
|
|
507
498
|
</div>
|
|
508
499
|
{/if}
|
|
500
|
+
|
|
501
|
+
{#if showExportDialog}
|
|
502
|
+
<ExportDialog {plotElement} {fileName}
|
|
503
|
+
initialWidth={pngWidth} initialRes={pngRes}
|
|
504
|
+
onclose={handleCloseExportDialog} />
|
|
505
|
+
{/if}
|
|
509
506
|
</div>
|
|
510
507
|
|
|
511
508
|
<style>
|
|
@@ -534,7 +531,6 @@
|
|
|
534
531
|
transition: bottom 0.35s ease;
|
|
535
532
|
box-shadow: 0 0 0.75em #00000060;
|
|
536
533
|
margin: 0.6em;
|
|
537
|
-
box-sizing: border-box;
|
|
538
534
|
|
|
539
535
|
display: flex;
|
|
540
536
|
align-items: center;
|
package/src/2d/Bars.svelte
CHANGED
|
@@ -68,11 +68,10 @@
|
|
|
68
68
|
console.error('Bars: if only one bar must be shown, value for property "barWidthExact" should be provided.');
|
|
69
69
|
return null;
|
|
70
70
|
} else {
|
|
71
|
-
w =
|
|
72
|
-
let lw = 0;
|
|
71
|
+
w = Infinity;
|
|
73
72
|
for (let i = 1; i < n; i++) {
|
|
74
|
-
lw = Math.abs(xv.v[i - 1] - xv.v[i]);
|
|
75
|
-
if (lw
|
|
73
|
+
const lw = Math.abs(xv.v[i - 1] - xv.v[i]);
|
|
74
|
+
if (lw < w) w = lw;
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
77
|
|
package/src/2d/Heatmap.svelte
CHANGED
|
@@ -83,6 +83,8 @@
|
|
|
83
83
|
const nb = lb.length; // number of breaks
|
|
84
84
|
const rl = Array(nb).fill().map(() => []); // coordinates of left for every break
|
|
85
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
|
|
86
88
|
|
|
87
89
|
// adjust left side for the first break and right side for the last
|
|
88
90
|
const llb = lb.slice();
|
|
@@ -97,11 +99,13 @@
|
|
|
97
99
|
if (vj > llb[i] && vj <= llb[i + 1]) {
|
|
98
100
|
rl[i].push(left.v[j] - 0.5);
|
|
99
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]);
|
|
100
104
|
break;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
};
|
|
104
|
-
return {rl, rt};
|
|
108
|
+
return {rl, rt, rr, rc};
|
|
105
109
|
});
|
|
106
110
|
|
|
107
111
|
|
|
@@ -119,13 +123,12 @@
|
|
|
119
123
|
const el = e.target;
|
|
120
124
|
if (el.tagName !== 'rect') return;
|
|
121
125
|
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
e.stopPropagation();
|
|
126
|
+
const row = parseInt(el.getAttribute('data-row'));
|
|
127
|
+
const col = parseInt(el.getAttribute('data-col'));
|
|
128
|
+
if (!isNaN(row) && !isNaN(col)) {
|
|
129
|
+
onclick(row, col);
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
}
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
|
|
@@ -143,7 +146,7 @@
|
|
|
143
146
|
{#if rx[i].length > 0}
|
|
144
147
|
<!-- loop over elements which fall into the interval -->
|
|
145
148
|
{#each rx[i] as v, j}
|
|
146
|
-
<rect x={rx[i][j]} y={ry[i][j]} width={rw} height={rh}/>
|
|
149
|
+
<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
150
|
{/each}
|
|
148
151
|
{/if}
|
|
149
152
|
</g>
|
package/src/2d/Legend.svelte
CHANGED
|
@@ -78,8 +78,9 @@
|
|
|
78
78
|
return null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
const result = [];
|
|
81
82
|
for (let i = 0; i < items.length; i++) {
|
|
82
|
-
const item = items[i];
|
|
83
|
+
const item = {...items[i]};
|
|
83
84
|
|
|
84
85
|
// check and transform label
|
|
85
86
|
if (!item.label) {
|
|
@@ -88,13 +89,14 @@
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
const newLabel = text2svg(item.label);
|
|
91
|
-
const labelHeight = newLabel.length === item.label ? 1 : 1.4;
|
|
92
|
+
const labelHeight = newLabel.length === item.label.length ? 1 : 1.4;
|
|
92
93
|
item.label = newLabel;
|
|
93
|
-
item
|
|
94
|
+
item.labelHeight = labelHeight;
|
|
94
95
|
|
|
95
96
|
// set default values for lines
|
|
96
97
|
if (item.line) {
|
|
97
|
-
|
|
98
|
+
item.line = {...item.line};
|
|
99
|
+
if (!item.line.lineType) item.line.lineType = 1
|
|
98
100
|
if (!(item.line.lineType > 0 && item.line.lineType <= 4)) {
|
|
99
101
|
console.error('Legend: parameter "lineType" for legend item ' + (i + 1) + ' is incorrect.');
|
|
100
102
|
}
|
|
@@ -104,7 +106,8 @@
|
|
|
104
106
|
|
|
105
107
|
// set default values for markers
|
|
106
108
|
if (item.point) {
|
|
107
|
-
|
|
109
|
+
item.point = {...item.point};
|
|
110
|
+
if (!item.point.marker) item.point.marker = 1
|
|
108
111
|
if (!(item.point.marker > 0 && item.point.marker <= MARKER_SYMBOLS.length)) {
|
|
109
112
|
console.error('Legend: parameter "marker" for legend item ' + (i + 1) + ' is incorrect.');
|
|
110
113
|
}
|
|
@@ -113,8 +116,10 @@
|
|
|
113
116
|
item.point.markerSize = item.point.markerSize ? item.point.markerSize : 1;
|
|
114
117
|
item.point.lineColor = item.point.lineColor ? item.point.lineColor : Colors.PRIMARY;
|
|
115
118
|
}
|
|
119
|
+
|
|
120
|
+
result.push(item);
|
|
116
121
|
}
|
|
117
|
-
return
|
|
122
|
+
return result;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
// get context and update group legend parameters reactivey
|
|
@@ -124,7 +129,7 @@
|
|
|
124
129
|
const newPosition = checkPosition(position);
|
|
125
130
|
axes.setGroupLegend(
|
|
126
131
|
newItems && newPosition ?
|
|
127
|
-
{show, position, items, lineColor, faceColor, lineWidth, fontSize} :
|
|
132
|
+
{show, position, items: newItems, lineColor, faceColor, lineWidth, fontSize} :
|
|
128
133
|
{show: false, position: null, items: null}
|
|
129
134
|
);
|
|
130
135
|
});
|
package/src/2d/TextLegend.svelte
CHANGED
package/src/3d/Axes.svelte
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
- `limX` - array with limits for x-axis (in plot units), default: `[0, 1]`.
|
|
6
6
|
- `limY` - array with limits for y-axis (in plot units), default: `[0, 1]`.
|
|
7
7
|
- `limZ` - array with limits for y-axis (in plot units), default: `[0, 1]`.
|
|
8
|
-
- `theta` - angle (in
|
|
9
|
-
- `phi` - angle (in
|
|
8
|
+
- `theta` - angle (in radians) for orientation of projection plane, default: `-10`.
|
|
9
|
+
- `phi` - angle (in radians) for orientation of projection plane, default: `-10`.
|
|
10
10
|
- `zoom` - zooming factor, default: `0.5`.
|
|
11
11
|
|
|
12
12
|
Properties for downloading the plot as PNG or SVG file or copying it to clipboard:
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
theta = -10,
|
|
49
49
|
phi = -10,
|
|
50
50
|
zoom = 0.5,
|
|
51
|
-
downloadLinks = '
|
|
51
|
+
downloadLinks = 'none',
|
|
52
52
|
fileName = 'plot',
|
|
53
53
|
pngWidth = 8,
|
|
54
54
|
pngHeight = 8,
|
|
@@ -290,9 +290,9 @@
|
|
|
290
290
|
</p>
|
|
291
291
|
{:else}
|
|
292
292
|
<div class="download-links">
|
|
293
|
-
<button onclick={handleClickSVG}
|
|
294
|
-
<button onclick={handleClickPNG}
|
|
295
|
-
<button onclick={handleClickCopy}
|
|
293
|
+
<button onclick={handleClickSVG}>⇩ svg</button>
|
|
294
|
+
<button onclick={handleClickPNG}>⇩ png</button>
|
|
295
|
+
<button onclick={handleClickCopy}>⧉ copy</button>
|
|
296
296
|
</div>
|
|
297
297
|
{/if}
|
|
298
298
|
</div>
|
|
@@ -370,7 +370,6 @@
|
|
|
370
370
|
transition: bottom 0.35s ease;
|
|
371
371
|
box-shadow: 0 0 0.75em #00000060;
|
|
372
372
|
margin: 0.6em;
|
|
373
|
-
box-sizing: border-box;
|
|
374
373
|
|
|
375
374
|
display: flex;
|
|
376
375
|
align-items: center;
|
package/src/3d/Mesh.svelte
CHANGED
|
@@ -101,14 +101,14 @@
|
|
|
101
101
|
// x-direction - start
|
|
102
102
|
x1s.v[n1] = ux.v[x];
|
|
103
103
|
z1s.v[n1] = uz.v[z];
|
|
104
|
-
y1s.v[n1] = Uy.v[z *
|
|
104
|
+
y1s.v[n1] = Uy.v[z * nx + x];
|
|
105
105
|
|
|
106
106
|
// x-direction - end
|
|
107
107
|
x1e.v[n1] = ux.v[x + 1];
|
|
108
108
|
z1e.v[n1] = uz.v[z];
|
|
109
|
-
y1e.v[n1] = Uy.v[z *
|
|
109
|
+
y1e.v[n1] = Uy.v[z * nx + x + 1];
|
|
110
110
|
|
|
111
|
-
const colInd = Math.round(((Uy.v[z *
|
|
111
|
+
const colInd = Math.round(((Uy.v[z * nx + x] + Uy.v[z * nx + x + 1]) / 2 - left) / span * (nbins - 1));
|
|
112
112
|
color1.v[n1] = colInd;
|
|
113
113
|
n1 = n1 + 1;
|
|
114
114
|
}
|
|
@@ -118,14 +118,14 @@
|
|
|
118
118
|
// z-direction - start
|
|
119
119
|
x2s.v[n2] = ux.v[x];
|
|
120
120
|
z2s.v[n2] = uz.v[z];
|
|
121
|
-
y2s.v[n2] = Uy.v[z *
|
|
121
|
+
y2s.v[n2] = Uy.v[z * nx + x];
|
|
122
122
|
|
|
123
123
|
// z-direction - end
|
|
124
124
|
x2e.v[n2] = ux.v[x];
|
|
125
125
|
z2e.v[n2] = uz.v[z + 1];
|
|
126
|
-
y2e.v[n2] = Uy.v[(z + 1) *
|
|
126
|
+
y2e.v[n2] = Uy.v[(z + 1) * nx + x];
|
|
127
127
|
|
|
128
|
-
const colInd = Math.round(((Uy.v[z *
|
|
128
|
+
const colInd = Math.round(((Uy.v[z * nx + x] + Uy.v[(z + 1) * nx + x]) / 2 - left) / span * (nbins - 1));
|
|
129
129
|
color2.v[n2] = colInd;
|
|
130
130
|
n2 = n2 + 1;
|
|
131
131
|
}
|
package/src/3d/Segments.svelte
CHANGED
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
const uz1 = $derived(uy1 ? checkCoords(zStart, 'Segments (3D)', ux1.length) : null);
|
|
61
61
|
|
|
62
62
|
const ux2 = $derived(uz1 ? checkCoords(xEnd, 'Segments (3D)', ux1.length) : null);
|
|
63
|
-
const uy2 = $derived(
|
|
64
|
-
const uz2 = $derived(
|
|
63
|
+
const uy2 = $derived(ux2 ? checkCoords(yEnd, 'Segments (3D)', ux1.length) : null);
|
|
64
|
+
const uz2 = $derived(uy2 ? checkCoords(zEnd, 'Segments (3D)', ux1.length) : null);
|
|
65
65
|
|
|
66
66
|
// combine world coordinates to matrices
|
|
67
67
|
const uW1 = $derived(ux1 && uy1 && uz1 ? cbind(ux1, uy1, uz1) : null);
|
package/src/3d/TextLabels.svelte
CHANGED
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
const U = $derived(ux && uy && uz ? cbind(ux, uy, uz) : null);
|
|
56
56
|
|
|
57
57
|
const l = $derived.by(() => {
|
|
58
|
+
if (!ux) return null;
|
|
58
59
|
if(Array.isArray(labels) && labels.length !== ux.length ) {
|
|
59
60
|
console.error('TextLabels: labels provided as an array should have the same number of elements as number of coordinates.');
|
|
60
61
|
return null;
|
|
@@ -79,7 +80,7 @@
|
|
|
79
80
|
<text data-id={i} x={s.x[i]} y={s.y[i]} dx={0} dy={0}>{@html l}</text>
|
|
80
81
|
{/each}
|
|
81
82
|
{:else}
|
|
82
|
-
{#each x as v, i}
|
|
83
|
+
{#each s.x as v, i}
|
|
83
84
|
<text data-id={i} x={s.x[i]} y={s.y[i]} dx={0} dy={0}>{@html l[i]}</text>
|
|
84
85
|
{/each}
|
|
85
86
|
{/if}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Modal dialog for advanced PNG export with live preview and dimension/resolution controls.
|
|
3
|
+
|
|
4
|
+
Properties:
|
|
5
|
+
- `plotElement` - reference to the SVG element to export.
|
|
6
|
+
- `fileName` - file name for download (without extension).
|
|
7
|
+
- `initialWidth` - initial width in cm (default: 8).
|
|
8
|
+
- `initialRes` - initial resolution in DPI (default: 300).
|
|
9
|
+
- `onclose` - callback when dialog is closed.
|
|
10
|
+
-->
|
|
11
|
+
<script>
|
|
12
|
+
import { downloadPNG } from './methods.js';
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
plotElement,
|
|
16
|
+
fileName = 'plot',
|
|
17
|
+
initialWidth = 8,
|
|
18
|
+
initialRes = 300,
|
|
19
|
+
onclose
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
// aspect ratio from the actual SVG element
|
|
23
|
+
const svgWidth = plotElement ? (plotElement.clientWidth || plotElement.getBoundingClientRect().width) : 1;
|
|
24
|
+
const svgHeight = plotElement ? (plotElement.clientHeight || plotElement.getBoundingClientRect().height) : 1;
|
|
25
|
+
const aspectRatio = svgWidth / svgHeight;
|
|
26
|
+
|
|
27
|
+
// editable state — height always derived from width to preserve aspect ratio
|
|
28
|
+
let width = $state(initialWidth);
|
|
29
|
+
const height = $derived(Math.max(4, Math.min(20, Math.round(width / aspectRatio * 10) / 10)));
|
|
30
|
+
let res = $state(initialRes);
|
|
31
|
+
let name = $state(fileName);
|
|
32
|
+
const nameValid = $derived(name.trim().length > 0);
|
|
33
|
+
|
|
34
|
+
// preview container ref
|
|
35
|
+
let previewContainer = $state(null);
|
|
36
|
+
|
|
37
|
+
// max preview width in pixels
|
|
38
|
+
const maxPreviewWidth = 400;
|
|
39
|
+
const previewHeight = $derived(Math.round(maxPreviewWidth / aspectRatio));
|
|
40
|
+
|
|
41
|
+
// clone SVG into preview container on mount
|
|
42
|
+
$effect(() => {
|
|
43
|
+
if (!previewContainer || !plotElement) return;
|
|
44
|
+
|
|
45
|
+
// clear previous content
|
|
46
|
+
previewContainer.innerHTML = '';
|
|
47
|
+
|
|
48
|
+
// clone the SVG
|
|
49
|
+
const clone = plotElement.cloneNode(true);
|
|
50
|
+
|
|
51
|
+
clone.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
52
|
+
clone.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|
53
|
+
clone.style.width = '100%';
|
|
54
|
+
clone.style.height = '100%';
|
|
55
|
+
clone.style.position = 'static';
|
|
56
|
+
|
|
57
|
+
// fixes duplicate clipPath IDs
|
|
58
|
+
const clipPaths = clone.querySelectorAll('clipPath');
|
|
59
|
+
clipPaths.forEach((cp) => {
|
|
60
|
+
const oldId = cp.getAttribute('id');
|
|
61
|
+
if (oldId) {
|
|
62
|
+
const newId = 'preview_' + oldId;
|
|
63
|
+
cp.setAttribute('id', newId);
|
|
64
|
+
const refs = clone.querySelectorAll(`[clip-path="url(#${oldId})"]`);
|
|
65
|
+
refs.forEach((ref) => ref.setAttribute('clip-path', `url(#${newId})`));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
previewContainer.appendChild(clone);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function handleWidthChange(e) {
|
|
73
|
+
width = parseFloat(e.target.value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resOptions = [100, 150, 300, 600];
|
|
77
|
+
let saving = $state(false);
|
|
78
|
+
|
|
79
|
+
function handleSave() {
|
|
80
|
+
if (saving) return;
|
|
81
|
+
saving = true;
|
|
82
|
+
|
|
83
|
+
const container = plotElement.parentElement;
|
|
84
|
+
const origStyle = container.style.cssText;
|
|
85
|
+
|
|
86
|
+
// scale reference width with export width so smaller plots get
|
|
87
|
+
// a smaller scale category → relatively larger fonts and details
|
|
88
|
+
// 4cm → 360px (small), 8cm → 520px (medium), 16cm → 840px (large), 20cm → 1000px (xlarge)
|
|
89
|
+
const refWidth = Math.round(200 + width * 40);
|
|
90
|
+
const refHeight = Math.round(refWidth / aspectRatio);
|
|
91
|
+
|
|
92
|
+
function doExport() {
|
|
93
|
+
requestAnimationFrame(() => {
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
// onSerialize callback fires after downloadPNG has serialized the SVG,
|
|
96
|
+
// so we can safely restore the container and close the dialog
|
|
97
|
+
downloadPNG(plotElement, name.trim(), width, height, res, () => {
|
|
98
|
+
container.style.cssText = origStyle;
|
|
99
|
+
saving = false;
|
|
100
|
+
onclose();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// temporarily resize plot container to a fixed reference size (off-screen,
|
|
107
|
+
// hidden behind the dialog backdrop) so Svelte recomputes the layout
|
|
108
|
+
container.style.cssText = `position:fixed;left:-9999px;top:-9999px;width:${refWidth}px;height:${refHeight}px;overflow:hidden;`;
|
|
109
|
+
|
|
110
|
+
// wait for ResizeObserver → Svelte bindings → derived recomputation → DOM update
|
|
111
|
+
// fallback timeout in case container is already at the target size
|
|
112
|
+
const fallbackTimer = setTimeout(() => { observer.disconnect(); doExport(); }, 500);
|
|
113
|
+
const observer = new ResizeObserver(() => {
|
|
114
|
+
observer.disconnect();
|
|
115
|
+
clearTimeout(fallbackTimer);
|
|
116
|
+
doExport();
|
|
117
|
+
});
|
|
118
|
+
observer.observe(plotElement);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleBackdropClick() {
|
|
122
|
+
if (!saving) onclose();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleDialogClick(e) {
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// close on Escape key
|
|
130
|
+
$effect(() => {
|
|
131
|
+
function handleKeydown(e) {
|
|
132
|
+
if (e.key === 'Escape' && !saving) {
|
|
133
|
+
onclose();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
document.addEventListener('keydown', handleKeydown);
|
|
137
|
+
return () => document.removeEventListener('keydown', handleKeydown);
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
142
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
143
|
+
<div class="export-dialog-backdrop" onclick={handleBackdropClick}>
|
|
144
|
+
<div class="export-dialog" onclick={handleDialogClick}>
|
|
145
|
+
<div class="export-dialog-title">Export as PNG</div>
|
|
146
|
+
|
|
147
|
+
<div class="export-dialog-body">
|
|
148
|
+
<!-- Preview -->
|
|
149
|
+
<div class="export-dialog-preview"
|
|
150
|
+
style="width:{maxPreviewWidth}px;height:{previewHeight}px;"
|
|
151
|
+
bind:this={previewContainer}
|
|
152
|
+
></div>
|
|
153
|
+
|
|
154
|
+
<!-- Controls -->
|
|
155
|
+
<div class="export-dialog-controls">
|
|
156
|
+
<div class="export-dialog-control">
|
|
157
|
+
<label for="export-name">File name:</label>
|
|
158
|
+
<input id="export-name" class="export-dialog-name" class:invalid={!nameValid} type="text" bind:value={name} />
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="export-dialog-control">
|
|
162
|
+
<label for="export-width">Size, cm: ({width} × {height})</label>
|
|
163
|
+
<input id="export-width" type="range" min="4" max="20" step="1" value={width} oninput={handleWidthChange} />
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="export-dialog-res">
|
|
167
|
+
<span class="export-dialog-res-label">Resolution, dpi:</span>
|
|
168
|
+
<div class="export-dialog-res-options">
|
|
169
|
+
{#each resOptions as opt}
|
|
170
|
+
<label class="export-dialog-res-btn" class:selected={res === opt}>
|
|
171
|
+
<input type="radio" name="export-res" value={opt} checked={res === opt}
|
|
172
|
+
onchange={() => res = opt} />
|
|
173
|
+
{opt}
|
|
174
|
+
</label>
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<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
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<style>
|
|
189
|
+
.export-dialog-backdrop {
|
|
190
|
+
position: fixed;
|
|
191
|
+
top: 0;
|
|
192
|
+
left: 0;
|
|
193
|
+
right: 0;
|
|
194
|
+
bottom: 0;
|
|
195
|
+
background: rgba(0, 0, 0, 0.4);
|
|
196
|
+
z-index: 1000;
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.export-dialog {
|
|
203
|
+
background: #fefefe;
|
|
204
|
+
border-radius: 0.5em;
|
|
205
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
|
|
206
|
+
padding: 1.25em;
|
|
207
|
+
min-width: 440px;
|
|
208
|
+
max-width: 90vw;
|
|
209
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
210
|
+
color: #606060;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.export-dialog-title {
|
|
214
|
+
font-size: 1.1em;
|
|
215
|
+
font-weight: bold;
|
|
216
|
+
margin-bottom: 1em;
|
|
217
|
+
color: #303030;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.export-dialog-body {
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-direction: column;
|
|
223
|
+
align-items: center;
|
|
224
|
+
gap: 1em;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.export-dialog-preview {
|
|
228
|
+
border: 1px solid #dadada;
|
|
229
|
+
background: #fff;
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
border-radius: 0.25em;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.export-dialog-controls {
|
|
235
|
+
width: 100%;
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
gap: 0.5em;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.export-dialog-control {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
gap: 0.5em;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.export-dialog-control label {
|
|
248
|
+
min-width: 110px;
|
|
249
|
+
font-size: 0.85em;
|
|
250
|
+
white-space: nowrap;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.export-dialog-control input[type="range"] {
|
|
254
|
+
flex: 1;
|
|
255
|
+
accent-color: #443333;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.export-dialog-name {
|
|
259
|
+
flex: 1;
|
|
260
|
+
padding: 0.25em 0.4em;
|
|
261
|
+
border: 1px solid #dadada;
|
|
262
|
+
border-radius: 0.35em;
|
|
263
|
+
font-size: 0.85em;
|
|
264
|
+
color: #606060;
|
|
265
|
+
background: #fafafa;
|
|
266
|
+
outline: none;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.export-dialog-name:focus {
|
|
270
|
+
border-color: #909090;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.export-dialog-name.invalid {
|
|
274
|
+
border-color: crimson;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.export-dialog-res {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 0.5em;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.export-dialog-res-label {
|
|
284
|
+
min-width: 110px;
|
|
285
|
+
font-size: 0.85em;
|
|
286
|
+
white-space: nowrap;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.export-dialog-res-options {
|
|
290
|
+
display: flex;
|
|
291
|
+
gap: 0.25em;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.export-dialog-res-btn {
|
|
295
|
+
box-sizing: border-box;
|
|
296
|
+
color: #606060;
|
|
297
|
+
border: none;
|
|
298
|
+
box-shadow: none;
|
|
299
|
+
background: #fafafa;
|
|
300
|
+
border-radius: 0.35em;
|
|
301
|
+
padding: 0.25em 0.5em;
|
|
302
|
+
font-size: 0.85em;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
user-select: none;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.export-dialog-res-btn:hover {
|
|
308
|
+
background: #dadada;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.export-dialog-res-btn.selected {
|
|
312
|
+
background: #443333;
|
|
313
|
+
color: #fafafa;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.export-dialog-res-btn input {
|
|
317
|
+
display: none;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.export-dialog-actions {
|
|
321
|
+
display: flex;
|
|
322
|
+
justify-content: flex-end;
|
|
323
|
+
gap: 0.5em;
|
|
324
|
+
margin-top: 1em;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.export-dialog-actions button {
|
|
328
|
+
padding: 0.4em 1.2em;
|
|
329
|
+
border-radius: 0.35em;
|
|
330
|
+
border: none;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
font-size: 0.85em;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.export-dialog-cancel {
|
|
336
|
+
background: #fafafa;
|
|
337
|
+
color: #606060;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.export-dialog-cancel:hover {
|
|
341
|
+
background: #dadada;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.export-dialog-save {
|
|
345
|
+
background: #443333;
|
|
346
|
+
color: #fafafa;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.export-dialog-save:hover {
|
|
350
|
+
background: #665555;
|
|
351
|
+
color: #fafafa;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.export-dialog-save:disabled {
|
|
355
|
+
background: #dadada;
|
|
356
|
+
color: #909090;
|
|
357
|
+
cursor: default;
|
|
358
|
+
}
|
|
359
|
+
</style>
|
package/src/methods.js
CHANGED
|
@@ -290,7 +290,7 @@ export function validateTicks(ticks, lim) {
|
|
|
290
290
|
const right = lim[0] < lim[1] ? lim[1] : lim[0];
|
|
291
291
|
|
|
292
292
|
// check if provided ticks values are outside the axis limit range continue with automatic ticks
|
|
293
|
-
const newTicks = ticks.filter(x => x >= left
|
|
293
|
+
const newTicks = ticks.filter(x => x >= left && x <= right);
|
|
294
294
|
if (newTicks.length !== ticks.length) {
|
|
295
295
|
return [null, 'some of ticks values are outside axis limits.'];
|
|
296
296
|
}
|
|
@@ -381,7 +381,7 @@ export function getAxisTicks(ticks, limIn, maxTickNum, round, whole, deltaFactor
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// make sure the ticks are not aligned with axes limits
|
|
384
|
-
return ticks.filter(x => x >= lim[0]
|
|
384
|
+
return ticks.filter(x => x >= lim[0] && x <= lim[1]);
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
|
|
@@ -456,7 +456,7 @@ export function getScale(width, height) {
|
|
|
456
456
|
* @returns {text} the scale level ("small", "medium" or "large").
|
|
457
457
|
*
|
|
458
458
|
*/
|
|
459
|
-
export function getAxisScale(width
|
|
459
|
+
export function getAxisScale(width) {
|
|
460
460
|
if (width < 400.2) return "small";
|
|
461
461
|
if (width < 700.2) return "medium";
|
|
462
462
|
if (width < 900.2) return "large";
|
|
@@ -588,7 +588,7 @@ function changeDPI(base64Image, dpi) {
|
|
|
588
588
|
* @param {number} res - resolution (pixels per inch)
|
|
589
589
|
*
|
|
590
590
|
*/
|
|
591
|
-
export function downloadPNG (svg, fileName, width, height, res) {
|
|
591
|
+
export function downloadPNG (svg, fileName, width, height, res, onSerialize) {
|
|
592
592
|
|
|
593
593
|
if (!width) width = 10;
|
|
594
594
|
if (!height) height = 10;
|
|
@@ -645,7 +645,9 @@ export function downloadPNG (svg, fileName, width, height, res) {
|
|
|
645
645
|
canvas.width = svgWidth * (specialScale < 1 ? specialScale : scaleFactor);
|
|
646
646
|
canvas.height = svgHeight * (specialScale < 1 ? specialScale : scaleFactor);
|
|
647
647
|
|
|
648
|
-
// set corresponding attributes for SVG element
|
|
648
|
+
// save and set corresponding attributes for SVG element
|
|
649
|
+
const origWidth = svg.getAttribute('width');
|
|
650
|
+
const origHeight = svg.getAttribute('height');
|
|
649
651
|
svg.setAttribute('width', svgWidth * scaleFactor);
|
|
650
652
|
svg.setAttribute('height', svgHeight * scaleFactor);
|
|
651
653
|
|
|
@@ -659,12 +661,19 @@ export function downloadPNG (svg, fileName, width, height, res) {
|
|
|
659
661
|
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
660
662
|
const url = URL.createObjectURL(blob);
|
|
661
663
|
|
|
664
|
+
// restore original attributes
|
|
665
|
+
origWidth ? svg.setAttribute('width', origWidth) : svg.removeAttribute('width');
|
|
666
|
+
origHeight ? svg.setAttribute('height', origHeight) : svg.removeAttribute('height');
|
|
667
|
+
|
|
668
|
+
// notify caller that serialization is done (used by ExportDialog to restore container)
|
|
669
|
+
if (onSerialize) onSerialize();
|
|
670
|
+
|
|
662
671
|
const img = new Image();
|
|
663
672
|
|
|
664
673
|
img.onload = function () {
|
|
665
674
|
|
|
666
675
|
// downscale image if it must be smaller than the current SVG element
|
|
667
|
-
if (specialScale <
|
|
676
|
+
if (specialScale < 1) {
|
|
668
677
|
img.width = img.width * specialScale;
|
|
669
678
|
img.height = img.height * specialScale;
|
|
670
679
|
}
|
|
@@ -708,6 +717,12 @@ export function downloadSVG(svg, fileName) {
|
|
|
708
717
|
|
|
709
718
|
const svgHeight = svg.clientHeight;
|
|
710
719
|
const svgWidth = svg.clientWidth;
|
|
720
|
+
|
|
721
|
+
// save original attributes
|
|
722
|
+
const origWidth = svg.getAttribute('width');
|
|
723
|
+
const origHeight = svg.getAttribute('height');
|
|
724
|
+
const origViewBox = svg.getAttribute('viewBox');
|
|
725
|
+
|
|
711
726
|
svg.setAttribute('width', svgWidth + 'px');
|
|
712
727
|
svg.setAttribute('height', svgHeight + 'px');
|
|
713
728
|
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
@@ -718,6 +733,11 @@ export function downloadSVG(svg, fileName) {
|
|
|
718
733
|
a.setAttribute('href', url)
|
|
719
734
|
a.setAttribute('download', `${fileName}.svg`);
|
|
720
735
|
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');
|
|
721
741
|
}
|
|
722
742
|
|
|
723
743
|
/**
|
|
@@ -793,15 +813,21 @@ export async function createPngBlob(svg, width, height) {
|
|
|
793
813
|
// scale the context
|
|
794
814
|
context.scale(scaleFactor, scaleFactor)
|
|
795
815
|
|
|
796
|
-
// set corresponding attributes for SVG element
|
|
816
|
+
// save and set corresponding attributes for SVG element
|
|
817
|
+
const origWidth = svg.getAttribute('width');
|
|
818
|
+
const origHeight = svg.getAttribute('height');
|
|
797
819
|
svg.setAttribute('width', canvas.width);
|
|
798
820
|
svg.setAttribute('height', canvas.height);
|
|
799
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');
|
|
826
|
+
|
|
800
827
|
// create promise
|
|
801
828
|
const promise = new Promise((resolve, reject) => {
|
|
802
829
|
|
|
803
|
-
//
|
|
804
|
-
const svgData = new XMLSerializer().serializeToString(svg);
|
|
830
|
+
// create blob and URL for serialized SVG
|
|
805
831
|
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
|
806
832
|
const DOMURL = window.URL || window.webkitURL || window;
|
|
807
833
|
const url = DOMURL.createObjectURL(svgBlob);
|
|
@@ -1035,10 +1061,6 @@ export function getYAxisParams(limX, limY, scales, tX, axis) {
|
|
|
1035
1061
|
tickLabels[pos] = ' ';
|
|
1036
1062
|
}
|
|
1037
1063
|
|
|
1038
|
-
// old version of the code above - remove after testing
|
|
1039
|
-
// if (tfLabel.length > 0 && (limY[1] - ticksY.v[ticksY.length - 1]) < ((limY[1] - limY[0]) * 0.05)) {
|
|
1040
|
-
// tickLabels[tickLabels.length - 1] = ' ';
|
|
1041
|
-
// }
|
|
1042
1064
|
|
|
1043
1065
|
return {grid, axisLine, tickCoords, tfCoords, ticks, tickLabels, tickFactor, tfLabel};
|
|
1044
1066
|
}
|
|
@@ -1248,8 +1270,6 @@ export function text2svg(text) {
|
|
|
1248
1270
|
}
|
|
1249
1271
|
|
|
1250
1272
|
let i = 0;
|
|
1251
|
-
const dy = 0.5;
|
|
1252
|
-
const fs = 0.8;
|
|
1253
1273
|
let svgText = '';
|
|
1254
1274
|
while (i < text.length) {
|
|
1255
1275
|
|
|
@@ -1445,7 +1465,7 @@ export function getYAxisCoords3D(yaxis, limX, limY, limZ, scale) {
|
|
|
1445
1465
|
|
|
1446
1466
|
/**
|
|
1447
1467
|
* Computes coordinates of all elements (lines, labels, etc) for z-axis ticks.
|
|
1448
|
-
* @param {Object}
|
|
1468
|
+
* @param {Object} zaxis - JSON with z-axis paramaters.
|
|
1449
1469
|
* @param {Array} limX - array with x-axis limits.
|
|
1450
1470
|
* @param {Array} limY - array with y-axis limits.
|
|
1451
1471
|
* @param {Array} limZ - array with z-axis limits.
|
|
@@ -1469,8 +1489,8 @@ export function getZAxisCoords3D(zaxis, limX, limY, limZ, scale) {
|
|
|
1469
1489
|
|
|
1470
1490
|
// tick labels
|
|
1471
1491
|
const tickLabels = (!zaxis.ticks || !zaxis.tickLabels) ? ticksZ.v : zaxis.tickLabels;
|
|
1472
|
-
if (tickLabels.length !==
|
|
1473
|
-
console.error('
|
|
1492
|
+
if (tickLabels.length !== ticksZ.length) {
|
|
1493
|
+
console.error('ZAxis (3D): "tickLabels" must be a array of the same size as ticks.');
|
|
1474
1494
|
}
|
|
1475
1495
|
|
|
1476
1496
|
// combine all coordinates together
|