svelte-plots-basic 3.2.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021-2026 Sergey Kucheryavskiy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,7 +15,31 @@ These websites and web-applications use `svelte-plots-basic` library:
15
15
 
16
16
  ## News
17
17
 
18
- ### 3.2.0
18
+ ### 3.4.0
19
+
20
+ * Added the `plotActions` property to `Axes` for adding custom buttons to the download panel.
21
+ * Added advanced validation for axis limits, text-label positions, heatmap breaks, and custom plot actions.
22
+ * Fixed text-label positioning and rotation when individual positions are provided.
23
+ * Fixed heatmaps with automatic, custom, or constant-value breaks.
24
+ * Added support for rectangular and constant-height 3D meshes.
25
+ * Improved PNG, SVG, and clipboard export by serializing a clone instead of modifying the visible plot.
26
+ * Fixed the advanced PNG export dialog so a default resolution is always selected, falling back to 300 dpi when necessary.
27
+ * Prevented plot controls from submitting an enclosing HTML form.
28
+ * Added package validation before publishing and synchronized Svelte 5 peer-dependency metadata.
29
+ * Fixed conditionally rendered axes, boxes, and legends so their parent state is cleared when they are removed.
30
+ * Improved heatmap interval processing and added automatically generated colormaps with more than 16 colors.
31
+ * Fixed `TextLegend` positioning at zero and strengthened finite-range validation for 3D axes.
32
+ * Reduced reactive and repeated allocation overhead for axis configuration, text measurement, PNG export, and colormap generation.
33
+ * Added dependency-free regression tests and made them run automatically before packaging.
34
+ * Documented the trust requirements for labels rendered as SVG/HTML markup.
35
+
36
+ ### 3.3.0
37
+
38
+ * Added advanced "save as PNG" option where user can select size and resolution of the image as well as change the filename.
39
+ * several small improvements and bug fixes.
40
+
41
+
42
+ ### 3.2.1
19
43
 
20
44
  * Added mouse click handler option for `Heatmap.svelte` component (returns row and column index of the element).
21
45
 
@@ -51,6 +75,13 @@ In addition to Svelte 5 syntax and functionality, this release also introduces a
51
75
 
52
76
  * **Properites** — from 3.x.x the naming of the properties is more consistent. For example, before the library used properties `borderColor` and `borderWidth` for areas, rectangels, bars and markers, while for lines, segments and multilines the similar properties were named as `lineColor` and `lineWidth`. Now they all have prefix `line*` if it is something about lines or segments and `face*` if it is somthing inside a closed contour. So no more `border*` properties.
53
77
 
78
+
79
+ ## Security and label markup
80
+
81
+ Several title and label properties support SVG/HTML markup and are rendered as raw markup using Svelte's `{@html}` mechanism. This enables formatted labels, HTML entities, subscripts, and superscripts.
82
+
83
+ Only pass trusted content to these properties. Do not pass unsanitized user input or external data directly as a plot title, axis label, tick label, legend label, or text-label value. Escape untrusted text, or sanitize it with an allow-list appropriate for SVG markup, before passing it to a component.
84
+
54
85
  ## Installation
55
86
 
56
87
  The set up process is similar to any other Svelte component library. Just use:
@@ -134,6 +165,7 @@ Here is a table with properties of the component related to saving plot to a fil
134
165
  `pngRes` | `300` | resolution of PNG image (pixels per inch)
135
166
  `clipboardWidth` | `1200` | width of image in clipboard in pixels
136
167
  `clipboardHeight` | `800` | height of image in clipboard in pixels
168
+ `plotActions` | `[]` | array of `{ text, callback }` objects for adding custom buttons to the download panel.
137
169
 
138
170
  Please pay attention that most of the browsers enable clipboard functionality only if website is available via HTTPS.
139
171
 
@@ -835,15 +867,12 @@ Here is an example of simple 3D scatter plot:
835
867
  <Axes limX={[-10, 10]} limY={[-10, 10]} limZ={[-10, 10]} {zoom} {phi} {theta}>
836
868
  <Points {xValues} {yValues} {zValues} />
837
869
 
838
- <XAxis showGrid={true} title="X" />
839
- <YAxis showGrid={true} title="Y" />
840
- <ZAxis showGrid={true} title="Z" />
870
+ <XAxis showGrid={true} label="X" />
871
+ <YAxis showGrid={true} label="Y" />
872
+ <ZAxis showGrid={true} label="Z" />
841
873
  </Axes>
