svelte-plots-basic 2.5.8 → 3.0.1
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 +533 -448
- package/package.json +5 -2
- package/src/2d/Area.svelte +56 -48
- package/src/2d/Axes.svelte +409 -390
- 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 -99
- package/src/2d/YAxis.svelte +47 -94
- 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 -639
package/src/2d/Axes.svelte
CHANGED
|
@@ -1,397 +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
|
-
|
|
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 } from '../methods.js';
|
|
37
|
+
|
|
38
|
+
import AxisLines from './AxisLines.svelte';
|
|
39
|
+
import AxisTickLabels from './AxisTickLabels.svelte';
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
title,
|
|
44
|
+
limX = [0, 1],
|
|
45
|
+
limY = [0, 1],
|
|
46
|
+
margins = [1.0, 1.1, 0.55, 0.60],
|
|
47
|
+
downloadLinks = 'none',
|
|
48
|
+
fileName = 'plot',
|
|
49
|
+
pngWidth = 8,
|
|
50
|
+
pngHeight = 8,
|
|
51
|
+
pngRes = 300,
|
|
52
|
+
clipboardWidth = 1200,
|
|
53
|
+
clipboardHeight = 800,
|
|
54
|
+
onclick = null,
|
|
55
|
+
onmousemove = null,
|
|
56
|
+
onmousedown = null,
|
|
57
|
+
onmouseup = null,
|
|
58
|
+
children
|
|
59
|
+
} = $props();
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/* handler for mouse events */
|
|
63
|
+
function handleMouse(e, f) {
|
|
64
|
+
if (f && e.target.id == "axes-box") {
|
|
65
|
+
const x = invTransformCoords([e.offsetX - fontSize * 1.5], tX)[0];
|
|
66
|
+
const y = invTransformCoords([e.offsetY], tY)[0];
|
|
67
|
+
f(x, y);
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'small': 5,
|
|
56
|
-
'medium': 8,
|
|
57
|
-
'large': 12,
|
|
58
|
-
'xlarge': 15
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// number of y-ticks along each axis
|
|
62
|
-
const YTICK_NUM = {
|
|
63
|
-
'small': 6,
|
|
64
|
-
'medium': 10,
|
|
65
|
-
'large': 14,
|
|
66
|
-
'xlarge': 18
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// size of ticks
|
|
70
|
-
const TICK_SIZE = {
|
|
71
|
-
'small': 4,
|
|
72
|
-
'medium': 6,
|
|
73
|
-
'large': 8,
|
|
74
|
-
'xlarge': 10
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// font size for plot element
|
|
78
|
-
const LEGEND_FONT_SIZE = {
|
|
79
|
-
"small": 11,
|
|
80
|
-
"medium": 14,
|
|
81
|
-
"large": 16,
|
|
82
|
-
"xlarge": 18
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// font size for legend items in pixels
|
|
86
|
-
const PLOT_FONT_SIZE = {
|
|
87
|
-
"small": 12,
|
|
88
|
-
"medium": 15,
|
|
89
|
-
"large": 19,
|
|
90
|
-
"xlarge": 21
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// margin between plot series elements and data labels
|
|
94
|
-
const LABELS_MARGIN = {
|
|
95
|
-
'small': 11,
|
|
96
|
-
'medium': 15,
|
|
97
|
-
'large': 17,
|
|
98
|
-
'xlarge': 19
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// line styles for different scales and types
|
|
102
|
-
const LINE_STYLES = {
|
|
103
|
-
small: ['0', '3,3', '1,1', '3,1'],
|
|
104
|
-
medium: ['0', '5,5', '2,2', '5,2'],
|
|
105
|
-
large: ['0', '7,7', '3,3', '7,3'],
|
|
106
|
-
xlarge: ['0', '9,9', '4,4', '9,3'],
|
|
71
|
+
function handleDown(e) {
|
|
72
|
+
handleMouse(e, onmousedown)
|
|
107
73
|
}
|
|
108
74
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// constant to make clip path ID unique
|
|
113
|
-
const clipPathID = 'plottingArea' + Math.round(Math.random() * 10000);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
/*****************************************/
|
|
117
|
-
/* Variable parameters for internal use */
|
|
118
|
-
/*****************************************/
|
|
119
|
-
|
|
120
|
-
/* parameters for internal use inside the component */
|
|
121
|
-
let plotElement;
|
|
122
|
-
let width = 100, height = 100;
|
|
123
|
-
let left = 0, bottom = 0, top = 0;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/*****************************************/
|
|
127
|
-
/* Helper functions */
|
|
128
|
-
/*****************************************/
|
|
75
|
+
function handleUp(e) {
|
|
76
|
+
handleMouse(e, onmouseup)
|
|
77
|
+
}
|
|
129
78
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
* @param {string} eventName - name of the event.
|
|
133
|
-
* @param {HTMLDOMElement} el - DOM element the click was registered for.
|
|
134
|
-
*
|
|
135
|
-
*/
|
|
136
|
-
function dispatchClickEvent(eventName, el) {
|
|
137
|
-
const id = Array.prototype.indexOf.call(el.parentNode.children, el)
|
|
138
|
-
dispatch(eventName, {seriesTitle: el.parentNode.getAttribute('title'), elementID: id});
|
|
79
|
+
function handleMove(e) {
|
|
80
|
+
handleMouse(e, onmousemove)
|
|
139
81
|
}
|
|
140
82
|
|
|
141
|
-
/**
|
|
142
|
-
* Handler (router) for axes or series click events.
|
|
143
|
-
*
|
|
144
|
-
* @param {event} e - event object.
|
|
145
|
-
*
|
|
146
|
-
* @description
|
|
147
|
-
* Checks which element the click was made on and dispatch a corresponding event.
|
|
148
|
-
*
|
|
149
|
-
*/
|
|
150
83
|
function handleClick(e) {
|
|
151
|
-
|
|
152
|
-
// click on scatter plot markers
|
|
153
|
-
if (e.target.tagName === 'text' && e.target.parentNode.classList.contains('series-points')) {
|
|
154
|
-
dispatchClickEvent('markerclick', e.target);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// click on bar plot bars
|
|
159
|
-
if (e.target.tagName === 'rect' && e.target.parentNode.classList.contains('series-bar')) {
|
|
160
|
-
dispatchClickEvent('barclick', e.target);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// click outside any plot element
|
|
165
|
-
dispatch('axesclick');
|
|
84
|
+
handleMouse(e, onclick)
|
|
166
85
|
}
|
|
167
86
|
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Handler (router) for click event on copy plot button.
|
|
171
|
-
*
|
|
172
|
-
* @description
|
|
173
|
-
* Copy plot to clipboard as PNG image.
|
|
174
|
-
*
|
|
175
|
-
*/
|
|
87
|
+
/* Copy plot to clipboard as PNG image. */
|
|
176
88
|
async function handleClickCopy(e) {
|
|
177
|
-
|
|
178
|
-
new ClipboardItem({ 'image/png': createPngBlob(plotElement, clipboardWidth, clipboardHeight) })
|
|
179
|
-
]).catch(function (error) { console.error(error); });
|
|
180
|
-
|
|
181
|
-
const content = e.target.textContent;
|
|
182
|
-
e.target.classList.add('copied');
|
|
183
|
-
e.target.textContent = '✓ done';
|
|
184
|
-
|
|
185
|
-
// Remove the class after 1 second and revert the text
|
|
186
|
-
setTimeout(() => {
|
|
187
|
-
e.target.classList.remove('copied');
|
|
188
|
-
e.target.textContent = content;
|
|
189
|
-
}, 500);
|
|
89
|
+
copyToClipboard(e.target, plotElement, clipboardWidth, clipboardHeight);
|
|
190
90
|
}
|
|
191
91
|
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Handler (router) for click event to download SVG image.
|
|
195
|
-
*
|
|
196
|
-
* @description
|
|
197
|
-
* Copy plot to clipboard as SVG image.
|
|
198
|
-
*
|
|
199
|
-
*/
|
|
92
|
+
/* Download plot as SVG image. */
|
|
200
93
|
function handleClickSVG() {
|
|
201
94
|
downloadSVG(plotElement, fileName);
|
|
202
95
|
}
|
|
203
96
|
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Handler (router) for click event to download PNG image.
|
|
207
|
-
*
|
|
208
|
-
* @description
|
|
209
|
-
* Copy plot to clipboard as PNG image.
|
|
210
|
-
*
|
|
211
|
-
*/
|
|
97
|
+
/* Download plot as PNG image. */
|
|
212
98
|
function handleClickPNG() {
|
|
213
99
|
downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes);
|
|
214
100
|
}
|
|
215
101
|
|
|
216
|
-
/**
|
|
217
|
-
* Generic function to transform x or y-values from plot coordinates to screen (SVG) coordinates.
|
|
218
|
-
*
|
|
219
|
-
* @param {Array|Vector} v - vector with coordinates (or objects size) in original plot coordinates.
|
|
220
|
-
* @param {Object} tA - array with scaling and translation factors.
|
|
221
|
-
*
|
|
222
|
-
* @returns {Vector} vector with rescaled values
|
|
223
|
-
*
|
|
224
|
-
*/
|
|
225
|
-
const transform = function(v, tA) {
|
|
226
|
-
|
|
227
|
-
if (!$isOk || v === undefined || v === null) return undefined;
|
|
228
|
-
if (Array.isArray(v)) v = vector(v);
|
|
229
|
-
if (!isvector(v) || v.length < 1) return undefined;
|
|
230
|
-
|
|
231
|
-
return v.apply(a => roundCoords((a - tA[1]) * tA[0] + tA[2])).v;
|
|
232
|
-
}
|
|
233
102
|
|
|
103
|
+
// unique ID for clip path
|
|
104
|
+
const clipPathID = 'plottingArea' + Math.round(Math.random() * 10000);
|
|
234
105
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const tY = writable({'coords': [1, 0, 0], 'objects': [1, 0, 0]}); // scaling and translation factors for y-dimension
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
/*****************************************/
|
|
268
|
-
/* Axes context */
|
|
269
|
-
/*****************************************/
|
|
270
|
-
|
|
271
|
-
let context = {
|
|
272
|
-
|
|
273
|
-
// methods
|
|
274
|
-
transform: transform,
|
|
275
|
-
invTransform: invTransform,
|
|
276
|
-
|
|
277
|
-
// variables
|
|
278
|
-
scale: scale,
|
|
279
|
-
xscale: xscale,
|
|
280
|
-
yscale: yscale,
|
|
281
|
-
isOk: isOk,
|
|
282
|
-
xLim: xLim,
|
|
283
|
-
yLim: yLim,
|
|
284
|
-
tX: tX,
|
|
285
|
-
tY: tY,
|
|
286
|
-
xLabel: xLabel,
|
|
287
|
-
yLabel: yLabel,
|
|
288
|
-
|
|
289
|
-
// constants
|
|
290
|
-
LINE_STYLES: LINE_STYLES,
|
|
291
|
-
LABELS_MARGIN: LABELS_MARGIN,
|
|
292
|
-
XTICK_NUM: XTICK_NUM,
|
|
293
|
-
YTICK_NUM: YTICK_NUM,
|
|
294
|
-
TICK_SIZE: TICK_SIZE,
|
|
295
|
-
MARKER_SYMBOLS: MARKER_SYMBOLS,
|
|
296
|
-
LEGEND_FONT_SIZE: LEGEND_FONT_SIZE
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
setContext('axes', context);
|
|
300
|
-
|
|
301
|
-
/*****************************************/
|
|
302
|
-
/* Reactive updates of the parameters */
|
|
303
|
-
/*****************************************/
|
|
304
|
-
|
|
305
|
-
// computes real margins in pixels based on current scale
|
|
306
|
-
$: pxMargins = margins.map(v => v * AXES_MARGIN_FACTORS[$scale]);
|
|
307
|
-
|
|
308
|
-
// update limits if necessary
|
|
309
|
-
$: xLim.update(v => limX);
|
|
310
|
-
$: yLim.update(v => limY);
|
|
311
|
-
|
|
312
|
-
// computes status which tells that axes limits look fine and it is safe to draw
|
|
313
|
-
// the status is based on the axis limits validity
|
|
314
|
-
$: isOk.update(v =>
|
|
315
|
-
Array.isArray($yLim) &&
|
|
316
|
-
Array.isArray($xLim) &&
|
|
317
|
-
$xLim.length === 2 &&
|
|
318
|
-
$yLim.length === 2 &&
|
|
319
|
-
!$yLim.some(v => v === undefined) &&
|
|
320
|
-
!$xLim.some(v => v === undefined) &&
|
|
321
|
-
!$yLim.some(v => isNaN(v)) &&
|
|
322
|
-
!$xLim.some(v => isNaN(v)) &&
|
|
323
|
-
$xLim[1] !== $xLim[0] &&
|
|
324
|
-
$yLim[1] !== $yLim[0] &&
|
|
106
|
+
// pointers to plot DOM element and its width and height
|
|
107
|
+
let plotElement = $state(0);
|
|
108
|
+
let plotWidth = $state(0);
|
|
109
|
+
let plotHeight = $state(0);
|
|
110
|
+
|
|
111
|
+
// scales and related parameters
|
|
112
|
+
const scales = $derived({'plot': getScale(plotWidth, plotHeight), 'x': getAxisScale(plotWidth), 'y': getAxisScale(plotHeight)});
|
|
113
|
+
const pxMargins = $derived(margins.map(v => v * AXES_MARGIN_FACTORS[scales.plot]));
|
|
114
|
+
const fontSize = $derived(PLOT_FONT_SIZE[scales.plot]);
|
|
115
|
+
const tickSize = $derived(TICK_SIZE[scales.plot]);
|
|
116
|
+
const lineStyles = $derived(LINE_STYLES[scales.plot]);
|
|
117
|
+
|
|
118
|
+
// left and bottom margins as well as width and height of the axes area (including margins)
|
|
119
|
+
const bottom = $derived(xaxis.show && xaxis.label !== '' ? fontSize * 1.5 : 0);
|
|
120
|
+
const left = $derived(yaxis.show && yaxis.label !== '' ? fontSize * 1.5 : 0);
|
|
121
|
+
const top = $derived(title && title !== '' ? fontSize * 1.5 : 0);
|
|
122
|
+
const width = $derived(plotWidth > left ? plotWidth - left : 0);
|
|
123
|
+
const height = $derived(plotHeight > (bottom + top) ? plotHeight - bottom - top : 0);
|
|
124
|
+
|
|
125
|
+
// size of the axes area without margins
|
|
126
|
+
const axisHeight = $derived(height - pxMargins[0] - pxMargins[2]);
|
|
127
|
+
const axisWidth = $derived(width - pxMargins[1] - pxMargins[3]);
|
|
128
|
+
|
|
129
|
+
// plot status
|
|
130
|
+
const isOk = $derived(
|
|
131
|
+
checkArray(limX, 2) &&
|
|
132
|
+
checkArray(limY, 2) &&
|
|
133
|
+
limX[0] < limX[1] &&
|
|
134
|
+
limY[0] < limY[1] &&
|
|
325
135
|
width > (pxMargins[1] + pxMargins[3]) &&
|
|
326
136
|
height > (pxMargins[0] + pxMargins[2])
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// transformation matrix for x
|
|
140
|
+
// fw: a - tA[1]) * tA[0] + tA[2]
|
|
141
|
+
// in: a - tA[2]) / tA[0] + tA[1]
|
|
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]
|
|
336
146
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
'objects': [ (height - pxMargins[0] - pxMargins[2]) / ($yLim[1] - $yLim[0]), 0, 0]
|
|
345
|
-
}));
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// computes coordinates for clip path box
|
|
350
|
-
$: cpx = $isOk ? transform($xLim, $tX.coords) : [0, 1];
|
|
351
|
-
$: cpy = $isOk ? transform($yLim, $tY.coords) : [1, 0];
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
/*****************************************/
|
|
355
|
-
/* Events observers */
|
|
356
|
-
/*****************************************/
|
|
357
|
-
|
|
358
|
-
// observer for the plot area size — to update the scale
|
|
359
|
-
const ro = new ResizeObserver(entries => {
|
|
360
|
-
for (let entry of entries) {
|
|
361
|
-
const pcr = plotElement.getBoundingClientRect();
|
|
362
|
-
const scl = getScale(pcr.width, pcr.height);
|
|
363
|
-
const xscl = getAxisScale(pcr.width);
|
|
364
|
-
const yscl = getAxisScale(pcr.height);
|
|
365
|
-
scale.update(x => scl);
|
|
366
|
-
xscale.update(x => xscl);
|
|
367
|
-
yscale.update(x => yscl);
|
|
368
|
-
|
|
369
|
-
const m = PLOT_FONT_SIZE[scl] * 1.5;
|
|
370
|
-
left = xLabel && xLabel !== '' ? m : 0;
|
|
371
|
-
bottom = yLabel && yLabel !== '' ? m : 0;
|
|
372
|
-
top = title && title !== '' ? m : 0;
|
|
373
|
-
width = pcr.width > left ? pcr.width - left : 0;
|
|
374
|
-
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]
|
|
375
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
|
|
376
199
|
});
|
|
200
|
+
</script>
|
|
377
201
|
|
|
378
|
-
|
|
202
|
+
<!-- snippet for axis -->
|
|
203
|
+
{#snippet axisSnippet(coords, params)}
|
|
379
204
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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}
|
|
383
229
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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}
|
|
387
256
|
|
|
388
|
-
</script>
|
|
389
257
|
|
|
390
|
-
|
|
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}
|
|
391
293
|
|
|
392
|
-
|
|
393
|
-
|
|
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}
|
|
307
|
+
|
|
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>
|
|
319
|
+
|
|
320
|
+
</svg>
|
|
321
|
+
{/each}
|
|
322
|
+
</svg>
|
|
323
|
+
{/snippet}
|
|
394
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">
|
|
395
330
|
<style>
|
|
396
331
|
.plot {
|
|
397
332
|
font-family: Arial, Helvetica, sans-serif;
|
|
@@ -400,82 +335,159 @@
|
|
|
400
335
|
.plot-labels text {
|
|
401
336
|
font-weight:bold;
|
|
402
337
|
text-anchor:middle;
|
|
338
|
+
user-select: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.series-points text {
|
|
342
|
+
/* dominant-baseline: central;
|
|
343
|
+
alignment-baseline: central */
|
|
403
344
|
}
|
|
404
345
|
|
|
405
346
|
.tick-labels text,
|
|
406
347
|
.tick-labels tspan,
|
|
348
|
+
.series-textlabels text,
|
|
349
|
+
.series-textlabels tspan,
|
|
407
350
|
.series-text text,
|
|
408
|
-
.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
|
+
|
|
409
359
|
.series-points text,
|
|
410
360
|
.series-points tspan {
|
|
411
|
-
dominant-baseline: middle;
|
|
412
361
|
cursor: default;
|
|
413
362
|
user-select: none;
|
|
363
|
+
-webkit-user-select: none;
|
|
414
364
|
}
|
|
415
365
|
|
|
416
366
|
.series-text text,
|
|
417
367
|
.series-text tspan {
|
|
418
368
|
text-anchor: middle;
|
|
369
|
+
user-select: none;
|
|
370
|
+
-webkit-user-select: none;
|
|
419
371
|
}
|
|
420
372
|
|
|
421
373
|
.tick-labels text,
|
|
422
374
|
.tick-labels tspan {
|
|
423
|
-
|
|
375
|
+
user-select: none;
|
|
376
|
+
-webkit-user-select: none;
|
|
424
377
|
}
|
|
425
378
|
</style>
|
|
426
379
|
|
|
380
|
+
{#if isOk}
|
|
427
381
|
<g class="plot-labels">
|
|
428
|
-
<!-- y-axis label -->
|
|
429
|
-
{#if yLabel && yLabel !== ''}
|
|
430
|
-
<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>
|
|
431
|
-
{/if}
|
|
432
382
|
<!-- x-axis label -->
|
|
433
|
-
{#if
|
|
434
|
-
<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>
|
|
435
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
|
+
|
|
436
393
|
<!-- plot title -->
|
|
437
|
-
{#if
|
|
438
|
-
<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>
|
|
439
397
|
{/if}
|
|
440
398
|
</g>
|
|
399
|
+
{/if}
|
|
441
400
|
|
|
442
|
-
|
|
443
|
-
|
|
401
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
402
|
+
<svg x={left} y={top} width={width} height={height} preserveAspectRatio="none" class="axes">
|
|
444
403
|
|
|
445
404
|
<!-- define clipping path -->
|
|
446
405
|
<defs>
|
|
447
406
|
<clipPath id={clipPathID}>
|
|
448
|
-
<rect
|
|
407
|
+
<rect x={cpx[0]} y={cpy[1]} width={cpx[1]-cpx[0]} height={cpy[0]-cpy[1]} />
|
|
449
408
|
</clipPath>
|
|
450
409
|
</defs>
|
|
451
410
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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}
|
|
455
471
|
|
|
456
|
-
<!-- main plot content -->
|
|
457
|
-
<g class="axes-content" clip-path="url(#{clipPathID})">
|
|
458
|
-
<slot></slot>
|
|
459
|
-
</g>
|
|
460
472
|
|
|
461
|
-
|
|
462
|
-
|
|
473
|
+
|
|
474
|
+
{/if}
|
|
475
|
+
|
|
463
476
|
</svg>
|
|
464
477
|
</svg>
|
|
465
478
|
|
|
466
|
-
{#if
|
|
467
|
-
<p class="
|
|
479
|
+
{#if !isOk}
|
|
480
|
+
<p class="message-error">
|
|
468
481
|
Axes component was not properly initialized. <br />
|
|
469
482
|
Check that you defined axes limits and margins correctly.
|
|
470
483
|
</p>
|
|
471
484
|
{:else}
|
|
472
485
|
<div class="download-links">
|
|
473
|
-
<button
|
|
474
|
-
<button
|
|
475
|
-
<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>
|
|
476
489
|
</div>
|
|
477
490
|
{/if}
|
|
478
|
-
|
|
479
491
|
</div>
|
|
480
492
|
|
|
481
493
|
<style>
|
|
@@ -493,7 +505,6 @@
|
|
|
493
505
|
|
|
494
506
|
.download-links {
|
|
495
507
|
box-sizing: border-box;
|
|
496
|
-
bottom: right 0.5s ease;
|
|
497
508
|
position: absolute;
|
|
498
509
|
font-size: 0.8em;
|
|
499
510
|
bottom: -50%;
|
|
@@ -527,6 +538,14 @@
|
|
|
527
538
|
color: #fafafa;
|
|
528
539
|
}
|
|
529
540
|
|
|
541
|
+
.download-links > :global(button.error){
|
|
542
|
+
background: #ee4433;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.download-links > :global(button.copied){
|
|
546
|
+
background: #4488ee;
|
|
547
|
+
}
|
|
548
|
+
|
|
530
549
|
|
|
531
550
|
/* Plot container */
|
|
532
551
|
.plot-container {
|
|
@@ -543,13 +562,13 @@
|
|
|
543
562
|
background: #fff;
|
|
544
563
|
}
|
|
545
564
|
|
|
546
|
-
.
|
|
565
|
+
.plot-error {
|
|
547
566
|
display: flex;
|
|
548
567
|
justify-content: center;
|
|
549
568
|
align-items: center;
|
|
550
569
|
}
|
|
551
570
|
|
|
552
|
-
.
|
|
571
|
+
.message-error {
|
|
553
572
|
font-size: 1.2em;
|
|
554
573
|
color: crimson;
|
|
555
574
|
padding: 1em;
|