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.
@@ -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
- * 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 } 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,
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
- // number of ticks along each axis
54
- const TICK_NUM = {
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
- // marker symbols
102
- const MARKER_SYMBOLS = ["●", "◼", "▲", "▼", "⬥", "+", "✳", "✕"];
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
- * Dispatcher for click events.
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
- navigator.clipboard.write([
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
- /* Storage to share with children */
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
- setContext('axes', context);
287
-
288
- /*****************************************/
289
- /* Reactive updates of the parameters */
290
- /*****************************************/
291
-
292
- // computes real margins in pixels based on current scale
293
- $: pxMargins = margins.map(v => v * AXES_MARGIN_FACTORS[$scale]);
294
-
295
- // update limits if necessary
296
- $: xLim.update(v => limX);
297
- $: yLim.update(v => limY);
298
-
299
- // computes status which tells that axes limits look fine and it is safe to draw
300
- // the status is based on the axis limits validity
301
- $: isOk.update(v =>
302
- Array.isArray($yLim) &&
303
- Array.isArray($xLim) &&
304
- $xLim.length === 2 &&
305
- $yLim.length === 2 &&
306
- !$yLim.some(v => v === undefined) &&
307
- !$xLim.some(v => v === undefined) &&
308
- !$yLim.some(v => isNaN(v)) &&
309
- !$xLim.some(v => isNaN(v)) &&
310
- $xLim[1] !== $xLim[0] &&
311
- $yLim[1] !== $yLim[0] &&
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
- // update transformation array for x-coordinates
317
- $: {
318
- if ($isOk) {
319
- tX.update(v => ({
320
- 'coords': [ (width - pxMargins[1] - pxMargins[3]) / ($xLim[1] - $xLim[0]), $xLim[0], pxMargins[1]],
321
- 'objects': [ (width - pxMargins[1] - pxMargins[3]) / ($xLim[1] - $xLim[0]), 0, 0]
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
- // computes coordinates for clip path box
337
- $: cpx = $isOk ? transform($xLim, $tX.coords) : [0, 1];
338
- $: cpy = $isOk ? transform($yLim, $tY.coords) : [1, 0];
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
- $: fontSize = PLOT_FONT_SIZE[$scale];
202
+ <!-- snippet for axis -->
203
+ {#snippet axisSnippet(coords, params)}
362
204
 
363
- onMount(() => {
364
- ro.observe(plotElement);
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
- onDestroy(() => {
368
- ro.unobserve(plotElement);
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
- <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}
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
- <svg class="plot" bind:this={plotElement} xmlns="http://www.w3.org/2000/svg"
376
- style={`font-size:${fontSize}px`}>
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
- font-size:1em;
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 xLabel && xLabel !== ''}
417
- <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>
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 title && title !== ''}
421
- <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>
422
397
  {/if}
423
398
  </g>
399
+ {/if}
424
400
 
425
- <svg x={left} y={top} width={width} height={height} on:click={handleClick}
426
- 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">
427
403
 
428
404
  <!-- define clipping path -->
429
405
  <defs>
430
406
  <clipPath id={clipPathID}>
431
- <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]} />
432
408
  </clipPath>
433
409
  </defs>
434
410
 
435
- <!-- axis and box -->
436
- <slot name="xaxis"></slot>
437
- <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}
438
471
 
439
- <!-- main plot content -->
440
- <g class="axes-content" clip-path="url(#{clipPathID})">
441
- <slot></slot>
442
- </g>
443
472
 
444
- <!-- axis and box -->
445
- <slot name="box"></slot>
473
+
474
+ {/if}
475
+
446
476
  </svg>
447
477
  </svg>
448
478
 
449
- {#if !$isOk}
450
- <p class="message_error">
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 on:click={handleClickSVG} on:keydown={handleClickSVG}>⇩ svg</button>
457
- <button on:click={handleClickPNG} on:keydown={handleClickPNG}>⇩ png</button>
458
- <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>
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
- .plot_error {
565
+ .plot-error {
530
566
  display: flex;
531
567
  justify-content: center;
532
568
  align-items: center;
533
569
  }
534
570
 
535
- .message_error {
571
+ .message-error {
536
572
  font-size: 1.2em;
537
573
  color: crimson;
538
574
  padding: 1em;