842
874
  ```
843
875
 
844
876
  You can also add `Lines`, `Segments` and `Mesh` series to 3D plots.
845
877
 
846
878
  Check more advanced example in Svelte REPL with mouse and keyboard support, and more: [plots-3d](https://svelte.dev/playground/2294eecba7d7477ab9a09c1734d32ac2)
847
-
848
-
849
-
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "svelte-plots-basic",
3
3
  "description": "Svelte components for creating simple plots",
4
- "version": "3.2.1",
4
+ "version": "3.4.0",
5
5
  "author": "Sergey Kucheryavskiy",
6
6
  "license": "MIT",
7
7
  "private": false,
8
+ "type": "module",
9
+ "scripts": {
10
+ "check": "node scripts/check.cjs",
11
+ "test": "node --test",
12
+ "prepack": "npm run check && npm test"
13
+ },
8
14
  "exports": {
9
15
  "./constants": "./src/constants.js",
10
16
  "./utils": "./src/methods.js",
@@ -16,6 +22,9 @@
16
22
  "devDependencies": {
17
23
  "svelte": "^5.x.x"
18
24
  },
25
+ "peerDependencies": {
26
+ "svelte": "^5.0.0"
27
+ },
19
28
  "dependencies": {
20
29
  "mdatools": "^1.x.x"
21
30
  },
@@ -32,7 +32,6 @@
32
32
  import { val2p, checkCoords, handleClick, transformCoords } from '../methods';
33
33
  import { LINE_STYLES } from '../constants';
34
34
 
35
- /** @type {Props} */
36
35
  let {
37
36
  xValues, // vector or array with x-coordinates of the points
38
37
  yValues, // vector or array with y-coordinates of the points
@@ -66,4 +65,3 @@
66
65
  <polygon points={pa}/>
67
66
  </g>
68
67
  {/if}
69
-
@@ -30,13 +30,14 @@
30
30
  <script>
31
31
  import { setContext } from 'svelte';
32
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,
33
+ import { downloadSVG, downloadPNG, copyToClipboard, getScale, getAxisScale, getXAxisParams, getYAxisParams,
34
34
  transformCoords, getColormapLegendParams, getColormapLegendCoords, getTickFactorLabel,
35
35
  getGroupLegendCoords, text2svg,
36
36
  invTransformCoords } from '../methods.js';
37
37
 
38
38
  import AxisLines from './AxisLines.svelte';
39
39
  import AxisTickLabels from './AxisTickLabels.svelte';
40
+ import ExportDialog from '../ExportDialog.svelte';
40
41
 
41
42
 
42
43
  let {
@@ -45,6 +46,7 @@
45
46
  limY = [0, 1],
46
47
  margins = [1.0, 1.1, 0.55, 0.60],
47
48
  downloadLinks = 'none',
49
+ plotActions = [],
48
50
  fileName = 'plot',
49
51
  pngWidth = 8,
50
52
  pngHeight = 8,
@@ -59,6 +61,44 @@
59
61
  } = $props();
60
62
 
61
63
 
64
+ const actions = $derived.by(() => {
65
+ if (plotActions == null) return [];
66
+
67
+ if (!Array.isArray(plotActions)) {
68
+ console.error('Axes: "plotActions" must be an array.');
69
+ return [];
70
+ }
71
+
72
+ return plotActions.filter((action, index) => {
73
+ const isValid =
74
+ action &&
75
+ typeof action.text === 'string' &&
76
+ typeof action.callback === 'function';
77
+
78
+ if (!isValid) {
79
+ console.error(
80
+ `Axes: plot action ${index + 1} must contain a text string and callback function.`
81
+ );
82
+ }
83
+
84
+ return isValid;
85
+ });
86
+ });
87
+
88
+
89
+ // Check that axis limits contain a finite, meaningful numeric range.
90
+ function hasUsableRange(lim) {
91
+ if (!Array.isArray(lim) || lim.length !== 2) return false;
92
+ if (!lim.every(v => typeof v === 'number' && Number.isFinite(v))) return false;
93
+
94
+ const span = Math.abs(lim[1] - lim[0]);
95
+ const scale = Math.max(Math.abs(lim[0]), Math.abs(lim[1]));
96
+
97
+ return Number.isFinite(1 / span) &&
98
+ span > Number.EPSILON * scale * 16;
99
+ }
100
+
101
+
62
102
  /* handler for mouse events */
63
103
  function handleMouse(e, f) {
64
104
  if (f && e.target.id == "axes-box") {
@@ -103,9 +143,14 @@
103
143
  downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes);
104
144
  }
105
145
 
146
+ /* Open advanced PNG export dialog. */
147
+ let showExportDialog = $state(false);
148
+ function handleClickPNGAdvanced() { showExportDialog = true; }
149
+ function handleCloseExportDialog() { showExportDialog = false; }
150
+
106
151
 
107
152
  // unique ID for clip path
108
- const clipPathID = 'plottingArea' + Math.round(Math.random() * 10000);
153
+ const clipPathID = 'plottingArea' + Math.random().toString(36).slice(2);
109
154
 
110
155
  // pointers to plot DOM element and its width and height
111
156
  let plotElement = $state(0);
@@ -132,8 +177,8 @@
132
177
 
133
178
  // plot status
134
179
  const isOk = $derived(
135
- checkArray(limX, 2) &&
136
- checkArray(limY, 2) &&
180
+ hasUsableRange(limX) &&
181
+ hasUsableRange(limY) &&
137
182
  width > (pxMargins[1] + pxMargins[3]) &&
138
183
  height > (pxMargins[0] + pxMargins[2])
139
184
  );
@@ -166,14 +211,14 @@
166
211
 
167
212
  // parameters of plot title
168
213
  const plotTitle = $derived(text2svg(title));
169
- const titleHeight = $derived(plotTitle ? (plotTitle.length === title.left ? 1.0 : 1.4) : 0);
214
+ const titleHeight = $derived(plotTitle ? (plotTitle.length === title.length ? 1.0 : 1.4) : 0);
170
215
 
171
216
  // default parameters of box, axis elements, colomap legend and group legend
172
- let box = $state({show: false});
173
- let xaxis = $state({show: false, error: ''});
174
- let yaxis = $state({show: false, error: ''});
175
- let clg = $state({show: false, colmap: null, breaks: null});
176
- let glg = $state({show: false, items: null, position: null});
217
+ let box = $state.raw({show: false});
218
+ let xaxis = $state.raw({show: false, error: ''});
219
+ let yaxis = $state.raw({show: false, error: ''});
220
+ let clg = $state.raw({show: false, colmap: null, breaks: null});
221
+ let glg = $state.raw({show: false, items: null, position: null});
177
222
 
178
223
  // set colormap legend parameters if requested
179
224
  const clgParams = $derived(isOk && clg && clg.show ? getColormapLegendParams(clg) : null);
@@ -220,7 +265,7 @@
220
265
 
221
266
  {#if coords.tickCoords.length === 2 && coords.tickLabels.length === coords.tickCoords[1][0].length}
222
267
  <AxisTickLabels las={params.las} pos={params.pos} tickCoords={coords.tickCoords}
223
- tickLabels={coords.tickLabels} tickColor={Colors.DARKGRAY} hideLast={params.hideLast}/>
268
+ tickLabels={coords.tickLabels} textColor={Colors.DARKGRAY} />
224
269
  {/if}
225
270
 
226
271
  <!-- tick factor -->
@@ -262,7 +307,7 @@
262
307
  <text {x} y={coords.ltop} dx="1.2em" dy="-0.25em" dominant-baseline="hanging"
263
308
  fill={params.labelColor}
264
309
  font-size="{params.fontSize}em"
265
- text-anchor="right"
310
+ text-anchor="end"
266
311
  >{@html getTickFactorLabel(params.labelsFactor)}</text>
267
312
  {/if}
268
313
  {/snippet}
@@ -340,11 +385,6 @@
340
385
  user-select: none;
341
386
  }
342
387
 
343
- .series-points text {
344
- /* dominant-baseline: central;
345
- alignment-baseline: central */
346
- }
347
-
348
388
  .tick-labels text,
349
389
  .tick-labels tspan,
350
390
  .series-textlabels text,
@@ -444,27 +484,18 @@
444
484
 
445
485
  <!-- box -->
446
486
  <g class="axes-box">
447
- {#if box.show}
448
- <rect stroke={box.lineColor} stroke-width="{box.lineWidth}px" fill="transparent"
487
+ <!-- The plot callback requires real pointer coordinates; a keyboard-generated coordinate would be arbitrary. -->
488
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
489
+ <rect stroke={box.show ? box.lineColor : 'transparent'}
490
+ stroke-width={box.show ? box.lineWidth + 'px' : '0'}
491
+ fill="transparent"
449
492
  x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
450
493
  id="axes-box"
451
- onkeydown={handleClick}
452
494
  onclick={handleClick}
453
495
  onmousemove={handleMove}
454
496
  onmousedown={handleDown}
455
497
  onmouseup={handleUp}
456
498
  />
457
- {:else}
458
- <rect stroke="transparent" fill="transparent"
459
- x={cpx[0]} y={cpy[1]} width={cpx[1] - cpx[0]} height={cpy[0] - cpy[1]}
460
- id="axes-box"
461
- onkeydown={handleClick}
462
- onclick={handleClick}
463
- onmousemove={handleMove}
464
- onmousedown={handleDown}
465
- onmouseup={handleUp}
466
- />
467
- {/if}
468
499
  </g>
469
500
 
470
501
 
@@ -501,11 +532,24 @@
501
532
  </p>
502
533
  {:else}
503
534
  <div class="download-links">
504
- <button onclick={handleClickSVG} onkeydown={handleClickSVG}>⇩ svg</button>
505
- <button onclick={handleClickPNG} onkeydown={handleClickPNG}>⇩ png</button>
506
- <button onclick={handleClickCopy} onkeydown={handleClickCopy}>⧉ copy</button>
535
+ <button type="button" onclick={handleClickSVG}>↓ svg</button>
536
+ <button type="button" onclick={handleClickPNG}>↓ png</button>
537
+ <button type="button" onclick={handleClickPNGAdvanced}>↓ png+</button>
538
+ <button type="button" onclick={handleClickCopy}>⧉ copy</button>
539
+ {#if actions.length > 0}
540
+ <span class="divider">⋮</span>
541
+ {#each actions as ac}
542
+ <button type="button" onclick={ac.callback}>{ac.text}</button>
543
+ {/each}
544
+ {/if}
507
545
  </div>
508
546
  {/if}
547
+
548
+ {#if showExportDialog}
549
+ <ExportDialog {plotElement} {fileName}
550
+ initialWidth={pngWidth} initialRes={pngRes}
551
+ onclose={handleCloseExportDialog} />
552
+ {/if}
509
553
  </div>
510
554
 
511
555
  <style>
@@ -534,7 +578,6 @@
534
578
  transition: bottom 0.35s ease;
535
579
  box-shadow: 0 0 0.75em #00000060;
536
580
  margin: 0.6em;
537
- box-sizing: border-box;
538
581
 
539
582
  display: flex;
540
583
  align-items: center;
@@ -549,6 +592,9 @@
549
592
  background: #fafafa;
550
593
  border-radius: 0.35em;
551
594
  padding: 0.25em 0.5em;
595
+ margin: 0 1px;
596
+
597
+ line-height: 1.25;
552
598
  }
553
599
 
554
600
  .download-links > button:hover{
@@ -564,6 +610,12 @@
564
610
  background: #4488ee;
565
611
  }
566
612
 
613
+ .download-links > .divider {
614
+ display: inline-block;
615
+ width: 1em;
616
+ text-align: center;
617
+ color: #909090;
618
+ }
567
619
 
568
620
  /* Plot container */
569
621
  .plot-container {
@@ -621,4 +673,4 @@
621
673
  min-width: 100%;
622
674
  }
623
675
 
624
- </style>
676
+ </style>
@@ -34,7 +34,6 @@
34
34
  import Rectangles from './Rectangles.svelte';
35
35
 
36
36
 
37
- /** @type {Props} */
38
37
  let {
39
38
  xValues, // vector with x-coordinates of middle points of the bars.
40
39
  yValues, // vector with y-coordinates of top points of the bars.
@@ -68,11 +67,10 @@
68
67
  console.error('Bars: if only one bar must be shown, value for property "barWidthExact" should be provided.');
69
68
  return null;
70
69
  } else {
71
- w = 0;
72
- let lw = 0;
70
+ w = Infinity;
73
71
  for (let i = 1; i < n; i++) {
74
- lw = Math.abs(xv.v[i - 1] - xv.v[i]);
75
- if (lw > w) w = lw;
72
+ const lw = Math.abs(xv.v[i - 1] - xv.v[i]);
73
+ if (lw < w) w = lw;
76
74
  }
77
75
  }
78
76
 
@@ -117,4 +115,4 @@
117
115
  {#if isOk}
118
116
  <Rectangles className="series series-bar" left={x.left} width={x.width} top={y.top} height={y.height}
119
117
  lineWidth={lineWidth} {lineColor} {faceColor} {onclick} />
120
- {/if}
118
+ {/if}
package/src/2d/Box.svelte CHANGED
@@ -16,7 +16,7 @@
16
16
  ```
