@cnrs/hecate-views-charts 0.4.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/package.json +1 -1
- package/src/init.js +1 -1
- package/src/util.js +13 -0
- package/src/views/all.js +2 -1
- package/src/views/cartesianchart.js +23 -17
- package/src/views/index.js +1 -1
- package/src/views/linechart.js +73 -174
- package/src/views/scatterchart.js +87 -0
- package/src/views/stackedareachart.js +12 -12
- package/src/views/scatterplot.js +0 -180
- package/src/views/stackedchart.js +0 -0
package/package.json
CHANGED
package/src/init.js
CHANGED
package/src/util.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function getMetadata(item, pid) {
|
|
2
|
+
for (const meta of item['metadata']) {
|
|
3
|
+
if (pid != null && meta['pid'] == pid) return meta['value'];
|
|
4
|
+
}
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
function getProperty(db, pid) { // TODO why can't I use await db.getProperty?
|
|
8
|
+
// faster than return db.properties.filter(e => e.id == pid)[0];
|
|
9
|
+
// we use find(...), which stops as soon as it finds our special something
|
|
10
|
+
return db.properties.find(e => e.id == pid);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { getProperty, getMetadata, };
|
package/src/views/all.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { View } from '@cnrs/hecate-views';
|
|
2
2
|
import * as d3 from 'd3';
|
|
3
|
+
import { getProperty, getMetadata, } from '../util.js';
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
|
|
@@ -17,7 +18,18 @@ class CartesianChart extends View {
|
|
|
17
18
|
|
|
18
19
|
getItemX(item) { return getMetadata(item, this.settings.x?.pid); }
|
|
19
20
|
getItemY(item) { return getMetadata(item, this.settings.y?.pid); }
|
|
20
|
-
|
|
21
|
+
getItemColor(item) { return getMetadata(item, this.settings.color?.pid); }
|
|
22
|
+
|
|
23
|
+
plotX(item, plot, parsers) {
|
|
24
|
+
return plot.vx(parsers.x(this.getItemX(item))) + plot.padding.left;
|
|
25
|
+
}
|
|
26
|
+
plotY(item, plot, parsers) {
|
|
27
|
+
return plot.vy(parsers.y(this.getItemY(item))) + plot.padding.top;
|
|
28
|
+
}
|
|
29
|
+
plotColor(item, plot, parsers) {
|
|
30
|
+
const color = plot.color(parsers.color(this.getItemColor(item)));
|
|
31
|
+
return color ? color : this.settings.color?.value;
|
|
32
|
+
}
|
|
21
33
|
|
|
22
34
|
renderView() {
|
|
23
35
|
// cleanup
|
|
@@ -31,7 +43,7 @@ class CartesianChart extends View {
|
|
|
31
43
|
|
|
32
44
|
const xProperty = getProperty(this.db, this.settings.x?.pid);
|
|
33
45
|
const yProperty = getProperty(this.db, this.settings.y?.pid);
|
|
34
|
-
const cProperty = getProperty(this.db, this.settings.
|
|
46
|
+
const cProperty = getProperty(this.db, this.settings.color?.pid);
|
|
35
47
|
const xType = xProperty?.type || 'text';
|
|
36
48
|
const yType = yProperty?.type || 'text';
|
|
37
49
|
const cType = cProperty?.type || 'text';
|
|
@@ -90,8 +102,8 @@ class CartesianChart extends View {
|
|
|
90
102
|
prepare(parsers, scales) { }
|
|
91
103
|
|
|
92
104
|
renderData(svg, plot, parsers) {
|
|
93
|
-
//const { x, y,
|
|
94
|
-
const message = //([x, y,
|
|
105
|
+
//const { x, y, color } = this.settings;
|
|
106
|
+
const message = //([x, y, color].every(v => v !== undefined)) ?
|
|
95
107
|
`${this.constructor.name} should implement renderData()`
|
|
96
108
|
// : "Missing settings"
|
|
97
109
|
;
|
|
@@ -135,15 +147,19 @@ class CartesianChart extends View {
|
|
|
135
147
|
}
|
|
136
148
|
createColorScale(parsers, scale) {
|
|
137
149
|
const cats = new Set();
|
|
138
|
-
for (const item of this.db.items) cats.add(this.
|
|
150
|
+
for (const item of this.db.items) cats.add(this.getItemColor(item));
|
|
139
151
|
const categories = [...cats]; // Set -> Array
|
|
140
152
|
const domain = this.createColorDomain(categories);
|
|
141
153
|
const range = this.createColorRange();
|
|
142
154
|
return scale().domain(domain).range(range);
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
createXDomain(parsers) {
|
|
146
|
-
|
|
157
|
+
createXDomain(parsers) {
|
|
158
|
+
return d3.extent(this.db.items, item => parsers.x(this.getItemX(item)));
|
|
159
|
+
}
|
|
160
|
+
createYDomain(parsers) {
|
|
161
|
+
return d3.extent(this.db.items, item => parsers.y(this.getItemY(item)));
|
|
162
|
+
}
|
|
147
163
|
createColorDomain(categories) { return categories; }
|
|
148
164
|
createXRange(plot) { return this.vertical ? [plot.height, 0] : [0, plot.width]; }
|
|
149
165
|
createYRange(plot) { return this.vertical ? [0, plot.width] : [plot.height, 0]; }
|
|
@@ -275,16 +291,6 @@ const SCALES = {
|
|
|
275
291
|
function getParser(type) { return PARSERS[type]; }
|
|
276
292
|
function getScale(type) { return SCALES[type]; }
|
|
277
293
|
|
|
278
|
-
function getMetadata(item, pid) {
|
|
279
|
-
for (const meta of item['metadata']) {
|
|
280
|
-
if (pid != null && meta['pid'] == pid) return meta['value'];
|
|
281
|
-
}
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
function getProperty(db, pid) { // TODO why can't I use await db.getProperty?
|
|
285
|
-
return db.properties.filter(e => e.id == pid)[0];
|
|
286
|
-
}
|
|
287
|
-
|
|
288
294
|
export { CartesianChart };
|
|
289
295
|
|
|
290
296
|
/**
|
package/src/views/index.js
CHANGED
package/src/views/linechart.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CartesianChart } from './cartesianchart.js';
|
|
2
2
|
import * as d3 from 'd3';
|
|
3
3
|
|
|
4
4
|
|
|
@@ -7,192 +7,91 @@ import * as d3 from 'd3';
|
|
|
7
7
|
* LineChart web component.
|
|
8
8
|
* @see https://en.wikipedia.org/wiki/Line_chart
|
|
9
9
|
*/
|
|
10
|
-
class LineChart extends
|
|
11
|
-
|
|
12
|
-
get
|
|
13
|
-
get
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
|
24
31
|
}
|
|
25
|
-
|
|
26
|
-
var dataset = this.prepareData();
|
|
27
|
-
|
|
28
|
-
var {sceneWidth, sceneHeight, svg} = this.createscene(this.padding, 600, 1200);
|
|
29
|
-
|
|
30
|
-
var {x_scale, y_scale} = this.createAxisScales(sceneHeight, sceneWidth, dataset)
|
|
31
|
-
this.addAxises(sceneWidth, sceneHeight, this.padding, x_scale, y_scale, svg);
|
|
32
|
-
|
|
33
|
-
this.displayChart(x_scale, y_scale, dataset, svg);
|
|
32
|
+
this._items_by_c = map;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/**
|
|
37
|
-
* Prepare the data by giving it the correct format
|
|
38
|
-
* Creates a javascript object that contains the elements of metadata, each key representing a different pid
|
|
39
|
-
* @param {Hera} data - Hera-formatted data parameter
|
|
40
|
-
* @param {Object} settings - Settings parameter
|
|
41
|
-
* @returns {Object} JS object containing the elements of metadata,
|
|
42
|
-
* x (resp. y) key contains the value for x (resp. y) axis
|
|
43
|
-
*/
|
|
44
|
-
prepareData() {
|
|
45
|
-
var yValues = [];
|
|
46
|
-
var xValues = [];
|
|
47
|
-
this.db.items.forEach(item => {
|
|
48
|
-
item.metadata.forEach(meta => {
|
|
49
|
-
if (meta.pid === this.pidy) {
|
|
50
|
-
yValues.push(parseInt(meta.value));
|
|
51
|
-
}
|
|
52
|
-
if (meta.pid === this.pidx){
|
|
53
|
-
xValues.push(parseInt(meta.value));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
const dataset = xValues.map((x, index) => ({
|
|
58
|
-
x: x,
|
|
59
|
-
y: yValues[index]
|
|
60
|
-
}));
|
|
61
|
-
return dataset;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Creates a svg element that will contain the viewer.
|
|
66
|
-
* @param {Object} margin - Space for the axes
|
|
67
|
-
* @param {number} width - Desired int width for the svg
|
|
68
|
-
* @param {number} height - Desired int height for the svg
|
|
69
|
-
* @returns The svg and its height, width and the width of each bars
|
|
70
|
-
*/
|
|
71
|
-
createscene(margin, height, width){
|
|
72
|
-
const lineWidth = width - margin.left - margin.right;
|
|
73
|
-
const lineHeight = height - margin.top - margin.bottom;
|
|
74
|
-
|
|
75
|
-
// create svg and append it
|
|
76
|
-
const divElem = document.createElement('div');
|
|
77
|
-
this.viewRoot.appendChild(divElem);
|
|
78
|
-
const container = d3.select(divElem);
|
|
79
35
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
sceneWidth : lineWidth,
|
|
87
|
-
sceneHeight : lineHeight,
|
|
88
|
-
svg : svg
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* creates and sets x and y linear scales
|
|
94
|
-
* @param {*} sceneHeight
|
|
95
|
-
* @param {*} sceneWidth
|
|
96
|
-
* @param {*} dataset
|
|
97
|
-
* @return x and y linear scales
|
|
98
|
-
*/
|
|
99
|
-
createAxisScales(sceneHeight, sceneWidth, dataset){
|
|
100
|
-
//set x and y scales
|
|
101
|
-
var x_scale = d3.scaleLinear()
|
|
102
|
-
.range([0,sceneWidth])
|
|
103
|
-
.domain([0,d3.max(dataset, d => d.x)]);
|
|
104
|
-
var y_scale = d3.scaleLinear()
|
|
105
|
-
.range([sceneHeight,0])
|
|
106
|
-
.domain([0,d3.max(dataset, d => d.y)]);
|
|
107
|
-
|
|
108
|
-
return{
|
|
109
|
-
x_scale : x_scale,
|
|
110
|
-
y_scale : y_scale
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* add the left and bottom axises, with their Label
|
|
118
|
-
* @param sceneWidth - Scene width
|
|
119
|
-
* @param x_scale - Linear scale for the X axis
|
|
120
|
-
* @param y_scale - Linear scale for the Y axis
|
|
121
|
-
* @param {*} sceneHeight
|
|
122
|
-
* @param {*} sceneWidth
|
|
123
|
-
* @param {*} margin
|
|
124
|
-
* @param {*} svg
|
|
125
|
-
*/
|
|
126
|
-
addAxises(sceneWidth, sceneHeight, margin, x_scale, y_scale, svg)
|
|
127
|
-
{
|
|
128
|
-
//add axises
|
|
129
|
-
var x_axis = d3.axisBottom(x_scale);
|
|
130
|
-
var y_axis = d3.axisLeft(y_scale);
|
|
131
|
-
const labels = this.getAxesLabels();
|
|
132
|
-
|
|
133
|
-
//x axis
|
|
134
|
-
svg.append("g")
|
|
135
|
-
.attr("transform", `translate(0,${sceneHeight})`)
|
|
136
|
-
.call(x_axis)
|
|
137
|
-
.call((g) => g.append("text")
|
|
138
|
-
.attr("x", sceneWidth)
|
|
139
|
-
.attr("y", margin.bottom - 4)
|
|
140
|
-
.attr("fill", "currentColor")
|
|
141
|
-
.attr("text-anchor", "end")
|
|
142
|
-
.text(labels.x));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//y axis
|
|
146
|
-
svg.append("g")
|
|
147
|
-
.call(y_axis)
|
|
148
|
-
.call((g) => g.append("text")
|
|
149
|
-
.attr("x", -margin.left)
|
|
150
|
-
.attr("y",-10)
|
|
151
|
-
.attr("fill", "currentColor")
|
|
152
|
-
.attr("text-anchor", "start")
|
|
153
|
-
.text(labels.y));
|
|
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
|
+
;
|
|
154
42
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
* @param {*} svg
|
|
162
|
-
*/
|
|
163
|
-
displayChart(x_scale, y_scale, dataset, svg){
|
|
164
|
-
//draw lines
|
|
165
|
-
const line = d3.line()
|
|
166
|
-
.x(d => x_scale(d.x))
|
|
167
|
-
.y(d => y_scale(d.y));
|
|
168
|
-
|
|
169
|
-
//add line path to svg
|
|
170
|
-
svg.append("path")
|
|
171
|
-
.datum(dataset)
|
|
172
|
-
.attr("fill", "none")
|
|
173
|
-
.attr("stroke", this.color)
|
|
174
|
-
.attr("stroke-width", 1)
|
|
175
|
-
.attr("d", line);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
getAxesLabels() {
|
|
179
|
-
const px = this.getProperty(this.pidx);
|
|
180
|
-
const py = this.getProperty(this.pidy);
|
|
181
|
-
return {
|
|
182
|
-
x: px ? px.name : `Missing property '${this.pidx}'`,
|
|
183
|
-
y: py ? py.name : `Missing property '${this.pidy}'`,
|
|
184
|
-
};
|
|
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
|
+
}
|
|
185
49
|
}
|
|
186
50
|
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
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
|
+
});
|
|
190
80
|
}
|
|
191
81
|
|
|
192
82
|
}
|
|
193
83
|
|
|
194
84
|
|
|
195
85
|
|
|
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
|
+
};
|
|
94
|
+
|
|
196
95
|
customElements.define('hecate-linechart', LineChart);
|
|
197
96
|
|
|
198
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 };
|
|
@@ -46,25 +46,25 @@ class StackedAreaChart extends CartesianChart {
|
|
|
46
46
|
|
|
47
47
|
prepare(parsers, scales) {
|
|
48
48
|
// (1) compute pivot table:
|
|
49
|
-
//
|
|
50
|
-
// x1
|
|
51
|
-
// x2
|
|
52
|
-
// x3
|
|
53
|
-
// …
|
|
49
|
+
// c1 c2 c3 …
|
|
50
|
+
// x1 y11 y12 y13 …
|
|
51
|
+
// x2 y21 y32 y33 …
|
|
52
|
+
// x3 y31 y32 y33 …
|
|
53
|
+
// … … … … …
|
|
54
54
|
this._table_pivot = new Map();
|
|
55
55
|
for (const item of this.db.items) {
|
|
56
56
|
const x = asKey(parsers.x(this.getItemX(item)));
|
|
57
57
|
const y = parsers.y(this.getItemY(item));
|
|
58
|
-
const
|
|
58
|
+
const c = this.getItemColor(item);
|
|
59
59
|
if (!this._table_pivot.has(x)) this._table_pivot.set(x, new Map());
|
|
60
|
-
this._table_pivot.get(x).set(
|
|
60
|
+
this._table_pivot.get(x).set(c, y);
|
|
61
61
|
}
|
|
62
|
-
// (2) compute categories
|
|
63
|
-
// [
|
|
62
|
+
// (2) compute colors (ie. categories)
|
|
63
|
+
// [ c1, c2, c3, … ]
|
|
64
64
|
const cats = new Set();
|
|
65
65
|
for (const [, values] of this._table_pivot) {
|
|
66
|
-
for (const
|
|
67
|
-
cats.add(
|
|
66
|
+
for (const c of values.keys()) {
|
|
67
|
+
cats.add(c);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
this._categories = [...cats];
|
|
@@ -74,7 +74,7 @@ class StackedAreaChart extends CartesianChart {
|
|
|
74
74
|
.offset(this.createStackOffset())
|
|
75
75
|
.order(this.createStackOrder())
|
|
76
76
|
.keys(this._categories)
|
|
77
|
-
.value(([, values],
|
|
77
|
+
.value(([, values], c) => values.get(c) ?? 0)(rows);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
createStackOffset() {
|
package/src/views/scatterplot.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { View } from '@cnrs/hecate-views';
|
|
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 ScatterPlot extends View {
|
|
12
|
-
|
|
13
|
-
get pidx() { return this.settings.x || 'x'; }
|
|
14
|
-
get pidy() { return this.settings.y || 'y'; }
|
|
15
|
-
get pidlabel() { return this.settings.label || 'label'; }
|
|
16
|
-
|
|
17
|
-
get padding() {
|
|
18
|
-
return this.settings.padding
|
|
19
|
-
|| { top: 3, right: 0, bottom: 10, left: 30 };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
renderView() {
|
|
23
|
-
// cleanup
|
|
24
|
-
while (this.viewRoot.lastChild.name !== 'view') { // <slot />
|
|
25
|
-
this.viewRoot.removeChild(this.viewRoot.lastChild);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
this.width = this.parentElement.clientWidth;
|
|
29
|
-
this.height = this.parentElement.clientHeight;
|
|
30
|
-
const svgWidth = this.width + this.padding.right + this.padding.left;
|
|
31
|
-
const svgHeight = this.height + this.padding.top + this.padding.bottom;
|
|
32
|
-
|
|
33
|
-
/* TODO
|
|
34
|
-
<svg role="img">
|
|
35
|
-
<title>Weight by age scatter plot</title>
|
|
36
|
-
<desc>
|
|
37
|
-
Scatter plot showing weight distribution by age.
|
|
38
|
-
X axis ranges from 0 to 100 years.
|
|
39
|
-
Y axis ranges from 0 to 200 kilograms.
|
|
40
|
-
</desc>
|
|
41
|
-
</svg>
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
const container = document.createElement('div');
|
|
45
|
-
this.viewRoot.appendChild(container);
|
|
46
|
-
const svg = d3.select(container)
|
|
47
|
-
.append('svg')
|
|
48
|
-
.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`)
|
|
49
|
-
;
|
|
50
|
-
const viewport = this.viewport;
|
|
51
|
-
|
|
52
|
-
// create a dot for each item in this.items
|
|
53
|
-
svg.selectAll('circle')
|
|
54
|
-
.data(this.db.items)
|
|
55
|
-
.enter()
|
|
56
|
-
.append('circle')
|
|
57
|
-
.attr('cx', (item) => viewport.vx(this.getItemX(item)))
|
|
58
|
-
.attr('cy', (item) => viewport.vy(this.getItemY(item)))
|
|
59
|
-
.attr('r', (item) => this.getItemRadius(item))
|
|
60
|
-
.attr('fill', (item) => this.getItemColor(item))
|
|
61
|
-
.append('title')
|
|
62
|
-
.text((item) => this.getItemLabel(item))
|
|
63
|
-
;
|
|
64
|
-
svg.selectAll('circle')
|
|
65
|
-
.on('click', (event) => console.log(event));
|
|
66
|
-
|
|
67
|
-
this.addAxes(svg, viewport);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
addAxes(svg, viewport) {
|
|
72
|
-
const labels = this.getAxesLabels();
|
|
73
|
-
this.addXAxis(svg, viewport, labels.y);
|
|
74
|
-
this.addYAxis(svg, viewport, labels.x);
|
|
75
|
-
}
|
|
76
|
-
addXAxis(svg, viewport, label) {
|
|
77
|
-
var xAxis = d3.axisBottom(viewport.vx).ticks(11);
|
|
78
|
-
svg.append('g')
|
|
79
|
-
.attr('transform', `translate(0,${this.height-this.padding.top})`)
|
|
80
|
-
.call(xAxis)
|
|
81
|
-
.call((g) => g.append('text')
|
|
82
|
-
.attr('x', this.width)
|
|
83
|
-
.attr('y', 0)
|
|
84
|
-
.attr('fill', 'black')
|
|
85
|
-
.attr('text-anchor', 'end')
|
|
86
|
-
.text(label))
|
|
87
|
-
;
|
|
88
|
-
}
|
|
89
|
-
addYAxis(svg, viewport, label) {
|
|
90
|
-
var yAxis = d3.axisLeft(viewport.vy).ticks(2);
|
|
91
|
-
svg.append('g')
|
|
92
|
-
.attr('transform', `translate(${this.padding.left},0)`)
|
|
93
|
-
.call(yAxis)
|
|
94
|
-
.call((g) => g.append('text')
|
|
95
|
-
.attr('x', 0)
|
|
96
|
-
.attr('y', '1em')
|
|
97
|
-
.attr('fill', 'currentColor')
|
|
98
|
-
.attr('text-anchor', 'start')
|
|
99
|
-
.text(label))
|
|
100
|
-
;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* @callback scale
|
|
105
|
-
* @property {number} n - A number inside the scale's domain
|
|
106
|
-
* @see https://d3js.org/d3-scale
|
|
107
|
-
*/
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* This object is a wrapper for two d3.scale objects, which can be used to
|
|
111
|
-
* convert coordinates from the graph space to this viewer viewport space.
|
|
112
|
-
* @typedef {Object} Viewport
|
|
113
|
-
* @property {scale} vx - A scale returning the x (horizontal) coordinate
|
|
114
|
-
* @property {scale} vy - A scale returning the y (vertical) coordinate
|
|
115
|
-
* @see https://d3js.org/d3-scale
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Property returning this element viewport.
|
|
120
|
-
* The viewport is composed of two scale functions, which can be used to
|
|
121
|
-
* convert x (resp. y) to a number between 0 and the width (resp. height)
|
|
122
|
-
* of the viewer.
|
|
123
|
-
* @returns {Viewport} This viewer current viewport
|
|
124
|
-
*/
|
|
125
|
-
get viewport() {
|
|
126
|
-
const xMax = d3.max(this.db.items, (item) => this.getItemX(item));
|
|
127
|
-
const yMax = d3.max(this.db.items, (item) => this.getItemY(item));
|
|
128
|
-
// a scale is a function, domain is its input and range is its output
|
|
129
|
-
const vx = d3.scaleLinear()
|
|
130
|
-
.domain([0, xMax])
|
|
131
|
-
.range([this.padding.left, this.width-this.padding.right]);
|
|
132
|
-
const vy = d3.scaleLinear()
|
|
133
|
-
.domain([0, yMax])
|
|
134
|
-
.range([this.height-this.padding.top, this.padding.bottom]);
|
|
135
|
-
// y is inverted because having the origin (0;0) of the scatter graph
|
|
136
|
-
// in the bottom left corner feels more natural than the top left origin
|
|
137
|
-
// which is standard in SVG.
|
|
138
|
-
return { vx, vy };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
getItemX(item) {
|
|
142
|
-
return this.getItemMetadata(item, this.pidx);
|
|
143
|
-
}
|
|
144
|
-
getItemY(item) {
|
|
145
|
-
return this.getItemMetadata(item, this.pidy);
|
|
146
|
-
}
|
|
147
|
-
getItemRadius(item) { // eslint-disable-line no-unused-vars
|
|
148
|
-
return 5;
|
|
149
|
-
}
|
|
150
|
-
getItemColor(item) { // eslint-disable-line no-unused-vars
|
|
151
|
-
return this.color;
|
|
152
|
-
}
|
|
153
|
-
getItemLabel(item) {
|
|
154
|
-
const name = this.getItemMetadata(item, this.pidlabel);
|
|
155
|
-
return name ? name : `Missing property '${this.pidlabel}'`;
|
|
156
|
-
}
|
|
157
|
-
getAxesLabels() {
|
|
158
|
-
const px = this.getProperty(this.pidx);
|
|
159
|
-
const py = this.getProperty(this.pidy);
|
|
160
|
-
return {
|
|
161
|
-
x: px ? px.name : `Missing property '${this.pidx}'`,
|
|
162
|
-
y: py ? py.name : `Missing property '${this.pidy}'`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
getItemMetadata(item, pid) {
|
|
167
|
-
const found = item.metadata.find((m) => m.pid === pid);
|
|
168
|
-
return found ? found.value : null;
|
|
169
|
-
}
|
|
170
|
-
getProperty(pid) {
|
|
171
|
-
const found = this.db.properties.find((p) => p['id'] === pid);
|
|
172
|
-
return found ? found : null;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
customElements.define('hecate-scatterplot', ScatterPlot);
|
|
179
|
-
|
|
180
|
-
export { ScatterPlot };
|
|
File without changes
|