@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.
- package/README.md +21 -4
- package/package.json +45 -20
- package/src/init.js +8 -8
- package/src/util.js +13 -0
- package/src/views/all.js +6 -0
- package/src/views/barchart.js +37 -63
- package/src/views/cartesianchart.js +309 -0
- package/src/views/histogram.js +19 -51
- package/src/views/index.js +6 -4
- package/src/views/linechart.js +76 -213
- package/src/views/scatterchart.js +87 -0
- package/src/views/stackedareachart.js +126 -0
- package/src/views/streamgraph.js +39 -0
- package/src/components/index.js +0 -1
- package/src/components/sample.js +0 -37
- package/src/views/scatterplot.js +0 -193
package/src/views/histogram.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
this
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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.
|
|
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['
|
|
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 };
|
package/src/views/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
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
|
package/src/views/linechart.js
CHANGED
|
@@ -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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 };
|