@cnrs/hecate-views-charts 0.2.0 → 0.5.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,74 +1,40 @@
1
+ import { View } from '@cnrs/hecate-views';
1
2
  import * as d3 from 'd3';
2
3
 
3
4
 
5
+
4
6
  /**
5
7
  * Histogram web component.
6
8
  * @see https://en.wikipedia.org/wiki/Histogram
7
9
  */
8
- class Histogram extends HTMLElement{
9
-
10
- constructor() {
11
- super();
12
- this.attachShadow({ mode: 'open' });
13
- this.hera = {};
14
- this.settings = {};
15
- }
16
- /**
17
- * Static property returning the array of attributes
18
- * observed by the browser.
19
- * @returns {Array<string>} Attribute names
20
- */
21
- static get observedAttributes() {
22
- return ['data-hera', 'data-settings'];
23
- }
24
-
25
- get items() { return this.hera.items || []; }
26
- get entities() { return this.hera.entities || []; }
27
- get properties() { return this.hera.properties || []; }
10
+ class Histogram extends View {
28
11
 
29
12
  get pidx() { return this.settings.x || 'x'; }
30
13
  get pidy() { return this.settings.y || 'y'; }
31
- get color() { return this.settings.color || '#7b4c98'; }
32
14
 
33
15
  get padding() {
34
16
  return this.settings.padding
35
17
  || { top: 20, right: 20, bottom: 30, left: 40 };
36
18
  }
37
19
 
38
- /**
39
- * This is called when an observed attribute is
40
- * defined in the HTML or changed using Javascript.
41
- * @param {!string} attribute - Attribute name
42
- * @param old - Previous value for `attribute`
43
- * @param now - Current value for `attribute`
20
+ /** Runs any required rendering.
44
21
  */
45
- attributeChangedCallback(attribute, old, now) {
46
- if (old === now) return; // don't bother if there is nothing new
47
- this[attribute] = now;
48
- // data-* attributes can only be strings, so we cannot deserialize JSON
49
- // in them ; so we use plain object fields to store deserialized values
50
- // we trim the prefix to get the field name, so 'data-x' becomes 'x'
51
- const fieldName = attribute.split('-').slice(1).join('-');
52
- this[fieldName] = JSON.parse(now);
53
- // note that this does not defeat the purpose of using data-* attributes
54
- // in the first place, as if a name clash should happen in the future,
55
- // we could edit this class with no impact on the users code
56
- }
22
+ renderView() {
23
+ // cleanup
24
+ while (this.viewRoot.lastChild.name !== 'view') { // <slot />
25
+ this.viewRoot.removeChild(this.viewRoot.lastChild);
26
+ }
57
27
 
58
- /**
59
- * Called when the element is connected to a DOM.
60
- * Runs any required rendering.
61
- */
62
- connectedCallback(){
28
+ const bins = this.prepareData();
29
+ // create scene
63
30
 
64
31
  this.width = this.parentElement.clientWidth;
65
32
  this.height = this.parentElement.clientHeight;
66
33
 
67
34
  const container = document.createElement('div');
68
- this.shadowRoot.appendChild(container);
35
+ this.viewRoot.appendChild(container);
69
36
 
70
37
  // group items into "bins", so that scene creation is easier
71
- const bins = this.prepareData();
72
38
  // create viewport, ie. d3 "scale" functions for axes
73
39
  const { vx, vy } = this.createViewport(bins);
74
40
  // create a SVG container for the whole scene
@@ -110,7 +76,7 @@ class Histogram extends HTMLElement{
110
76
  prepareData() {
111
77
  var barValues = [];
112
78
  var classValues = [];
113
- this.items.forEach(item => {
79
+ this.db.items.forEach(item => {
114
80
  item.metadata.forEach(meta => {
115
81
  if (meta.pid === this.pidx) {
116
82
  barValues.push(parseInt(meta.value));
@@ -139,12 +105,12 @@ class Histogram extends HTMLElement{
139
105
  /**
140
106
  * Creates callbacks for changing each bar color when hovering onto.
141
107
  */
142
- createHoverCallbacks(msg) {
108
+ createHoverCallbacks(msg) { // eslint-disable-line no-unused-vars
143
109
  return {
144
- mouseover: function(d) {
110
+ mouseover: function(d) { // eslint-disable-line no-unused-vars
145
111
  d3.select(this).style('opacity', .5)
146
112
  },
147
- mouseleave: function(d) {
113
+ mouseleave: function(d) { // eslint-disable-line no-unused-vars
148
114
  d3.select(this).style('opacity', 1)
149
115
  },
150
116
  };
@@ -212,11 +178,13 @@ class Histogram extends HTMLElement{
212
178
  }
213
179
 
214
180
  getProperty(pid) {
215
- const found = this.properties.find((p) => p['@id'] === pid);
181
+ const found = this.db.properties.find((p) => p['id'] === pid);
216
182
  return found ? found : null;
217
183
  }
218
184
  }
219
185
 
220
186
 
221
187
 
188
+ customElements.define('hecate-histogram', Histogram);
189
+
222
190
  export { Histogram };
@@ -1,7 +1,9 @@
1
- export { BarChart } from './barchart.js';
2
- export { Histogram } from './histogram.js';
3
- export { LineChart } from './linechart.js';
4
- export { ScatterPlot } from './scatterplot.js';
1
+ export * from './barchart.js';
2
+ export * from './histogram.js';
3
+ export * from './linechart.js';
4
+ export * from './scatterchart.js';
5
+ export * from './stackedareachart.js';
6
+ export * from './streamgraph.js';
5
7
 
6
8
  /**
7
9
  * This object is a wrapper for three arrays
@@ -1,234 +1,97 @@
1
+ import { CartesianChart } from './cartesianchart.js';
1
2
  import * as d3 from 'd3';
2
3
 
3
4
 
5
+
4
6
  /**
5
7
  * LineChart web component.
6
8
  * @see https://en.wikipedia.org/wiki/Line_chart
7
9
  */
8
- class LineChart extends HTMLElement{
9
-
10
- constructor(){
11
- super();
12
- this.attachShadow({ mode: 'open' });
13
- this.hera = {};
14
- this.settings = {};
15
- }
16
-
17
-
18
- /** Static property returning an array of attributes observed by the browser
19
- */
20
- static get observedAttributes() {
21
- return ['data-hera', 'data-settings'];
10
+ class LineChart extends CartesianChart {
11
+
12
+ get style() { return this.settings.style; }
13
+ get curve() { return CURVES[this.settings.mark?.curve] ?? d3.curveMonotoneX; }
14
+
15
+ prepare() {
16
+ const map = new Map();
17
+ if (this.settings.color?.pid) {
18
+ // mode: multiple path lines
19
+ this._multiple = true;
20
+ for (const item of this.db.items) {
21
+ const key = this.getItemColor(item);
22
+ const values = map.get(key) || [];
23
+ values.push(item);
24
+ map.set(key, values);
25
+ }
26
+ } else {
27
+ // mode: single path line
28
+ this._multiple = false;
29
+ const key = this.settings.mark?.tooltip || null;
30
+ map.set(key, this.db.items);
22
31
  }
23
-
24
- get items() { return this.hera.items || []; }
25
- get entities() { return this.hera.entities || []; }
26
- get properties() { return this.hera.properties || []; }
27
-
28
- get pidx() { return this.settings.x || 'x'; }
29
- get pidy() { return this.settings.y || 'y'; }
30
- // WARNING: settings.color should be a valid color, ie. #xxx or
31
- // #xxxxxx or one of the acceptable words for color (purple, blue, ...)
32
- // if this value is invalid, line will be drawn, but stroke color
33
- // will be transparent, and thus invisible on any background
34
- get color() { return this.settings.color || '#7b4c98'; }
35
-
36
- /**
37
- * This is called when an observed attribute is
38
- * defined in the HTML or changed using Javascript.
39
- * @param {!string} attribute - Attribute name
40
- * @param old - Previous value for `attribute`
41
- * @param now - Current value for `attribute`
42
- */
43
- attributeChangedCallback(attribute, old, now) {
44
- if (old === now) return; // don't bother if there is nothing new
45
- this[attribute] = now;
46
- // data-* attributes can only be strings, so we cannot deserialize JSON
47
- // in them ; so we use plain object fields to store deserialized values
48
- // we trim the prefix to get the field name, so 'data-x' becomes 'x'
49
- const fieldName = attribute.split('-').slice(1).join('-');
50
- this[fieldName] = JSON.parse(now);
51
- // note that this does not defeat the purpose of using data-* attributes
52
- // in the first place, as if a name clash should happen in the future,
53
- // we could edit this class with no impact on the users code
32
+ this._items_by_c = map;
54
33
  }
55
34
 
56
- connectedCallback(){
57
- var dataset = this.prepareData();
58
-
59
- const margin = { top : 70, right : 30, bottom: 40, left : 80};
60
- var {sceneWidth, sceneHeight, svg} = this.createscene(margin, 600, 1200);
61
-
62
- var {x_scale, y_scale} = this.createAxisScales(sceneHeight, sceneWidth, dataset)
63
- this.addAxises(sceneWidth, sceneHeight, margin, x_scale, y_scale, svg);
64
35
 
65
- this.displayChart(x_scale, y_scale, dataset, svg);
66
- }
36
+ renderData(svg, plot, parsers) {
37
+ const line = d3.line()
38
+ .curve(this.curve)
39
+ .x(item => this.plotX(item, plot, parsers))
40
+ .y(item => this.plotY(item, plot, parsers))
41
+ ;
67
42
 
68
- /**
69
- * Sets the shadow dom of the component, currently creates a single div
70
- * @returns the element that was appended to the shadow dom
71
- */
72
- createShadowDom() {
73
- const e = document.createElement('div');
74
- this.shadowRoot.appendChild(e);
75
- return e;
43
+ for (const [key, items] of this._items_by_c) {
44
+ const color = this._multiple ?
45
+ plot.color(parsers.color(key))
46
+ : this.settings.color?.value || '#7b4c98';
47
+ this.renderPath(svg, items, line, color, key);
48
+ }
76
49
  }
77
50
 
78
- /**
79
- * Prepare the data by giving it the correct format
80
- * Creates a javascript object that contains the elements of metadata, each key representing a different pid
81
- * @param {Hera} data - Hera-formatted data parameter
82
- * @param {Object} settings - Settings parameter
83
- * @returns {Object} JS object containing the elements of metadata,
84
- * x (resp. y) key contains the value for x (resp. y) axis
85
- */
86
- prepareData(){
87
- var yValues = [];
88
- var xValues = [];
89
- this.items.forEach(item => {
90
- item.metadata.forEach(meta => {
91
- if (meta.pid === this.pidy) {
92
- yValues.push(parseInt(meta.value));
93
- }
94
- if (meta.pid === this.pidx){
95
- xValues.push(parseInt(meta.value));
96
- }
97
- });
98
- });
99
- const dataset = xValues.map((x, index) => ({
100
- x: x,
101
- y: yValues[index]
102
- }));
103
- return dataset;
104
- }
105
-
106
- /**
107
- * Creates a svg element that will contain the viewer.
108
- * @param {Object} margin - Space for the axes
109
- * @param {number} width - Desired int width for the svg
110
- * @param {number} height - Desired int height for the svg
111
- * @returns The svg and its height, width and the width of each bars
112
- */
113
- createscene(margin, height, width){
114
- const lineWidth = width - margin.left - margin.right;
115
- const lineHeight = height - margin.top - margin.bottom;
116
-
117
- // create svg and append it
118
- const divElem = this.createShadowDom();
119
- const container = d3.select(divElem);
51
+ renderPath(svg, items, line, color, text=null) {
52
+ const klass = 'series';
53
+ //add line path to svg
54
+ const path = svg.append('path')
55
+ .datum(items)
56
+ .attr('class', klass)
57
+ .attr('fill', 'none')
58
+ .attr('stroke', color)
59
+ .attr('stroke-width', this.style?.normal?.strokeWidth || 2)
60
+ .attr('opacity', this.style?.normal?.opacity || 2)
61
+ .attr('d', line)
62
+ ;
63
+ // add tooltip appearing when line path is hovered, if any
64
+ if (text) path.append('title').text(text);
65
+ path // embiggen hovered line path to make it more visible
66
+ .on('mouseenter', (event) => {
67
+ svg.selectAll(`.${klass}`)
68
+ .attr('stroke-width', this.style?.nothover?.strokeWidth || 2)
69
+ .attr('opacity', this.style?.nothover?.opacity || 0.25);
70
+ d3.select(event.currentTarget)
71
+ .attr('stroke-width', this.style?.hover?.strokeWidth || 4)
72
+ .attr('opacity', this.style?.hover?.opacity || 1)
73
+ .raise();
74
+ }) // make non-hovered line path less visible
75
+ .on('mouseleave', (event) => {
76
+ svg.selectAll(`.${klass}`)
77
+ .attr('stroke-width', this.style?.normal?.strokeWidth || 2)
78
+ .attr('opacity', this.style?.normal?.opacity || 1);
79
+ });
80
+ }
120
81
 
121
- const svg = container.append("svg")
122
- .attr("width", lineWidth + margin.left + margin.right)
123
- .attr("height", lineHeight + margin.top + margin.left)
124
- .append("g")
125
- .attr("transform", `translate(${margin.left},${margin.top})`);
126
- return {
127
- sceneWidth : lineWidth,
128
- sceneHeight : lineHeight,
129
- svg : svg
130
- }
131
- }
132
-
133
- /**
134
- * creates and sets x and y linear scales
135
- * @param {*} sceneHeight
136
- * @param {*} sceneWidth
137
- * @param {*} dataset
138
- * @return x and y linear scales
139
- */
140
- createAxisScales(sceneHeight, sceneWidth, dataset){
141
- //set x and y scales
142
- var x_scale = d3.scaleLinear()
143
- .range([0,sceneWidth])
144
- .domain([0,d3.max(dataset, d => d.x)]);
145
- var y_scale = d3.scaleLinear()
146
- .range([sceneHeight,0])
147
- .domain([0,d3.max(dataset, d => d.y)]);
82
+ }
148
83
 
149
- return{
150
- x_scale : x_scale,
151
- y_scale : y_scale
152
- }
153
-
154
- }
155
84
 
156
85
 
157
- /**
158
- * add the left and bottom axises, with their Label
159
- * @param sceneWidth - Scene width
160
- * @param x_scale - Linear scale for the X axis
161
- * @param y_scale - Linear scale for the Y axis
162
- * @param {*} sceneHeight
163
- * @param {*} sceneWidth
164
- * @param {*} margin
165
- * @param {*} svg
166
- */
167
- addAxises(sceneWidth, sceneHeight, margin, x_scale, y_scale, svg)
168
- {
169
- //add axises
170
- var x_axis = d3.axisBottom(x_scale);
171
- var y_axis = d3.axisLeft(y_scale);
172
- const labels = this.getAxesLabels();
173
-
174
- //x axis
175
- svg.append("g")
176
- .attr("transform", `translate(0,${sceneHeight})`)
177
- .call(x_axis)
178
- .call((g) => g.append("text")
179
- .attr("x", sceneWidth)
180
- .attr("y", margin.bottom - 4)
181
- .attr("fill", "currentColor")
182
- .attr("text-anchor", "end")
183
- .text(labels.x));
184
-
185
-
186
- //y axis
187
- svg.append("g")
188
- .call(y_axis)
189
- .call((g) => g.append("text")
190
- .attr("x", -margin.left)
191
- .attr("y",-10)
192
- .attr("fill", "currentColor")
193
- .attr("text-anchor", "start")
194
- .text(labels.y));
86
+ const CURVES = {
87
+ 'basis': d3.curveBasis,
88
+ 'cardinal': d3.curveCardinal,
89
+ 'catmullrom': d3.curveCatmullRom,
90
+ 'monotoneX': d3.curveMonotoneX,
91
+ 'linear': d3.curveLinear,
92
+ 'step': d3.curveStep,
93
+ };
195
94
 
196
- }
197
- /**
198
- * display the line chart
199
- * @param {*} x_scale
200
- * @param {*} y_scale
201
- * @param {*} dataset
202
- * @param {*} svg
203
- */
204
- displayChart(x_scale, y_scale, dataset, svg){
205
- //draw lines
206
- const line = d3.line()
207
- .x(d => x_scale(d.x))
208
- .y(d => y_scale(d.y));
209
-
210
- //add line path to svg
211
- svg.append("path")
212
- .datum(dataset)
213
- .attr("fill", "none")
214
- .attr("stroke", this.color)
215
- .attr("stroke-width", 1)
216
- .attr("d", line);
217
- }
95
+ customElements.define('hecate-linechart', LineChart);
218
96
 
219
- getAxesLabels() {
220
- const px = this.getProperty(this.pidx);
221
- const py = this.getProperty(this.pidy);
222
- return {
223
- x: px ? px.name : `Missing property '${this.pidx}'`,
224
- y: py ? py.name : `Missing property '${this.pidy}'`,
225
- };
226
- }
227
-
228
- getProperty(pid) {
229
- const found = this.properties.find((p) => p['@id'] === pid);
230
- return found ? found : null;
231
- }
232
-
233
- }
234
97
  export { LineChart };
@@ -0,0 +1,87 @@
1
+ import { CartesianChart } from './cartesianchart.js';
2
+ import * as d3 from 'd3';
3
+
4
+
5
+
6
+ /**
7
+ * Scatterplot web component.
8
+ * @see https://en.wikipedia.org/wiki/Scatter_plot
9
+ * @see https://scottmurray.org/tutorials/d3/
10
+ */
11
+ class ScatterChart extends CartesianChart {
12
+
13
+ prepare() {
14
+ const map = new Map();
15
+ if (this.settings.color?.pid) {
16
+ // mode: multiple path lines
17
+ this._multiple = true;
18
+ for (const item of this.db.items) {
19
+ const key = this.getItemColor(item);
20
+ const values = map.get(key) || [];
21
+ values.push(item);
22
+ map.set(key, values);
23
+ }
24
+ } else {
25
+ // mode: single path line
26
+ this._multiple = false;
27
+ const key = this.settings.mark?.tooltip || null;
28
+ map.set(key, this.db.items);
29
+ }
30
+ this._items_by_c = map;
31
+ }
32
+
33
+ renderData(svg, plot, parsers) {
34
+ for (const [key, items] of this._items_by_c) {
35
+ this.renderGroup(svg, items, key, plot, parsers);
36
+ }
37
+ }
38
+
39
+ renderGroup(svg, items, key, plot, parsers) {
40
+ const klass = 'series';
41
+ const group = svg.append('g').attr('class', klass);//.attr('data-key', key);
42
+ // create a dot for each item in items
43
+ const color = this._multiple ?
44
+ plot.color(parsers.color(key))
45
+ : this.settings.color?.value || '#7b4c98';
46
+ const circles = group.selectAll('circle')
47
+ .data(items)
48
+ .enter()
49
+ .append('circle')
50
+ .attr('cx', item => this.plotX(item, plot, parsers))
51
+ .attr('cy', item => this.plotY(item, plot, parsers))
52
+ .attr('r', item => this.getItemRadius(item))
53
+ .attr('fill', item => color)
54
+ ;
55
+ circles.append('title').text(item => this.getItemLabel(item));
56
+
57
+ group // embiggen hovered line path to make it more visible
58
+ .on('mouseenter', (event) => {
59
+ svg.selectAll(`.${klass}`)
60
+ .attr('opacity', this.style?.nothover?.opacity || 0.25);
61
+ d3.select(event.currentTarget)
62
+ .attr('opacity', this.style?.hover?.opacity || 1)
63
+ .raise();
64
+ }) // make non-hovered line path less visible
65
+ .on('mouseleave', (event) => {
66
+ svg.selectAll(`.${klass}`)
67
+ .attr('opacity', this.style?.normal?.opacity || 1);
68
+ });
69
+ }
70
+
71
+ getItemRadius(item) { // eslint-disable-line no-unused-vars
72
+ return 5;
73
+ }
74
+ getItemLabel(item) {
75
+ const x = this.getItemX(item);
76
+ const y = this.getItemY(item);
77
+ const c = this.getItemColor(item);
78
+ const mc = c ? ` for ${c}` : '';
79
+ return `${y} for ${x}${mc}`
80
+ }
81
+ }
82
+
83
+
84
+
85
+ customElements.define('hecate-scatterchart', ScatterChart);
86
+
87
+ export { ScatterChart };
@@ -0,0 +1,126 @@
1
+ import { CartesianChart } from './cartesianchart.js';
2
+ import * as d3 from 'd3';
3
+
4
+
5
+
6
+ /**
7
+ * Stacked area chart web component.
8
+ * @see https://en.wikipedia.org/wiki/Area_chart
9
+ */
10
+ class StackedAreaChart extends CartesianChart {
11
+
12
+ createXDomain(parsers) {
13
+ return d3.extent(this.db.items, item => parsers.x(this.getItemX(item)));
14
+ }
15
+ createYDomain(parsers) {
16
+ const extent = [0, 1];
17
+ if (!this.normalizeY) {
18
+ const totals = new Map();
19
+ let max = 0;
20
+ for (const item of this.db.items) {
21
+ const x = asKey(parsers.x(this.getItemX(item)));
22
+ const y = parsers.y(this.getItemY(item));
23
+ const total = (totals.get(x) ?? 0) + y;
24
+ totals.set(x, total);
25
+ if (total > max) max = total;
26
+ }
27
+ extent[1] = max;
28
+ }
29
+ return extent;
30
+ }
31
+
32
+ renderVAxis(svg, plot, axis) {
33
+ const xOffset = plot.padding.left;
34
+ const yOffset = plot.padding.top;
35
+ // append the y-axis, while extending 0% & 100% ticks to become top
36
+ // and bottom ticker "main" lines. Add its label, too.
37
+ axis.attr('transform', `translate(${xOffset},${yOffset})`)
38
+ .call(
39
+ g => g.selectAll('.tick line')
40
+ .filter(d => d === 0 || d === 1)
41
+ .clone()
42
+ .attr('x2', plot.width)
43
+ );
44
+ svg.append(() => axis.node());
45
+ }
46
+
47
+ prepare(parsers, scales) {
48
+ // (1) compute pivot table:
49
+ // c1 c2 c3 …
50
+ // x1 y11 y12 y13 …
51
+ // x2 y21 y32 y33 …
52
+ // x3 y31 y32 y33 …
53
+ // … … … … …
54
+ this._table_pivot = new Map();
55
+ for (const item of this.db.items) {
56
+ const x = asKey(parsers.x(this.getItemX(item)));
57
+ const y = parsers.y(this.getItemY(item));
58
+ const c = this.getItemColor(item);
59
+ if (!this._table_pivot.has(x)) this._table_pivot.set(x, new Map());
60
+ this._table_pivot.get(x).set(c, y);
61
+ }
62
+ // (2) compute colors (ie. categories)
63
+ // [ c1, c2, c3, … ]
64
+ const cats = new Set();
65
+ for (const [, values] of this._table_pivot) {
66
+ for (const c of values.keys()) {
67
+ cats.add(c);
68
+ }
69
+ }
70
+ this._categories = [...cats];
71
+ // (3) "stack" series (ie. categories)
72
+ const rows = [...this._table_pivot.entries()];
73
+ this._stacked_series = d3.stack()
74
+ .offset(this.createStackOffset())
75
+ .order(this.createStackOrder())
76
+ .keys(this._categories)
77
+ .value(([, values], c) => values.get(c) ?? 0)(rows);
78
+ }
79
+
80
+ createStackOffset() {
81
+ // stack computation only depends on the axis where values are added, and
82
+ // in this implementation, it's always Y ; thus, only normalizeY matters
83
+ return this.normalizeY ? d3.stackOffsetExpand : d3.stackOffsetNone;
84
+ }
85
+ createStackOrder() {
86
+ return d3.stackOrderNone;
87
+ }
88
+
89
+ renderData(svg, plot, parsers) {
90
+ // Construct an area shape.
91
+ const area = d3.area();
92
+ if (this.vertical) {
93
+ area.y(d => plot.vy(d.data[0]))
94
+ .x0(d => plot.vx(d[0]))
95
+ .x1(d => plot.vx(d[1]));
96
+ } else {
97
+ area.x(d => plot.vx(d.data[0]))
98
+ .y0(d => plot.vy(d[0]))
99
+ .y1(d => plot.vy(d[1]));
100
+ }
101
+
102
+ // Append one area path to render per stacked series (ie. per category)
103
+ svg.append('g')
104
+ .attr('transform', `translate(${plot.padding.left},${plot.padding.top})`)
105
+ .selectAll()
106
+ .data(this._stacked_series)
107
+ .join('path')
108
+ .attr('fill', d => plot.color(d.key))
109
+ .attr('d', area)
110
+ .append('title')
111
+ .text(d => d.key);
112
+ }
113
+ }
114
+
115
+ /** This function is usefulbecause, in JavaScript, when you do:
116
+ * `const x = new Date('1815-12-10'); const y = new Date('1815-12-10');`,
117
+ * you DON'T have x === y, and thus Date is kinda meh as a map key type.
118
+ */
119
+ function asKey(k) { return k instanceof Date ? k.getTime() : k; }
120
+
121
+
122
+
123
+
124
+ customElements.define('hecate-stackedareachart', StackedAreaChart);
125
+
126
+ export { StackedAreaChart };