svelte-plots-basic 1.1.2 → 1.1.4

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 CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  ## Description
4
4
 
5
- `svelte-plots-basic` is a [Svelte](https://svelte.dev) component library for creating very simple 2D plots/charts. It is currently under active development and breaking changes may occur. In its current state (v. 1.1.2) it allows to create bar, scatter, area and line charts as well as any combination of the four. The charts are created using [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) and are re-scalable.
5
+ `svelte-plots-basic` is a [Svelte](https://svelte.dev) component library for creating very simple 2D plots/charts. It is currently under active development and breaking changes may occur. In its current state (v. 1.1.4) it allows to create bar, scatter, area and line charts as well as any combination of the four. The charts are created using [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) and are re-scalable.
6
6
 
7
- The description is not complete yet and will be updated during February-March of 2022.
8
7
 
9
8
  ## Installation
10
9
 
@@ -52,7 +51,7 @@ Just create a new Svelte app following the [quick start guide](https://svelte.de
52
51
  </div>
53
52
  ```
54
53
 
55
- Then run `npm run dev` in terminal and open `localhost:5000` in browser. That is it.
54
+ Then run `npm run dev` in terminal and open the URL provided by npm in browser. That is it.
56
55
 
57
56
  Example below shows how to create a plot with axis, box and grid as well as how to combine several plot series together.
58
57
 
package/package.json CHANGED
@@ -1,35 +1,24 @@
1
1
  {
2
- "name": "svelte-plots-basic",
3
- "svelte": "src/index.js",
4
- "module": "dist/index.mjs",
5
- "main": "dist/index.js",
6
- "scripts": {
7
- "build": "rollup -c",
8
- "prepublishOnly": "npm run build"
9
- },
10
- "devDependencies": {
11
- "@rollup/plugin-node-resolve": "^9.0.0",
12
- "rollup": "^2.0.0",
13
- "rollup-plugin-svelte": "^6.0.0",
14
- "svelte": "^3.0.0"
15
- },
16
- "dependencies": {
17
- "mdatools": "^0.5.3"
18
- },
19
- "keywords": [
20
- "svelte",
21
- "svelte plots"
22
- ],
23
- "files": [
24
- "src",
25
- "dist"
26
- ],
27
- "description": "Svelte components for creating simple plots",
28
- "version": "1.1.2",
29
- "repository": {
30
- "type": "git",
31
- "url": "svkucheryavski/svelte-plots-basic"
32
- },
33
- "author": "Sergey Kucheryavskiy",
34
- "license": "MIT"
2
+ "name": "svelte-plots-basic",
3
+ "description": "Svelte components for creating simple plots",
4
+ "version": "1.1.4",
5
+ "author": "Sergey Kucheryavskiy",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "main": "src/index.js",
9
+
10
+ "dependencies": {
11
+ "mdatools": "^0.6.1"
12
+ },
13
+ "keywords": [
14
+ "svelte",
15
+ "svelte plots"
16
+ ],
17
+ "files": [
18
+ "src"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "svkucheryavski/svelte-plots-basic"
23
+ }
35
24
  }
package/src/Axes.svelte CHANGED
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { setContext, createEventDispatcher, onMount } from 'svelte';
2
+ import { setContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
3
3
  import { writable } from 'svelte/store';
4
4
 
5
5
  /* input parameters */
@@ -118,6 +118,13 @@
118
118
  return adjustedLim;
119
119
  }
120
120
 
121
+ /**
122
+ * Rounds coordinate values to single decimal
123
+ */
124
+ const roundCoords = function(x) {
125
+ return Math.round(x * 10) / 10
126
+ }
127
+
121
128
  /** Rescales x-values from plot coordinates to screen (SVG) coordinates
122
129
  * @param {Array} x - vector with coordinates (or objects size) in original plot coordinates
123
130
  * @param {Array} xLim - vector with current limits for x-axis in original plot coordinates
@@ -126,14 +133,16 @@
126
133
  * @returns {Array} vector with rescaled values
127
134
  */
128
135
  const scaleX = function(x, xLim, width, doSizeScale = false) {
129
- if (!$isOk || x === undefined || !Array.isArray(x)) return undefined;
136
+ if (!$isOk || x === undefined || !Array.isArray(x)) return undefined;
130
137
 
131
138
  if (doSizeScale) {
132
139
  // scale size of objects instead of coordinates
133
- return x.map(v => v / (xLim[1] - xLim[0]) * (width - margins[1] - margins[3]));
140
+ return x.map(v => roundCoords(v / (xLim[1] - xLim[0]) * (width - margins[1] - margins[3])));
134
141
  }
135
142
 
136
- return x.map(v => (v - xLim[0]) / (xLim[1] - xLim[0]) * (width - margins[1] - margins[3]) + margins[1]);
143
+ return x.map(v =>
144
+ roundCoords((v - xLim[0]) / (xLim[1] - xLim[0]) * (width - margins[1] - margins[3]) + margins[1])
145
+ );
137
146
  }
138
147
 
139
148
  /** Rescales x-values from plot coordinates to screen (SVG) coordinates
@@ -144,15 +153,17 @@
144
153
  * @returns {Array} vector with rescaled values
145
154
  */
146
155
  const scaleY = function(y, yLim, height, doSizeScale = false) {
147
- if (!$isOk || y === undefined || !Array.isArray(y)) return undefined;
156
+ if (!$isOk || y === undefined || !Array.isArray(y)) return undefined;
148
157
 
149
158
  if (doSizeScale) {
150
159
  // scale size of objects instead of coordinates
151
- return y.map(v => v / (yLim[1] - yLim[0]) * (height - margins[0] - margins[2]));
160
+ return y.map(v => roundCoords(v / (yLim[1] - yLim[0]) * (height - margins[0] - margins[2])));
152
161
  }
153
162
 
154
163
  // for coordinates we also need to invert (flip) the y-axis
155
- return y.map(v => (yLim[1] - v) / (yLim[1] - yLim[0]) * (height - margins[0] - margins[2]) + margins[2]);
164
+ return y.map(v =>
165
+ roundCoords((yLim[1] - v) / (yLim[1] - yLim[0]) * (height - margins[0] - margins[2]) + margins[2])
166
+ );
156
167
  }
157
168
 
158
169
  /** Computes nice tick values for axis
@@ -317,22 +328,32 @@
317
328
  $: cpx = $isOk ? scaleX($xLim, $xLim, $width) : [0, 1];
318
329
  $: cpy = $isOk ? scaleY($yLim, $yLim, $height) : [1, 0];
319
330
 
320
- /* observer for the plotting area size */
321
- var ro = new ResizeObserver(entries => {
331
+ /* observer for the axes area size - to update size of axes */
332
+ const ro2 = new ResizeObserver(entries => {
322
333
  for (let entry of entries) {
323
-
324
- const pcr = plotElement.getBoundingClientRect();
325
334
  const acr = axesElement.getBoundingClientRect();
326
-
327
335
  width.update(x => acr.width);
328
336
  height.update(x => acr.height);
337
+ }
338
+ });
339
+
340
+ /* observer for the plot area size — to update scale */
341
+ const ro1 = new ResizeObserver(entries => {
342
+ for (let entry of entries) {
343
+ const pcr = plotElement.getBoundingClientRect();
329
344
  scale.update(x => getScale(pcr.width, pcr.height));
330
345
  }
331
346
  });
332
347
 
333
348
  onMount(() => {
334
- ro.observe(plotElement);
349
+ ro1.observe(plotElement);
350
+ ro2.observe(axesElement);
335
351
  });
352
+
353
+ onDestroy(() => {
354
+ ro1.unobserve(plotElement);
355
+ ro2.unobserve(axesElement);
356
+ })
336
357
  </script>
337
358
 
338
359
 
@@ -345,7 +366,7 @@
345
366
 
346
367
  <!-- axes (coordinate system) -->
347
368
  <div class="axes-wrapper" bind:this={axesElement} >
348
- <svg on:click={handleClick} preserveAspectRatio="none" class="axes">
369
+ <svg on:click={handleClick} on:keydown={handleClick} preserveAspectRatio="none" class="axes">
349
370
 
350
371
  <!-- define clipping path -->
351
372
  <defs>
@@ -367,26 +388,26 @@
367
388
  <slot name="box"></slot>
368
389
 
369
390
  </svg>
370
- </div>
371
-
372
391
  {#if !$isOk}
373
392
  <p class="message_error">
374
393
  Axes component was not properly initialized. <br />
375
394
  Add plot series (check that coordinates are numeric) or define axes limits manually.
376
395
  </p>
377
396
  {/if}
397
+ </div>
398
+
378
399
 
379
400
  </div>
380
401
 
381
402
  <style>
382
403
 
383
404
  /* Plot (main container) */
384
- :global(.plot) {
405
+ .plot {
385
406
  font-family: Arial, Helvetica, sans-serif;
386
407
 
387
408
  display: grid;
388
- grid-template-columns: min-content 1fr;
389
- grid-template-rows: auto 1fr auto;
409
+ grid-template-columns: min-content auto;
410
+ grid-template-rows: min-content auto min-content;
390
411
  grid-template-areas:
391
412
  ". title"
392
413
  "ylab axes"
@@ -425,7 +446,8 @@
425
446
  :global(.message_error) {
426
447
  font-size: 1.2em;
427
448
  color: crimson;
428
- padding: 20px;
449
+ padding: 1em;
450
+ box-sizing: border-box;
429
451
  text-align: center;
430
452
  }
431
453
 
@@ -66,13 +66,3 @@
66
66
  </script>
67
67
 
68
68
  <Rectangles style="series_bar" {left} {top} {width} {height} {borderColor} {faceColor} {title} />
69
-
70
- <style>
71
- :global(.series_bar > rect){
72
- stroke-width: 1px;
73
- }
74
-
75
- :global(.series_bar > rect:hover){
76
- opacity: 90%;
77
- }
78
- </style>
@@ -48,7 +48,7 @@
48
48
  $: rh = axes.scaleY(height, $yLim, $axesHeight, true);
49
49
 
50
50
  // styles for bars and labels
51
- $: barsStyleStr = `fill:${faceColor};stroke:${borderColor};stroke-width: ${lineWidth}px;`;
51
+ $: barsStyleStr = `fill:${faceColor};stroke:${borderColor};stroke-width:${lineWidth}px;`;
52
52
 
53
53
  </script>
54
54
 
@@ -67,19 +67,3 @@
67
67
  textSize={markerSize}
68
68
  />
69
69
 
70
- <style>
71
-
72
- :global(.series_scatter > text) {
73
- font-size: 1em;
74
- cursor: default;
75
- }
76
-
77
- :global(.series_scatter > text:hover) {
78
- opacity: 90%;
79
- }
80
-
81
- :global(.series_scatter > *:hover + .labels_hover) {
82
- visibility: visible;
83
- opacity: 1;
84
- }
85
- </style>
@@ -10,6 +10,8 @@
10
10
  export let lineColor = Colors.PRIMARY;
11
11
  export let lineType = 1;
12
12
  export let lineWidth = 1;
13
+ export let title = "";
14
+ export let style = "series_seg";
13
15
 
14
16
  /* sanity check for input parameters */
15
17
  if (!Array.isArray(xStart) || !Array.isArray(xEnd) || !Array.isArray(yStart) || !Array.isArray(yEnd)) {
@@ -39,9 +41,11 @@
39
41
  </script>
40
42
 
41
43
  {#if x1 !== undefined && y1 !== undefined}
44
+ <g class="series {style}" title={title} style={lineStyleStr}>
42
45
  {#each x1 as v, i}
43
- <line x1="{x1[i]}" x2="{x2[i]}" y1="{y1[i]}" y2="{y2[i]}" style="{lineStyleStr}"></line>
46
+ <line x1={x1[i]} x2={x2[i]} y1={y1[i]} y2={y2[i]}></line>
44
47
  {/each}
48
+ </g>
45
49
  {/if}
46
50
 
47
51
  <style>
@@ -8,6 +8,8 @@
8
8
 
9
9
  import { getContext } from 'svelte';
10
10
  import { Colors } from './Colors';
11
+ import { subset } from 'mdatools/stat';
12
+ import { vmult } from 'mdatools/matrix';
11
13
 
12
14
  // input parameters
13
15
  export let xValues;
@@ -21,28 +23,24 @@
21
23
  export let style = "";
22
24
  export let title = "series_text";
23
25
 
24
- // text-anchor values depending on position
25
- const textAnchors = ["middle", "middle", "start", "middle", "end"];
26
+
27
+ // function to prepare values for variables 'label' and 'pos'
28
+ function processValues(v, n, name) {
29
+ if (!Array.isArray(v)) return Array(n).fill(v);
30
+ if (v.length !== n) {
31
+ throw(`TextLabels: parameter ${name} must be a single number or a vector of the same size as 'x' and 'y'.`)
32
+ }
33
+ return v;
34
+ }
26
35
 
27
36
  // sanity check for input parameters
28
- if (!Array.isArray(xValues) || !Array.isArray(yValues) || xValues.length !== yValues.length) {
37
+ if (!Array.isArray(xValues) || !Array.isArray(yValues) || xValues.length !== yValues.length) {
29
38
  throw("TextLabels: parameters 'xValues' and 'yValues' must be vectors of the same length.")
30
39
  }
31
40
 
32
- // multiply label values if needed
33
- $: {
34
- const n = xValues.length;
35
- if (!Array.isArray(labels)) labels = Array(n).fill(labels);
36
-
37
- // workaround for an issue when xValues and yValues are changed in parent app
38
- // but array of labels is still the same as in the
39
- if (labels.length != n) labels = Array(n).fill(labels[0]);
40
-
41
- // check that the length of labels vector is correct
42
- if (labels.length !== n) {
43
- throw("TextLabels: parameter 'labels' must be a single text value or a vector of the same size as 'x' and 'y'.")
44
- }
45
- }
41
+ // prepare values for labels and positions
42
+ $: labelsLocal = processValues(labels, xValues.length);
43
+ $: posLocal = pos === 0 ? pos : processValues(pos, xValues.length);
46
44
 
47
45
  // get axes context and reactive variables needed to compute coordinates
48
46
  const axes = getContext('axes');
@@ -55,24 +53,36 @@
55
53
  // reactive variables for coordinates of data points in pixels
56
54
  $: x = axes.scaleX(xValues, $xLim, $axesWidth);
57
55
  $: y = axes.scaleY(yValues, $yLim, $axesHeight);
58
- $: dx = [0, 0, 1, 0, -1][pos] * axes.LABELS_MARGIN[$scale];
59
- $: dy = [0, 1, 0, -1, 0][pos] * axes.LABELS_MARGIN[$scale];
56
+ $: dx = pos === 0 ? 0 : subset(vmult([0, 1, 0, -1], axes.LABELS_MARGIN[$scale]), posLocal)
57
+ $: dy = pos === 0 ? 0 : subset(vmult([1, 0, -1, 0], axes.LABELS_MARGIN[$scale]), posLocal)
58
+ $: textAnchors = pos === 0 ? "middle" : subset([ "middle", "start", "middle", "end"], posLocal);
60
59
 
61
60
  // styles for the elements
62
- $: textStyleStr = `fill:${faceColor};stroke-width:${borderWidth}px;stroke:${borderColor};
63
- font-size:${textSize}em; text-anchor:${textAnchors[pos]};`;
61
+ $: textStyleStr = `dominant-baseline:middle;fill:${faceColor};stroke-width:${borderWidth}px;stroke:${borderColor};
62
+ font-size:${textSize}em; text-anchor:middle;`;
64
63
  </script>
65
64
 
66
65
  {#if x !== undefined && y !== undefined}
67
66
  <g class="series {style}" title={title} style={textStyleStr} >
67
+
68
+ {#if pos === 0}
69
+ {#each x as v, i}
70
+ <text data-id={i} x={x[i]} y={y[i]}>{@html labelsLocal[i]}</text>
71
+ {/each}
72
+
73
+ {:else}
68
74
  {#each x as v, i}
69
- <text data-id={i} x={x[i]} y={y[i]} dx={dx} dy={dy}>{@html labels[i]}</text>
75
+ <text data-id={i} x={x[i]} y={y[i]} dx={dx[i]} dy={dy[i]} text-anchor={textAnchors[i]}>{@html labelsLocal[i]}</text>
70
76
  {/each}
77
+ {/if}
78
+
71
79
  </g>
72
80
  {/if}
73
81
 
74
82
  <style>
75
83
  text, text > :global(tspan) {
84
+ cursor: default;
85
+ user-select: none;
76
86
  dominant-baseline: middle;
77
87
  }
78
88
  </style>