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.
@@ -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
- * Axes *
4
- * -------------------- *
5
- * root item for any plot *
6
- *****************************************************/
7
-
8
- import { setContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
9
- import { writable } from 'svelte/store';
10
- import { isvector, vector, Vector } from 'mdatools/arrays';
11
- import { roundCoords, getScale, downloadSVG, downloadPNG, createPngBlob, getAxisScale } from '../Utils.js';
12
-
13
-
14
- /*****************************************/
15
- /* Input parameters */
16
- /*****************************************/
17
-
18
- export let limX; // array with limits for x-axis (in plot units) [min, max]
19
- export let limY; // array with limits for y-axis (in plot units) [min, max]
20
- export let title = ''; // title of the plot
21
- export let xLabel = ''; // label for x-axis
22
- export let yLabel = ''; // label for y-axis
23
- export let margins = [1.0, 0.75, 0.5, 0.5]; // margins [bottom, left, top, right] )
24
-
25
-
26
- /***********************************************/
27
- /* Input parameters for downloading and copy */
28
- /***********************************************/
29
- export let downloadLinks = 'none'; // how to show download links panel ('none', 'hover', 'fixed')
30
- export let fileName = 'plot'; // file name for download (without extension)
31
- export let pngWidth = 8; // width of PNG image in cm
32
- export let pngHeight = 8; // height of PNG image in cm
33
- export let pngRes = 300; // resolution of PNG image (pixels per inch)
34
- export let clipboardWidth = 1200; // width of PNG image to copy to clipboard in pixels
35
- export let clipboardHeight = 800; // height of PNG image to copy to clipboard in pixels
36
-
37
-
38
- /*****************************************/
39
- /* Constants */
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
- // number of x-ticks along each axis
54
- const XTICK_NUM = {
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
- // marker symbols
110
- const MARKER_SYMBOLS = ["●", "◼", "▲", "▼", "⬥", "+", "✳", "✕"];
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
- * Dispatcher for click events.
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
- navigator.clipboard.write([
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
- * Generic function to transform x or y-values from screen (SVG) coordinates to plot coordinates.
237
- *
238
- * @param {Array|Vector} v - vector with coordinates (or objects size) in screen (SVG) coordinates.
239
- * @param {Object} tA - array with scaling and translation factors.
240
- *
241
- * @returns {Vector} vector plot coordinates.
242
- *
243
- */
244
- const invTransform = function(v, tA) {
245
-
246
- if (!$isOk || v === undefined || v === null) return undefined;
247
- if (Array.isArray(v)) v = vector(v);
248
- if (!isvector(v) || v.length < 1) return undefined;
249
-
250
- return v.apply(a => (a - tA[2]) / tA[0] + tA[1]).v;
251
- }
252
-
253
- /*****************************************/
254
- /* Storage to share with children */
255
- /*****************************************/
256
-
257
- const xscale = writable('medium'); // scale factor for x-axis (influences number of ticks)
258
- const yscale = writable('medium'); // scale factor for y-axis (influences number of ticks)
259
- const scale = writable('medium'); // scale factor (how big the shown plot is)
260
- const isOk = writable(false); // are axes ready for drawing
261
- const xLim = writable(limX); // validated values for x-axis limits
262
- const yLim = writable(limY); // validated values for y-axis limits
263
- const tX = writable({'coords': [1, 0, 0], 'objects': [1, 0, 0]}); // scaling and translation factors for x-dimension
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
- // update transformation array for x-coordinates
330
- $: {
331
- if ($isOk) {
332
- tX.update(v => ({
333
- 'coords': [ (width - pxMargins[1] - pxMargins[3]) / ($xLim[1] - $xLim[0]), $xLim[0], pxMargins[1]],
334
- 'objects': [ (width - pxMargins[1] - pxMargins[3]) / ($xLim[1] - $xLim[0]), 0, 0]
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
- // update transformation array for y-coordinates
340
- $: {
341
- if ($isOk) {
342
- tY.update(v => ({
343
- 'coords': [-(height - pxMargins[0] - pxMargins[2]) / ($yLim[1] - $yLim[0]), $yLim[1], pxMargins[2]],
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
- $: fontSize = PLOT_FONT_SIZE[$scale];
202
+ <!-- snippet for axis -->
203
+ {#snippet axisSnippet(coords, params)}
379
204
 
380
- onMount(() => {
381
- ro.observe(plotElement);
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
- onDestroy(() => {
385
- ro.unobserve(plotElement);
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
- <div class="plot-container show-download-links-{downloadLinks}" class:plot_error={!$isOk}>
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
- <svg class="plot" bind:this={plotElement} xmlns="http://www.w3.org/2000/svg"
393
- style={`font-size:${fontSize}px`}>
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
- font-size:1em;
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 xLabel && xLabel !== ''}
434
- <text x={left + width/2 } y={height + top} dx={0} dy={5} dominant-baseline="middle" style="font-size:1.1em;">{@html xLabel}</text>
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 title && title !== ''}
438
- <text x={left + width/2 } y={0} dx={0} dy={10} dominant-baseline="hanging" style="font-size:1.2em;">{@html title}</text>
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
- <svg x={left} y={top} width={width} height={height} on:click={handleClick}
443
- on:keydown={handleClick} preserveAspectRatio="none" class="axes">
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 style="pointer-events:none" x={cpx[0]} y={cpy[1]} width={cpx[1]-cpx[0]} height={cpy[0]-cpy[1]} />
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
- <!-- axis and box -->
453
- <slot name="xaxis"></slot>
454
- <slot name="yaxis"></slot>
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
- <!-- axis and box -->
462
- <slot name="box"></slot>
473
+
474
+ {/if}
475
+
463
476
  </svg>
464
477
  </svg>
465
478
 
466
- {#if !$isOk}
467
- <p class="message_error">
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 on:click={handleClickSVG} on:keydown={handleClickSVG}>⇩ svg</button>
474
- <button on:click={handleClickPNG} on:keydown={handleClickPNG}>⇩ png</button>
475
- <button on:click={handleClickCopy} on:keydown={handleClickCopy}>⧉ copy</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
- .plot_error {
565
+ .plot-error {
547
566
  display: flex;
548
567
  justify-content: center;
549
568
  align-items: center;
550
569
  }
551
570
 
552
- .message_error {
571
+ .message-error {
553
572
  font-size: 1.2em;
554
573
  color: crimson;
555
574
  padding: 1em;