17
17
  -->
18
18
  <script>
19
- import { getContext } from 'svelte';
19
+ import { getContext, onDestroy } from 'svelte';
20
20
 
21
21
  let {lineWidth = 1, lineColor = '#606060'} = $props();
22
22
  const axes = getContext('axes');
@@ -24,5 +24,8 @@
24
24
  $effect(() => {
25
25
  axes.setBox({show:true, lineWidth, lineColor});
26
26
  });
27
- </script>
28
27
 
28
+ onDestroy(() => {
29
+ axes.setBox({show: false});
30
+ });
31
+ </script>
@@ -34,7 +34,7 @@
34
34
  ```
35
35
  -->
36
36
  <script>
37
- import { getContext } from 'svelte';
37
+ import { getContext, onDestroy } from 'svelte';
38
38
  import { Colors } from '../constants';
39
39
 
40
40
 
@@ -52,7 +52,10 @@
52
52
  $effect(() => {
53
53
  axes.setColmapLegend({show, breaks, colmap, labels, labelColor, fontSize})
54
54
  });
55
- </script>
56
55
 
56
+ onDestroy(() => {
57
+ axes.setColmapLegend({show: false});
58
+ });
59
+ </script>
57
60
 
58
61
 
@@ -27,13 +27,12 @@
27
27
  import { getContext } from 'svelte';
28
28
  import { ismatrix, isvector } from 'mdatools/arrays';
29
29
  import { expandgrid } from 'mdatools/misc';
30
- import { split } from 'mdatools/stat';
30
+ import { range, split } from 'mdatools/stat';
31
31
 
32
32
  import { getcolmap, transformObjects, transformCoords, seq } from '../methods';
33
33
  import { HEATMAP_NUM_SPLITS } from '../constants';
34
34
 
35
35
 
36
- /** @type {Props} */
37
36
  let {
38
37
  values, // Matrix with values to show heatmap for
39
38
  breaks, // vector with breaks to distribute the values in
@@ -42,6 +41,54 @@
42
41
  } = $props();
43
42
 
44
43
 
44
+ function validateBreaks(values) {
45
+ if (!values || values.length < 2) {
46
+ console.error('Heatmap: "breaks" must contain at least two values.');
47
+ return null;
48
+ }
49
+
50
+ const items = Array.from(values);
51
+
52
+ if (items.some(value =>
53
+ typeof value !== 'number' || Number.isNaN(value)
54
+ )) {
55
+ console.error('Heatmap: values in "breaks" must be numeric.');
56
+ return null;
57
+ }
58
+
59
+ if (items.some((value, index) =>
60
+ index > 0 && value <= items[index - 1]
61
+ )) {
62
+ console.error('Heatmap: values in "breaks" must be strictly increasing.');
63
+ return null;
64
+ }
65
+
66
+ return values;
67
+ }
68
+
69
+
70
+ function findInterval(value, breaks) {
71
+ let low = 1;
72
+ let high = breaks.length - 1;
73
+
74
+ while (low < high) {
75
+ const middle = Math.floor((low + high) / 2);
76
+
77
+ if (value <= breaks[middle]) {
78
+ high = middle;
79
+ } else {
80
+ low = middle + 1;
81
+ }
82
+ }
83
+
84
+ const index = low - 1;
85
+
86
+ return value > breaks[index] && value <= breaks[index + 1]
87
+ ? index
88
+ : -1;
89
+ }
90
+
91
+
45
92
 
46
93
  // check that values are provided as a matrix
47
94
  let v = $derived(ismatrix(values) && values.nrows > 0 && values.ncols > 0 ? values : null);
@@ -49,19 +96,37 @@
49
96
  // check and process left values for breaks
50
97
  let lb = $derived.by(() => {
51
98
  if (!v) return null;
52
- if (breaks && Array.isArray(breaks)) return breaks;
53
- if (breaks && isvector(breaks)) return breaks.v;
54
- return split(v.v, HEATMAP_NUM_SPLITS).v;
99
+
100
+ if (breaks !== undefined && breaks !== null) {
101
+ if (Array.isArray(breaks)) return validateBreaks(breaks);
102
+ if (isvector(breaks)) return validateBreaks(breaks.v);
103
+
104
+ console.error('Heatmap: "breaks" must be an array or Vector.');
105
+ return null;
106
+ }
107
+
108
+ const valueRange = range(v.v);
109
+ if (valueRange[0] === valueRange[1]) {
110
+ const value = valueRange[0];
111
+ const delta = Math.abs(value) > 0 ? Math.abs(value) * 0.01 : 1;
112
+ const lower = Number.isFinite(value - delta) ? value - delta : value;
113
+ const upper = Number.isFinite(value + delta) ? value + delta : value;
114
+ return validateBreaks([lower, upper]);
115
+ }
116
+
117
+ return validateBreaks(split(v.v, HEATMAP_NUM_SPLITS).v);
55
118
  });
56
119
 
57
120
  // check and process colormap values
58
121
  let lc = $derived.by(() => {
122
+ if (!lb) return null;
123
+
59
124
  if (colmap) {
60
125
  if (!Array.isArray(colmap)) {
61
126
  console.error('Heatmap: parameter "colmap" must be array with colors.');
62
127
  return null;
63
128
  }
64
- if (colmap.length !== breaks.length - 1) {
129
+ if (colmap.length !== lb.length - 1) {
65
130
  console.error('Heatmap: number of color values in colormap does not match number of intervals defined by breaks.');
66
131
  return null;
67
132
  }
@@ -81,8 +146,11 @@
81
146
  const [top, left] = lt;
82
147
  const nv = left.length; // number of values in matrix
83
148
  const nb = lb.length; // number of breaks
84
- const rl = Array(nb).fill().map(() => []); // coordinates of left for every break
85
- const rt = Array(nb).fill().map(() => []); // coordinates of top for every break
149
+ const intervalCount = nb - 1;
150
+ const rl = Array.from({length: intervalCount}, () => []); // coordinates of left for every interval
151
+ const rt = Array.from({length: intervalCount}, () => []); // coordinates of top for every interval
152
+ const rr = Array.from({length: intervalCount}, () => []); // row indices for every interval
153
+ const rc = Array.from({length: intervalCount}, () => []); // column indices for every interval
86
154
 
87
155
  // adjust left side for the first break and right side for the last
88
156
  const llb = lb.slice();
@@ -93,15 +161,15 @@
93
161
  // loop over all breaks
94
162
  for (let j = 0; j < nv; j++) {
95
163
  const vj = v.v[j];
96
- for (let i = 0; i < nb - 1; i++) {
97
- if (vj > llb[i] && vj <= llb[i + 1]) {
98
- rl[i].push(left.v[j] - 0.5);
99
- rt[i].push(v.nrows - top.v[j] + 1.5);
100
- break;
101
- }
102
- }
164
+ const i = findInterval(vj, llb);
165
+ if (i === -1) continue;
166
+
167
+ rl[i].push(left.v[j] - 0.5);
168
+ rt[i].push(v.nrows - top.v[j] + 1.5);
169
+ rr[i].push(top.v[j]);
170
+ rc[i].push(left.v[j]);
103
171
  };
104
- return {rl, rt};
172
+ return {rl, rt, rr, rc};
105
173
  });
106
174
 
107
175
 
@@ -119,13 +187,12 @@
119
187
  const el = e.target;
120
188
  if (el.tagName !== 'rect') return;
121
189
 
122
- const pel = el.parentNode.parentNode;
123
- const pr = pel.getBoundingClientRect();
124
- const r = el.getBoundingClientRect();
125
- const x = (r.x - pr.x) + r.width * 0.2;
126
- const y = (r.y - pr.y) + r.height * 0.2;
127
- onclick(Math.abs(Math.round(y / r.height)), Math.abs(Math.round(x / r.width)))
128
- e.stopPropagation();
190
+ const row = parseInt(el.getAttribute('data-row'));
191
+ const col = parseInt(el.getAttribute('data-col'));
192
+ if (!isNaN(row) && !isNaN(col)) {
193
+ onclick(row, col);
194
+ e.stopPropagation();
195
+ }
129
196
  }
130
197
 
131
198
 
@@ -143,7 +210,7 @@
143
210
  {#if rx[i].length > 0}
144
211
  <!-- loop over elements which fall into the interval -->
145
212
  {#each rx[i] as v, j}
146
- <rect x={rx[i][j]} y={ry[i][j]} width={rw} height={rh}/>
213
+ <rect x={rx[i][j]} y={ry[i][j]} width={rw} height={rh} data-row={wc.rr[i][j]} data-col={wc.rc[i][j]}/>
147
214
  {/each}
148
215
  {/if}
149
216
  </g>