svelte-plots-basic 2.5.7 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +530 -445
- package/package.json +5 -2
- package/src/2d/Area.svelte +56 -48
- package/src/2d/Axes.svelte +409 -373
- package/src/2d/AxisLines.svelte +37 -36
- package/src/2d/AxisTickLabels.svelte +41 -49
- package/src/2d/Bars.svelte +102 -47
- package/src/2d/Box.svelte +21 -41
- package/src/2d/ColormapLegend.svelte +47 -111
- package/src/2d/Heatmap.svelte +107 -85
- package/src/2d/Legend.svelte +115 -126
- package/src/2d/Lines.svelte +54 -40
- package/src/2d/Multilines.svelte +73 -46
- package/src/2d/Points.svelte +76 -58
- package/src/2d/Rectangles.svelte +67 -58
- package/src/2d/Segments.svelte +59 -46
- package/src/2d/TextLabels.svelte +102 -72
- package/src/2d/TextLegend.svelte +17 -39
- package/src/2d/XAxis.svelte +49 -98
- package/src/2d/YAxis.svelte +47 -93
- package/src/2d/index.js +2 -1
- package/src/3d/Axes.svelte +306 -348
- package/src/3d/AxisLines.svelte +19 -37
- package/src/3d/AxisTickLabels.svelte +15 -32
- package/src/3d/Lines.svelte +59 -32
- package/src/3d/Mesh.svelte +106 -128
- package/src/3d/Points.svelte +56 -36
- package/src/3d/Segments.svelte +68 -52
- package/src/3d/TextLabels.svelte +63 -49
- package/src/3d/XAxis.svelte +36 -113
- package/src/3d/YAxis.svelte +36 -113
- package/src/3d/ZAxis.svelte +37 -114
- package/src/3d/index.js +2 -1
- package/src/constants.js +103 -0
- package/src/methods.js +1484 -0
- package/src/2d/Axis.svelte +0 -60
- package/src/3d/Axis.svelte +0 -64
- package/src/Colors.js +0 -51
- package/src/Utils.js +0 -626
package/src/2d/Axes.svelte
CHANGED
|
@@ -1,380 +1,332 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Parent component for any plot, all other components should be nested inside `<Axes></Axes>`.
|
|
3
|
+
|
|
4
|
+
Main properties:
|
|
5
|
+
- `limX` - array with limits for x-axis (in plot units), default: `[0, 1]`.
|
|
6
|
+
- `limY` - array with limits for y-axis (in plot units), default: `[0, 1]`.
|
|
7
|
+
- `title` - title of the plot (string).
|
|
8
|
+
- `margins` - array with relative margins (bottom, left, top, right), default `[1.0, 0.85, 0.5, 0.5]`.
|
|
9
|
+
|
|
10
|
+
Properties for downloading the plot as PNG or SVG file or copying it to clipboard:
|
|
11
|
+
- `downloadLinks` - how to show download links panel (`'none'` - default, `'hover'`, `'fixed'`).
|
|
12
|
+
- `fileName` - file name for download (without extension), default: `'plot'`.
|
|
13
|
+
- `pngWidth` - width of PNG image in cm, default `8`
|
|
14
|
+
- `pngHeight` - height of PNG image in cm, default `8`.
|
|
15
|
+
- `pngRes` - resolution of PNG image (pixels per inch), default `300`.
|
|
16
|
+
- `clipboardWidth` - width of plot image to copy to clipboard in pixels.
|
|
17
|
+
- `clipboardHeight` - height of plot image to copy to clipboard in pixels.Axes
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
|
|
21
|
+
```svelte
|
|
22
|
+
<script>
|
|
23
|
+
import {Axes} from 'svelte-plots-basic/2d';
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<Axes limX={[2000, 2050]} limY={[0, 100]} title="GDP of contries" >
|
|
27
|
+
</Axes>
|
|
28
|
+
```
|
|
29
|
+
-->
|
|
1
30
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// event dispatcher
|
|
43
|
-
const dispatch = createEventDispatcher();
|
|
44
|
-
|
|
45
|
-
// how big are margins (number of pixels in unit margin value) between axis and plot area if axis are shown
|
|
46
|
-
const AXES_MARGIN_FACTORS = {
|
|
47
|
-
'small': 30,
|
|
48
|
-
'medium': 40,
|
|
49
|
-
'large': 50,
|
|
50
|
-
'xlarge': 60
|
|
31
|
+
import { setContext } from 'svelte';
|
|
32
|
+
import { Colors, PLOT_FONT_SIZE, AXES_MARGIN_FACTORS, TICK_SIZE, LINE_STYLES, MARKER_SYMBOLS } from '../constants.js';
|
|
33
|
+
import { downloadSVG, downloadPNG, copyToClipboard, checkArray, getScale, getAxisScale, getXAxisParams, getYAxisParams,
|
|
34
|
+
transformCoords, getColormapLegendParams, getColormapLegendCoords, getTickFactorLabel,
|
|
35
|
+
getGroupLegendCoords, text2svg,
|
|
36
|
+
invTransformCoords,
|
|
37
|
+
invTransformObjects} from '../methods.js';
|
|
38
|
+
|
|
39
|
+
import AxisLines from './AxisLines.svelte';
|
|
40
|
+
import AxisTickLabels from './AxisTickLabels.svelte';
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
title,
|
|
45
|
+
limX = [0, 1],
|
|
46
|
+
limY = [0, 1],
|
|
47
|
+
margins = [1.0, 1.1, 0.55, 0.60],
|
|
48
|
+
downloadLinks = 'none',
|
|
49
|
+
fileName = 'plot',
|
|
50
|
+
pngWidth = 8,
|
|
51
|
+
pngHeight = 8,
|
|
52
|
+
pngRes = 300,
|
|
53
|
+
clipboardWidth = 1200,
|
|
54
|
+
clipboardHeight = 800,
|
|
55
|
+
onclick = null,
|
|
56
|
+
onmousemove = null,
|
|
57
|
+
onmousedown = null,
|
|
58
|
+
onmouseup = null,
|
|
59
|
+
children
|
|
60
|
+
} = $props();
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/* handler for mouse events */
|
|
64
|
+
function handleMouse(e, f) {
|
|
65
|
+
if (f && e.target.id == "axes-box") {
|
|
66
|
+
const x = invTransformCoords([e.offsetX], tX)[0];
|
|
67
|
+
const y = invTransformCoords([e.offsetY], tY)[0];
|
|
68
|
+
f(x, y);
|
|
69
|
+
}
|
|
51
70
|
}
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'small': 5,
|
|
56
|
-
'medium': 8,
|
|
57
|
-
'large': 12,
|
|
58
|
-
'xlarge': 15
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// size of ticks
|
|
62
|
-
const TICK_SIZE = {
|
|
63
|
-
'small': 4,
|
|
64
|
-
'medium': 6,
|
|
65
|
-
'large': 8,
|
|
66
|
-
'xlarge': 10
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// font size for plot element
|
|
70
|
-
const LEGEND_FONT_SIZE = {
|
|
71
|
-
"small": 11,
|
|
72
|
-
"medium": 14,
|
|
73
|
-
"large": 16,
|
|
74
|
-
"xlarge": 18
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// font size for legend items in pixels
|
|
78
|
-
const PLOT_FONT_SIZE = {
|
|
79
|
-
"small": 12,
|
|
80
|
-
"medium": 15,
|
|
81
|
-
"large": 19,
|
|
82
|
-
"xlarge": 21
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// margin between plot series elements and data labels
|
|
86
|
-
const LABELS_MARGIN = {
|
|
87
|
-
'small': 11,
|
|
88
|
-
'medium': 15,
|
|
89
|
-
'large': 17,
|
|
90
|
-
'xlarge': 19
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// line styles for different scales and types
|
|
94
|
-
const LINE_STYLES = {
|
|
95
|
-
small: ['0', '3,3', '1,1', '3,1'],
|
|
96
|
-
medium: ['0', '5,5', '2,2', '5,2'],
|
|
97
|
-
large: ['0', '7,7', '3,3', '7,3'],
|
|
98
|
-
xlarge: ['0', '9,9', '4,4', '9,3'],
|
|
72
|
+
function handleDown(e) {
|
|
73
|
+
handleMouse(e, onmousedown)
|
|
99
74
|
}
|
|
100
75
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// constant to make clip path ID unique
|
|
105
|
-
const clipPathID = 'plottingArea' + Math.round(Math.random() * 10000);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/*****************************************/
|
|
109
|
-
/* Variable parameters for internal use */
|
|
110
|
-
/*****************************************/
|
|
111
|
-
|
|
112
|
-
/* parameters for internal use inside the component */
|
|
113
|
-
let plotElement;
|
|
114
|
-
let width = 100, height = 100;
|
|
115
|
-
let left = 0, bottom = 0, top = 0;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/*****************************************/
|
|
119
|
-
/* Helper functions */
|
|
120
|
-
/*****************************************/
|
|
76
|
+
function handleUp(e) {
|
|
77
|
+
handleMouse(e, onmouseup)
|
|
78
|
+
}
|
|
121
79
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
* @param {string} eventName - name of the event.
|
|
125
|
-
* @param {HTMLDOMElement} el - DOM element the click was registered for.
|
|
126
|
-
*
|
|
127
|
-
*/
|
|
128
|
-
function dispatchClickEvent(eventName, el) {
|
|
129
|
-
const id = Array.prototype.indexOf.call(el.parentNode.children, el)
|
|
130
|
-
dispatch(eventName, {seriesTitle: el.parentNode.getAttribute('title'), elementID: id});
|
|
80
|
+
function handleMove(e) {
|
|
81
|
+
handleMouse(e, onmousemove)
|
|
131
82
|
}
|
|
132
83
|
|
|
133
|
-
/**
|
|
134
|
-
* Handler (router) for axes or series click events.
|
|
135
|
-
*
|
|
136
|
-
* @param {event} e - event object.
|
|
137
|
-
*
|
|
138
|
-
* @description
|
|
139
|
-
* Checks which element the click was made on and dispatch a corresponding event.
|
|
140
|
-
*
|
|
141
|
-
*/
|
|
142
84
|
function handleClick(e) {
|
|
143
|
-
|
|
144
|
-
// click on scatter plot markers
|
|
145
|
-
if (e.target.tagName === 'text' && e.target.parentNode.classList.contains('series-points')) {
|
|
146
|
-
dispatchClickEvent('markerclick', e.target);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// click on bar plot bars
|
|
151
|
-
if (e.target.tagName === 'rect' && e.target.parentNode.classList.contains('series-bar')) {
|
|
152
|
-
dispatchClickEvent('barclick', e.target);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// click outside any plot element
|
|
157
|
-
dispatch('axesclick');
|
|
85
|
+
handleMouse(e, onclick)
|
|
158
86
|
}
|
|
159
87
|
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Handler (router) for click event on copy plot button.
|
|
163
|
-
*
|
|
164
|
-
* @description
|
|
165
|
-
* Copy plot to clipboard as PNG image.
|
|
166
|
-
*
|
|
167
|
-
*/
|
|
88
|
+
/* Copy plot to clipboard as PNG image. */
|
|
168
89
|
async function handleClickCopy(e) {
|
|
169
|
-
|
|
170
|
-
new ClipboardItem({ 'image/png': createPngBlob(plotElement, clipboardWidth, clipboardHeight) })
|
|
171
|
-
]).catch(function (error) { console.error(error); });
|
|
172
|
-
|
|
173
|
-
const content = e.target.textContent;
|
|
174
|
-
e.target.classList.add('copied');
|
|
175
|
-
e.target.textContent = '✓ done';
|
|
176
|
-
|
|
177
|
-
// Remove the class after 1 second and revert the text
|
|
178
|
-
setTimeout(() => {
|
|
179
|
-
e.target.classList.remove('copied');
|
|
180
|
-
e.target.textContent = content;
|
|
181
|
-
}, 500);
|
|
90
|
+
copyToClipboard(e.target, plotElement, clipboardWidth, clipboardHeight);
|
|
182
91
|
}
|
|
183
92
|
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Handler (router) for click event to download SVG image.
|
|
187
|
-
*
|
|
188
|
-
* @description
|
|
189
|
-
* Copy plot to clipboard as SVG image.
|
|
190
|
-
*
|
|
191
|
-
*/
|
|
93
|
+
/* Download plot as SVG image. */
|
|
192
94
|
function handleClickSVG() {
|
|
193
95
|
downloadSVG(plotElement, fileName);
|
|
194
96
|
}
|
|
195
97
|
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Handler (router) for click event to download PNG image.
|
|
199
|
-
*
|
|
200
|
-
* @description
|
|
201
|
-
* Copy plot to clipboard as PNG image.
|
|
202
|
-
*
|
|
203
|
-
*/
|
|
98
|
+
/* Download plot as PNG image. */
|
|
204
99
|
function handleClickPNG() {
|
|
205
100
|
downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes);
|
|
206
101
|
}
|
|
207
102
|
|
|
208
|
-
/**
|
|
209
|
-
* Generic function to transform x or y-values from plot coordinates to screen (SVG) coordinates.
|
|
210
|
-
*
|
|
211
|
-
* @param {Array|Vector} v - vector with coordinates (or objects size) in original plot coordinates.
|
|
212
|
-
* @param {Object} tA - array with scaling and translation factors.
|
|
213
|
-
*
|
|
214
|
-
* @returns {Vector} vector with rescaled values
|
|
215
|
-
*
|
|
216
|
-
*/
|
|
217
|
-
const transform = function(v, tA) {
|
|
218
|
-
|
|
219
|
-
if (!$isOk || v === undefined || v === null) return undefined;
|
|
220
|
-
if (Array.isArray(v)) v = vector(v);
|
|
221
|
-
if (!isvector(v) || v.length < 1) return undefined;
|
|
222
|
-
|
|
223
|
-
return v.apply(a => roundCoords((a - tA[1]) * tA[0] + tA[2])).v;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Generic function to transform x or y-values from screen (SVG) coordinates to plot coordinates.
|
|
229
|
-
*
|
|
230
|
-
* @param {Array|Vector} v - vector with coordinates (or objects size) in screen (SVG) coordinates.
|
|
231
|
-
* @param {Object} tA - array with scaling and translation factors.
|
|
232
|
-
*
|
|
233
|
-
* @returns {Vector} vector plot coordinates.
|
|
234
|
-
*
|
|
235
|
-
*/
|
|
236
|
-
const invTransform = function(v, tA) {
|
|
237
|
-
|
|
238
|
-
if (!$isOk || v === undefined || v === null) return undefined;
|
|
239
|
-
if (Array.isArray(v)) v = vector(v);
|
|
240
|
-
if (!isvector(v) || v.length < 1) return undefined;
|
|
241
|
-
|
|
242
|
-
return v.apply(a => (a - tA[2]) / tA[0] + tA[1]).v;
|
|
243
|
-
}
|
|
244
103
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
/*****************************************/
|
|
248
|
-
|
|
249
|
-
const scale = writable('medium'); // scale factor (how big the shown plot is)
|
|
250
|
-
const isOk = writable(false); // are axes ready for drawing
|
|
251
|
-
const xLim = writable(limX); // validated values for x-axis limits
|
|
252
|
-
const yLim = writable(limY); // validated values for y-axis limits
|
|
253
|
-
const tX = writable({'coords': [1, 0, 0], 'objects': [1, 0, 0]}); // scaling and translation factors for x-dimension
|
|
254
|
-
const tY = writable({'coords': [1, 0, 0], 'objects': [1, 0, 0]}); // scaling and translation factors for y-dimension
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
/*****************************************/
|
|
258
|
-
/* Axes context */
|
|
259
|
-
/*****************************************/
|
|
260
|
-
|
|
261
|
-
let context = {
|
|
262
|
-
|
|
263
|
-
// methods
|
|
264
|
-
transform: transform,
|
|
265
|
-
invTransform: invTransform,
|
|
266
|
-
|
|
267
|
-
// variables
|
|
268
|
-
scale: scale,
|
|
269
|
-
isOk: isOk,
|
|
270
|
-
xLim: xLim,
|
|
271
|
-
yLim: yLim,
|
|
272
|
-
tX: tX,
|
|
273
|
-
tY: tY,
|
|
274
|
-
xLabel: xLabel,
|
|
275
|
-
yLabel: yLabel,
|
|
276
|
-
|
|
277
|
-
// constants
|
|
278
|
-
LINE_STYLES: LINE_STYLES,
|
|
279
|
-
LABELS_MARGIN: LABELS_MARGIN,
|
|
280
|
-
TICK_NUM: TICK_NUM,
|
|
281
|
-
TICK_SIZE: TICK_SIZE,
|
|
282
|
-
MARKER_SYMBOLS: MARKER_SYMBOLS,
|
|
283
|
-
LEGEND_FONT_SIZE: LEGEND_FONT_SIZE
|
|
284
|
-
}
|
|
104
|
+
// unique ID for clip path
|
|
105
|
+
const clipPathID = 'plottingArea' + Math.round(Math.random() * 10000);
|
|
285
106
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
107
|
+
// pointers to plot DOM element and its width and height
|
|
108
|
+
let plotElement = $state(0);
|
|
109
|
+
let plotWidth = $state(0);
|
|
110
|
+
let plotHeight = $state(0);
|
|
111
|
+
|
|
112
|
+
// scales and related parameters
|
|
113
|
+
const scales = $derived({'plot': getScale(plotWidth, plotHeight), 'x': getAxisScale(plotWidth), 'y': getAxisScale(plotHeight)});
|
|
114
|
+
const pxMargins = $derived(margins.map(v => v * AXES_MARGIN_FACTORS[scales.plot]));
|
|
115
|
+
const fontSize = $derived(PLOT_FONT_SIZE[scales.plot]);
|
|
116
|
+
const tickSize = $derived(TICK_SIZE[scales.plot]);
|
|
117
|
+
const lineStyles = $derived(LINE_STYLES[scales.plot]);
|
|
118
|
+
|
|
119
|
+
// left and bottom margins as well as width and height of the axes area (including margins)
|
|
120
|
+
const bottom = $derived(xaxis.show && xaxis.label !== '' ? fontSize * 1.5 : 0);
|
|
121
|
+
const left = $derived(yaxis.show && yaxis.label !== '' ? fontSize * 1.5 : 0);
|
|
122
|
+
const top = $derived(title && title !== '' ? fontSize * 1.5 : 0);
|
|
123
|
+
const width = $derived(plotWidth > left ? plotWidth - left : 0);
|
|
124
|
+
const height = $derived(plotHeight > (bottom + top) ? plotHeight - bottom - top : 0);
|
|
125
|
+
|
|
126
|
+
// size of the axes area without margins
|
|
127
|
+
const axisHeight = $derived(height - pxMargins[0] - pxMargins[2]);
|
|
128
|
+
const axisWidth = $derived(width - pxMargins[1] - pxMargins[3]);
|
|
129
|
+
|
|
130
|
+
// plot status
|
|
131
|
+
const isOk = $derived(
|
|
132
|
+
checkArray(limX, 2) &&
|
|
133
|
+
checkArray(limY, 2) &&
|
|
134
|
+
limX[0] < limX[1] &&
|
|
135
|
+
limY[0] < limY[1] &&
|
|
312
136
|
width > (pxMargins[1] + pxMargins[3]) &&
|
|
313
137
|
height > (pxMargins[0] + pxMargins[2])
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}));
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// update transformation array for y-coordinates
|
|
327
|
-
$: {
|
|
328
|
-
if ($isOk) {
|
|
329
|
-
tY.update(v => ({
|
|
330
|
-
'coords': [-(height - pxMargins[0] - pxMargins[2]) / ($yLim[1] - $yLim[0]), $yLim[1], pxMargins[2]],
|
|
331
|
-
'objects': [ (height - pxMargins[0] - pxMargins[2]) / ($yLim[1] - $yLim[0]), 0, 0]
|
|
332
|
-
}));
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
// transformation matrix for x
|
|
142
|
+
const tX = $derived(!isOk ? {'coords': [1, 0, 0], 'objects': [1, 0, 0]} :
|
|
143
|
+
{
|
|
144
|
+
'coords': [ axisWidth / (limX[1] - limX[0]), limX[0], pxMargins[1]],
|
|
145
|
+
'objects': [ axisWidth / (limX[1] - limX[0]), 0, 0]
|
|
333
146
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
/*****************************************/
|
|
342
|
-
/* Events observers */
|
|
343
|
-
/*****************************************/
|
|
344
|
-
|
|
345
|
-
// observer for the plot area size — to update the scale
|
|
346
|
-
const ro = new ResizeObserver(entries => {
|
|
347
|
-
for (let entry of entries) {
|
|
348
|
-
const pcr = plotElement.getBoundingClientRect();
|
|
349
|
-
const scl = getScale(pcr.width, pcr.height);
|
|
350
|
-
scale.update(x => scl);
|
|
351
|
-
|
|
352
|
-
const m = PLOT_FONT_SIZE[scl] * 1.5;
|
|
353
|
-
left = xLabel && xLabel !== '' ? m : 0;
|
|
354
|
-
bottom = yLabel && yLabel !== '' ? m : 0;
|
|
355
|
-
top = title && title !== '' ? m : 0;
|
|
356
|
-
width = pcr.width > left ? pcr.width - left : 0;
|
|
357
|
-
height = pcr.height > (bottom + top) ? pcr.height - bottom - top : 0;
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// transformation matrix for y
|
|
150
|
+
const tY = $derived(!isOk ? {'coords': [0, 1, 0], 'objects': [0, 1, 0]} :
|
|
151
|
+
{
|
|
152
|
+
'coords': [-axisHeight / (limY[1] - limY[0]), limY[1], pxMargins[2]],
|
|
153
|
+
'objects': [ axisHeight / (limY[1] - limY[0]), 0, 0]
|
|
358
154
|
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// coordinates if the middle point of plotting area (needed for axis labels location)
|
|
158
|
+
const my = $derived(axisHeight * 0.5 + top + pxMargins[2]);
|
|
159
|
+
const mx = $derived(axisWidth * 0.5 + left + pxMargins[1]);
|
|
160
|
+
|
|
161
|
+
// coordinates for clip path box
|
|
162
|
+
const cpx = $derived(isOk ? transformCoords(limX, tX) : [0, 1]);
|
|
163
|
+
const cpy = $derived(isOk ? transformCoords(limY, tY) : [1, 0]);
|
|
164
|
+
|
|
165
|
+
// parameters of plot title
|
|
166
|
+
const plotTitle = $derived(text2svg(title));
|
|
167
|
+
const titleHeight = $derived(plotTitle ? (plotTitle.length === title.left ? 1.0 : 1.4) : 0);
|
|
168
|
+
|
|
169
|
+
// default parameters of box, axis elements, colomap legend and group legend
|
|
170
|
+
let box = $state({show: false});
|
|
171
|
+
let xaxis = $state({show: false, error: ''});
|
|
172
|
+
let yaxis = $state({show: false, error: ''});
|
|
173
|
+
let clg = $state({show: false, colmap: null, breaks: null});
|
|
174
|
+
let glg = $state({show: false, items: null, position: null});
|
|
175
|
+
|
|
176
|
+
// set colormap legend parameters if requested
|
|
177
|
+
const clgParams = $derived(isOk && clg && clg.show ? getColormapLegendParams(clg) : null);
|
|
178
|
+
|
|
179
|
+
// compute world coordinates of axis elements
|
|
180
|
+
const xaxisCoords = $derived(isOk && xaxis.show ? getXAxisParams(limX, limY, scales, tY, xaxis) : null);
|
|
181
|
+
const yaxisCoords = $derived(isOk && yaxis.show ? getYAxisParams(limX, limY, scales, tX, yaxis) : null);
|
|
182
|
+
|
|
183
|
+
// compute screen coordinates of legend elements
|
|
184
|
+
const clgCoords = $derived(isOk && clgParams ? getColormapLegendCoords(clgParams, axisWidth, pxMargins, scales) : null);
|
|
185
|
+
const glgCoords = $derived(isOk && glg && glg.show && cpx? getGroupLegendCoords(glg, cpx, cpy, fontSize, tickSize) : null);
|
|
186
|
+
|
|
187
|
+
// axes context to share with children
|
|
188
|
+
setContext('axes', {
|
|
189
|
+
setBox: (v) => box = v,
|
|
190
|
+
setXAxis: (v) => xaxis = v,
|
|
191
|
+
setYAxis: (v) => yaxis = v,
|
|
192
|
+
setColmapLegend: (v) => clg = v,
|
|
193
|
+
setGroupLegend: (v) => glg = v,
|
|
194
|
+
scales: () => scales,
|
|
195
|
+
tX: () => tX,
|
|
196
|
+
tY: () => tY,
|
|
197
|
+
limX: () => limX,
|
|
198
|
+
limY: () => limY
|
|
359
199
|
});
|
|
200
|
+
</script>
|
|
360
201
|
|
|
361
|
-
|
|
202
|
+
<!-- snippet for axis -->
|
|
203
|
+
{#snippet axisSnippet(coords, params)}
|
|
362
204
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
205
|
+
{#if params.error}
|
|
206
|
+
{#if params.pos === 4}
|
|
207
|
+
<text x={cpx[0] - 10} y={my} fill="crimson" transform={`rotate(-90, ${cpx[0] - 10}, ${my})`} text-anchor="middle">{params.error}</text>
|
|
208
|
+
{:else}
|
|
209
|
+
<text x={mx} y={cpy[0]} dy='10px' fill="crimson" text-anchor="middle" dominant-baseline="hanging">{params.error}</text>
|
|
210
|
+
{/if}
|
|
211
|
+
{:else}
|
|
212
|
+
{#if params.showGrid }
|
|
213
|
+
<AxisLines lineCoords={coords.grid} lineColor={Colors.MIDDLEGRAY} lineType={3} />
|
|
214
|
+
{/if}
|
|
215
|
+
|
|
216
|
+
<AxisLines lineCoords={coords.axisLine} lineColor={Colors.DARKGRAY} lineType={1} />
|
|
217
|
+
<AxisLines lineCoords={coords.tickCoords} lineColor={Colors.DARKGRAY} lineType={1} />
|
|
218
|
+
|
|
219
|
+
{#if coords.tickCoords.length === 2 && coords.tickLabels.length === coords.tickCoords[1][0].length}
|
|
220
|
+
<AxisTickLabels las={params.las} pos={params.pos} tickCoords={coords.tickCoords}
|
|
221
|
+
tickLabels={coords.tickLabels} tickColor={Colors.DARKGRAY} hideLast={params.hideLast}/>
|
|
222
|
+
{/if}
|
|
223
|
+
|
|
224
|
+
<!-- tick factor -->
|
|
225
|
+
{#if coords.tickFactor !== 0}
|
|
226
|
+
<AxisTickLabels pos={params.pos} tickCoords={coords.tfCoords} tickLabels={[coords.tfLabel]} textColor={Colors.DARKGRAY} />
|
|
227
|
+
{/if}
|
|
228
|
+
{/if}
|
|
366
229
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
230
|
+
{/snippet}
|
|
231
|
+
|
|
232
|
+
<!-- snippedt for colormap legend -->
|
|
233
|
+
{#snippet clgSnippet(params, coords)}
|
|
234
|
+
|
|
235
|
+
{@const dy = coords.height * 1.25}
|
|
236
|
+
{@const n = params.colmap.length}
|
|
237
|
+
|
|
238
|
+
{#each params.colmap as col, i}
|
|
239
|
+
{@const x = coords.start + i * coords.width}
|
|
240
|
+
<rect {x} y={coords.top} width={coords.width} height={coords.height} fill={col} />
|
|
241
|
+
<text {x} y={coords.ltop} dx={coords.dxl} dy={0} dominant-baseline="hanging"
|
|
242
|
+
fill={params.labelColor}
|
|
243
|
+
font-size={params.fontSize + "em"}
|
|
244
|
+
text-anchor="middle"
|
|
245
|
+
>{@html params.labels[i]}</text>
|
|
246
|
+
{/each}
|
|
247
|
+
|
|
248
|
+
{#if !params.isCentered}
|
|
249
|
+
{@const x = coords.start + n * coords.width }
|
|
250
|
+
<text {x} y={coords.ltop} dx={0} dy={0} dominant-baseline="hanging"
|
|
251
|
+
fill={params.labelColor}
|
|
252
|
+
font-size="{params.fontSize}em"
|
|
253
|
+
text-anchor="middle"
|
|
254
|
+
>{@html params.labels[n]}</text>
|
|
255
|
+
{/if}
|
|
370
256
|
|
|
371
|
-
</script>
|
|
372
257
|
|
|
373
|
-
|
|
258
|
+
{#if params.labelsFactor !== 0}
|
|
259
|
+
{@const x = coords.start + (n + 0.85) * coords.width }
|
|
260
|
+
<text {x} y={coords.ltop} dx={0} dy={0} dominant-baseline="hanging"
|
|
261
|
+
fill={params.labelColor}
|
|
262
|
+
font-size="{params.fontSize}em"
|
|
263
|
+
text-anchor="middle"
|
|
264
|
+
>{@html getTickFactorLabel(params.labelsFactor)}</text>
|
|
265
|
+
{/if}
|
|
266
|
+
{/snippet}
|
|
267
|
+
|
|
268
|
+
<!-- snipped for group legend -->
|
|
269
|
+
{#snippet glgSnippet(params, coords)}
|
|
270
|
+
{@const elw = coords.elw + 2 * coords.elp}
|
|
271
|
+
{@const elp = coords.elp}
|
|
272
|
+
{@const elh = coords.elh}
|
|
273
|
+
{@const ely = coords.ely}
|
|
274
|
+
|
|
275
|
+
<svg x={coords.lgl} y={coords.lgt} height={coords.lgh} width={coords.lgw}>
|
|
276
|
+
<rect height="100%" width="100%" fill={params.faceColor} stroke={params.lineColor} stroke-width={params.lineWidth} />
|
|
277
|
+
|
|
278
|
+
{#each params.items as item, i}
|
|
279
|
+
<svg x={0} y={ely[i]} width={coords.lgw} height={elh[i]}>
|
|
280
|
+
|
|
281
|
+
<!-- line -->
|
|
282
|
+
{#if item.line}
|
|
283
|
+
<line
|
|
284
|
+
x1={elp}
|
|
285
|
+
x2={elw - elp}
|
|
286
|
+
y1={elh[i]/2}
|
|
287
|
+
y2={elh[i]/2}
|
|
288
|
+
stroke={item.line.lineColor}
|
|
289
|
+
stroke-width={item.line.lineWidth}
|
|
290
|
+
stroke-dasharray={lineStyles[item.line.lineType - 1]}
|
|
291
|
+
/>
|
|
292
|
+
{/if}
|
|
293
|
+
|
|
294
|
+
<!-- marker -->
|
|
295
|
+
{#if item.point}
|
|
296
|
+
<text
|
|
297
|
+
x={elw/2}
|
|
298
|
+
y={elh[i]/2}
|
|
299
|
+
dominant-baseline="middle"
|
|
300
|
+
text-anchor="middle"
|
|
301
|
+
fill={item.point.faceColor}
|
|
302
|
+
stroke-width={item.point.lineWidth}
|
|
303
|
+
stroke={item.point.lineColor}
|
|
304
|
+
font-size={fontSize}
|
|
305
|
+
>{MARKER_SYMBOLS[item.point.marker - 1]}</text>
|
|
306
|
+
{/if}
|
|
374
307
|
|
|
375
|
-
|
|
376
|
-
|
|
308
|
+
<!-- text -->
|
|
309
|
+
<text
|
|
310
|
+
class="legend-text"
|
|
311
|
+
xml:space="preserve"
|
|
312
|
+
x={elw + elp}
|
|
313
|
+
y={elh[i]/2}
|
|
314
|
+
dominant-baseline="middle"
|
|
315
|
+
text-anchor="start"
|
|
316
|
+
fill={Colors.LEGEND}
|
|
317
|
+
font-size="{params.fontSize}em"
|
|
318
|
+
> {@html item.label}</text>
|
|
377
319
|
|
|
320
|
+
</svg>
|
|
321
|
+
{/each}
|
|
322
|
+
</svg>
|
|
323
|
+
{/snippet}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
<div class="plot-container show-download-links-{downloadLinks}" class:plot-error={!isOk}>
|
|
327
|
+
|
|
328
|
+
<svg class="plot" bind:this={plotElement} bind:clientWidth={plotWidth} bind:clientHeight={plotHeight}
|
|
329
|
+
xmlns="http://www.w3.org/2000/svg" style="font-size:{fontSize}px">
|
|
378
330
|
<style>
|
|
379
331
|
.plot {
|
|
380
332
|
font-family: Arial, Helvetica, sans-serif;
|
|
@@ -383,82 +335,159 @@
|
|
|
383
335
|
.plot-labels text {
|
|
384
336
|
font-weight:bold;
|
|
385
337
|
text-anchor:middle;
|
|
338
|
+
user-select: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.series-points text {
|
|
342
|
+
/* dominant-baseline: central;
|
|
343
|
+
alignment-baseline: central */
|
|
386
344
|
}
|
|
387
345
|
|
|
388
346
|
.tick-labels text,
|
|
389
347
|
.tick-labels tspan,
|
|
348
|
+
.series-textlabels text,
|
|
349
|
+
.series-textlabels tspan,
|
|
390
350
|
.series-text text,
|
|
391
|
-
.series-text tspan
|
|
351
|
+
.series-text tspan {
|
|
352
|
+
dominant-baseline: middle;
|
|
353
|
+
cursor: default;
|
|
354
|
+
user-select: none;
|
|
355
|
+
-webkit-user-select: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
392
359
|
.series-points text,
|
|
393
360
|
.series-points tspan {
|
|
394
|
-
dominant-baseline: middle;
|
|
395
361
|
cursor: default;
|
|
396
362
|
user-select: none;
|
|
363
|
+
-webkit-user-select: none;
|
|
397
364
|
}
|
|
398
365
|
|
|
399
366
|
.series-text text,
|
|
400
367
|
.series-text tspan {
|
|
401
368
|
text-anchor: middle;
|
|
369
|
+
user-select: none;
|
|
370
|
+
-webkit-user-select: none;
|
|
402
371
|
}
|
|
403
372
|
|
|
404
373
|
.tick-labels text,
|
|
405
374
|
.tick-labels tspan {
|
|
406
|
-
|
|
375
|
+
user-select: none;
|
|
376
|
+
-webkit-user-select: none;
|
|
407
377
|
}
|
|
408
378
|
</style>
|
|
409
379
|
|
|
380
|
+
{#if isOk}
|
|
410
381
|
<g class="plot-labels">
|
|
411
|
-
<!-- y-axis label -->
|
|
412
|
-
{#if yLabel && yLabel !== ''}
|
|
413
|
-
<text x={0} y={(height + top) /2} dx={0} dy={0} dominant-baseline="top" transform={`rotate(-90, 10, ${height/2})`} style="font-size:1.1em;">{@html yLabel}</text>
|
|
414
|
-
{/if}
|
|
415
382
|
<!-- x-axis label -->
|
|
416
|
-
{#if
|
|
417
|
-
<text x={
|
|
383
|
+
{#if xaxis.show && xaxis.label && xaxis.label !== ''}
|
|
384
|
+
<text x={mx} y={height + top} dx={0} dy={0} dominant-baseline="middle" style="font-size:1.1em;">{@html xaxis.label}</text>
|
|
418
385
|
{/if}
|
|
386
|
+
|
|
387
|
+
<!-- y-axis label -->
|
|
388
|
+
{#if yaxis.show && yaxis.label && yaxis.label !== ''}
|
|
389
|
+
<text x={fontSize / 1.1 * yaxis.labelHeight} y={my} dx={0} dy={0} dominant-baseline="middle"
|
|
390
|
+
transform={`rotate(-90, ${fontSize * yaxis.labelHeight / 1.1}, ${my})`} style="font-size:1.1em;">{@html yaxis.label}</text>
|
|
391
|
+
{/if}
|
|
392
|
+
|
|
419
393
|
<!-- plot title -->
|
|
420
|
-
{#if
|
|
421
|
-
<text x={
|
|
394
|
+
{#if plotTitle && plotTitle !== ''}
|
|
395
|
+
<text x={mx} y={fontSize * titleHeight} dx={0} dy={0} dominant-baseline="middle"
|
|
396
|
+
style="font-size:1.2em;">{@html plotTitle}</text>
|
|
422
397
|
{/if}
|
|
423
398
|
</g>
|
|
399
|
+
{/if}
|
|
424
400
|
|
|
425
|
-
|
|
426
|
-
|
|
401
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
402
|
+
<svg x={left} y={top} width={width} height={height} preserveAspectRatio="none" class="axes">
|
|
427
403
|
|
|
428
404
|
<!-- define clipping path -->
|
|
429
405
|
<defs>
|
|
430
406
|
<clipPath id={clipPathID}>
|
|
431
|
-
<rect
|
|
407
|
+
<rect x={cpx[0]} y={cpy[1]} width={cpx[1]-cpx[0]} height={cpy[0]-cpy[1]} />
|
|
432
408
|
</clipPath>
|
|
433
409
|
</defs>
|
|
434
410
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
411
|
+
{#if isOk }
|
|
412
|
+
|
|
413
|
+
<!-- xaxis -->
|
|
414
|
+
{#if xaxis.show && xaxisCoords}
|
|
415
|
+
<g class="axis x-axis">
|
|
416
|
+
{@render axisSnippet(xaxisCoords, xaxis)}
|
|
417
|
+
</g>
|
|
418
|
+
{/if}
|
|
419
|
+
|
|
420
|
+
<!-- yaxis -->
|
|
421
|
+
{#if yaxis.show && yaxisCoords}
|
|
422
|
+
<g class="axis y-axis">
|
|
423
|
+
{@render axisSnippet(yaxisCoords, yaxis)}
|
|
424
|
+
</g>
|
|
425
|
+
{/if}
|
|
426
|
+
|
|
427
|
+
<!-- box -->
|
|
428
|
+
<g class="axes-box">
|
|
429
|
+
{#if box.show}
|
|
430
|
+
<rect stroke={box.lineColor} stroke-width="{box.lineWidth}px" fill="transparent"
|
|
431
|
+
x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
|
|
432
|
+
id="axes-box"
|
|
433
|
+
onkeydown={handleClick}
|
|
434
|
+
onclick={handleClick}
|
|
435
|
+
onmousemove={handleMove}
|
|
436
|
+
onmousedown={handleDown}
|
|
437
|
+
onmouseup={handleUp}
|
|
438
|
+
/>
|
|
439
|
+
{:else}
|
|
440
|
+
<rect stroke="transparent" fill="transparent"
|
|
441
|
+
x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
|
|
442
|
+
id="axes-box"
|
|
443
|
+
onkeydown={handleClick}
|
|
444
|
+
onclick={handleClick}
|
|
445
|
+
onmousemove={handleMove}
|
|
446
|
+
onmousedown={handleDown}
|
|
447
|
+
onmouseup={handleUp}
|
|
448
|
+
/>
|
|
449
|
+
{/if}
|
|
450
|
+
</g>
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
<!-- main plot content -->
|
|
454
|
+
<g class="axes-content" clip-path="url(#{clipPathID})">
|
|
455
|
+
{@render children?.()}
|
|
456
|
+
</g>
|
|
457
|
+
|
|
458
|
+
<!-- colormap legend -->
|
|
459
|
+
{#if clgCoords }
|
|
460
|
+
<g class="colormap-legend" style="stroke:0;stroke-width:0px;">
|
|
461
|
+
{@render clgSnippet(clgParams, clgCoords)}
|
|
462
|
+
</g>
|
|
463
|
+
{/if}
|
|
464
|
+
|
|
465
|
+
<!-- group legend -->
|
|
466
|
+
{#if glgCoords }
|
|
467
|
+
<g class="group-legend" style="stroke:0;stroke-width:0px;">
|
|
468
|
+
{@render glgSnippet(glg, glgCoords)}
|
|
469
|
+
</g>
|
|
470
|
+
{/if}
|
|
438
471
|
|
|
439
|
-
<!-- main plot content -->
|
|
440
|
-
<g class="axes-content" clip-path="url(#{clipPathID})">
|
|
441
|
-
<slot></slot>
|
|
442
|
-
</g>
|
|
443
472
|
|
|
444
|
-
|
|
445
|
-
|
|
473
|
+
|
|
474
|
+
{/if}
|
|
475
|
+
|
|
446
476
|
</svg>
|
|
447
477
|
</svg>
|
|
448
478
|
|
|
449
|
-
{#if
|
|
450
|
-
<p class="
|
|
479
|
+
{#if !isOk}
|
|
480
|
+
<p class="message-error">
|
|
451
481
|
Axes component was not properly initialized. <br />
|
|
452
482
|
Check that you defined axes limits and margins correctly.
|
|
453
483
|
</p>
|
|
454
484
|
{:else}
|
|
455
485
|
<div class="download-links">
|
|
456
|
-
<button
|
|
457
|
-
<button
|
|
458
|
-
<button
|
|
486
|
+
<button onclick={handleClickSVG} onkeydown={handleClickSVG}>⇩ svg</button>
|
|
487
|
+
<button onclick={handleClickPNG} onkeydown={handleClickPNG}>⇩ png</button>
|
|
488
|
+
<button onclick={handleClickCopy} onkeydown={handleClickCopy}>⧉ copy</button>
|
|
459
489
|
</div>
|
|
460
490
|
{/if}
|
|
461
|
-
|
|
462
491
|
</div>
|
|
463
492
|
|
|
464
493
|
<style>
|
|
@@ -476,7 +505,6 @@
|
|
|
476
505
|
|
|
477
506
|
.download-links {
|
|
478
507
|
box-sizing: border-box;
|
|
479
|
-
bottom: right 0.5s ease;
|
|
480
508
|
position: absolute;
|
|
481
509
|
font-size: 0.8em;
|
|
482
510
|
bottom: -50%;
|
|
@@ -510,6 +538,14 @@
|
|
|
510
538
|
color: #fafafa;
|
|
511
539
|
}
|
|
512
540
|
|
|
541
|
+
.download-links > :global(button.error){
|
|
542
|
+
background: #ee4433;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.download-links > :global(button.copied){
|
|
546
|
+
background: #4488ee;
|
|
547
|
+
}
|
|
548
|
+
|
|
513
549
|
|
|
514
550
|
/* Plot container */
|
|
515
551
|
.plot-container {
|
|
@@ -526,13 +562,13 @@
|
|
|
526
562
|
background: #fff;
|
|
527
563
|
}
|
|
528
564
|
|
|
529
|
-
.
|
|
565
|
+
.plot-error {
|
|
530
566
|
display: flex;
|
|
531
567
|
justify-content: center;
|
|
532
568
|
align-items: center;
|
|
533
569
|
}
|
|
534
570
|
|
|
535
|
-
.
|
|
571
|
+
.message-error {
|
|
536
572
|
font-size: 1.2em;
|
|
537
573
|
color: crimson;
|
|
538
574
|
padding: 1em;
|