@cnrs/hecate-views-charts 0.2.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 +661 -0
- package/README.md +15 -0
- package/package.json +134 -0
- package/src/components/index.js +1 -0
- package/src/components/sample.js +37 -0
- package/src/index.js +2 -0
- package/src/init.js +8 -0
- package/src/views/barchart.js +246 -0
- package/src/views/histogram.js +222 -0
- package/src/views/index.js +12 -0
- package/src/views/linechart.js +234 -0
- package/src/views/scatterplot.js +193 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as d3 from 'd3';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* LineChart web component.
|
|
6
|
+
* @see https://en.wikipedia.org/wiki/Line_chart
|
|
7
|
+
*/
|
|
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'];
|
|
22
|
+
}
|
|
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
|
|
54
|
+
}
|
|
55
|
+
|
|
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
|
+
|
|
65
|
+
this.displayChart(x_scale, y_scale, dataset, svg);
|
|
66
|
+
}
|
|
67
|
+
|
|
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;
|
|
76
|
+
}
|
|
77
|
+
|
|
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);
|
|
120
|
+
|
|
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)]);
|
|
148
|
+
|
|
149
|
+
return{
|
|
150
|
+
x_scale : x_scale,
|
|
151
|
+
y_scale : y_scale
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
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));
|
|
195
|
+
|
|
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
|
+
}
|
|
218
|
+
|
|
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
|
+
export { LineChart };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as d3 from 'd3';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scatterplot web component.
|
|
6
|
+
* @see https://en.wikipedia.org/wiki/Scatter_plot
|
|
7
|
+
* @see https://scottmurray.org/tutorials/d3/
|
|
8
|
+
*/
|
|
9
|
+
class ScatterPlot extends HTMLElement {
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this.attachShadow({ mode: 'open' });
|
|
14
|
+
this.hera = {};
|
|
15
|
+
this.settings = {};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Static property returning the array of attributes
|
|
19
|
+
* observed by the browser.
|
|
20
|
+
* @returns {Array<string>} Attribute names
|
|
21
|
+
*/
|
|
22
|
+
static get observedAttributes() {
|
|
23
|
+
return ['data-hera', 'data-settings'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get items() { return this.hera.items || []; }
|
|
27
|
+
get entities() { return this.hera.entities || []; }
|
|
28
|
+
get properties() { return this.hera.properties || []; }
|
|
29
|
+
|
|
30
|
+
get pidx() { return this.settings.x || 'x'; }
|
|
31
|
+
get pidy() { return this.settings.y || 'y'; }
|
|
32
|
+
get label() { return this.settings.label || 'label'; }
|
|
33
|
+
get color() { return this.settings.color || '#7b4c98'; }
|
|
34
|
+
|
|
35
|
+
get padding() {
|
|
36
|
+
return this.settings.padding
|
|
37
|
+
|| { top: 3, right: 0, bottom: 10, left: 30 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* This is called when an observed attribute is
|
|
42
|
+
* defined in the HTML or changed using Javascript.
|
|
43
|
+
* @param {!string} attribute - Attribute name
|
|
44
|
+
* @param old - Previous value for `attribute`
|
|
45
|
+
* @param now - Current value for `attribute`
|
|
46
|
+
*/
|
|
47
|
+
attributeChangedCallback(attribute, old, now) {
|
|
48
|
+
if (old === now) return; // don't bother if there is nothing new
|
|
49
|
+
this[attribute] = now;
|
|
50
|
+
// data-* attributes can only be strings, so we cannot deserialize JSON
|
|
51
|
+
// in them ; so we use plain object fields to store deserialized values
|
|
52
|
+
// we trim the prefix to get the field name, so 'data-x' becomes 'x'
|
|
53
|
+
const fieldName = attribute.split('-').slice(1).join('-');
|
|
54
|
+
this[fieldName] = JSON.parse(now);
|
|
55
|
+
// note that this does not defeat the purpose of using data-* attributes
|
|
56
|
+
// in the first place, as if a name clash should happen in the future,
|
|
57
|
+
// we could edit this class with no impact on the users code
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Called when the element is connected to a DOM.
|
|
62
|
+
* Runs any required rendering.
|
|
63
|
+
*/
|
|
64
|
+
connectedCallback() {
|
|
65
|
+
this.width = this.parentElement.clientWidth;
|
|
66
|
+
this.height = this.parentElement.clientHeight;
|
|
67
|
+
const svgWidth = this.width + this.padding.right + this.padding.left;
|
|
68
|
+
const svgHeight = this.height + this.padding.top + this.padding.bottom;
|
|
69
|
+
|
|
70
|
+
const container = document.createElement('div');
|
|
71
|
+
this.shadowRoot.appendChild(container);
|
|
72
|
+
const svg = d3.select(container)
|
|
73
|
+
.append('svg')
|
|
74
|
+
.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`)
|
|
75
|
+
;
|
|
76
|
+
const {vx, vy} = this.viewport;
|
|
77
|
+
|
|
78
|
+
// create a dot for each item in this.items
|
|
79
|
+
svg.selectAll('circle')
|
|
80
|
+
.data(this.items)
|
|
81
|
+
.enter()
|
|
82
|
+
.append('circle')
|
|
83
|
+
.attr('cx', (item) => vx(this.getItemX(item)))
|
|
84
|
+
.attr('cy', (item) => vy(this.getItemY(item)))
|
|
85
|
+
.attr('r', (item) => this.getItemRadius(item))
|
|
86
|
+
.attr('fill', (item) => this.getItemColor(item))
|
|
87
|
+
.append('title')
|
|
88
|
+
.text((item) => this.getItemLabel(item))
|
|
89
|
+
;
|
|
90
|
+
svg.selectAll('circle')
|
|
91
|
+
.on('click', (event) => console.log(event));
|
|
92
|
+
|
|
93
|
+
var xAxis = d3.axisBottom(vx).ticks(11);
|
|
94
|
+
var yAxis = d3.axisLeft(vy).ticks(2);
|
|
95
|
+
const labels = this.getAxesLabels();
|
|
96
|
+
svg.append('g')
|
|
97
|
+
.attr('transform', `translate(0,${this.height-this.padding.top})`)
|
|
98
|
+
.call(xAxis)
|
|
99
|
+
.call((g) => g.append('text')
|
|
100
|
+
.attr('x', this.width)
|
|
101
|
+
.attr('y', 0)
|
|
102
|
+
.attr('fill', 'black')
|
|
103
|
+
.attr('text-anchor', 'end')
|
|
104
|
+
.text(labels.y))
|
|
105
|
+
;
|
|
106
|
+
svg.append('g')
|
|
107
|
+
.attr('transform', `translate(${this.padding.left},0)`)
|
|
108
|
+
.call(yAxis)
|
|
109
|
+
.call((g) => g.append('text')
|
|
110
|
+
.attr('x', 0)
|
|
111
|
+
.attr('y', '1em')
|
|
112
|
+
.attr('fill', 'currentColor')
|
|
113
|
+
.attr('text-anchor', 'start')
|
|
114
|
+
.text(labels.x))
|
|
115
|
+
;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @callback scale
|
|
120
|
+
* @property {number} n - A number inside the scale's domain
|
|
121
|
+
* @see https://d3js.org/d3-scale
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* This object is a wrapper for two d3.scale objects, which can be used to
|
|
126
|
+
* convert coordinates from the graph space to this viewer viewport space.
|
|
127
|
+
* @typedef {Object} Viewport
|
|
128
|
+
* @property {scale} vx - A scale returning the x (horizontal) coordinate
|
|
129
|
+
* @property {scale} vy - A scale returning the y (vertical) coordinate
|
|
130
|
+
* @see https://d3js.org/d3-scale
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Property returning this element viewport.
|
|
135
|
+
* The viewport is composed of two scale functions, which can be used to
|
|
136
|
+
* convert x (resp. y) to a number between 0 and the width (resp. height)
|
|
137
|
+
* of the viewer.
|
|
138
|
+
* @returns {Viewport} This viewer current viewport
|
|
139
|
+
*/
|
|
140
|
+
get viewport() {
|
|
141
|
+
const xMax = d3.max(this.items, (item) => this.getItemX(item));
|
|
142
|
+
const yMax = d3.max(this.items, (item) => this.getItemY(item));
|
|
143
|
+
// a scale is a function, domain is its input and range is its output
|
|
144
|
+
const vx = d3.scaleLinear()
|
|
145
|
+
.domain([0, xMax])
|
|
146
|
+
.range([this.padding.left, this.width-this.padding.right]);
|
|
147
|
+
const vy = d3.scaleLinear()
|
|
148
|
+
.domain([0, yMax])
|
|
149
|
+
.range([this.height-this.padding.top, this.padding.bottom]);
|
|
150
|
+
// y is inverted because having the origin (0;0) of the scatter graph
|
|
151
|
+
// in the bottom left corner feels more natural than the top left origin
|
|
152
|
+
// which is standard in SVG.
|
|
153
|
+
return { vx, vy };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getItemX(item) {
|
|
157
|
+
return this.getItemMetadata(item, this.pidx);
|
|
158
|
+
}
|
|
159
|
+
getItemY(item) {
|
|
160
|
+
return this.getItemMetadata(item, this.pidy);
|
|
161
|
+
}
|
|
162
|
+
getItemRadius(item) {
|
|
163
|
+
return 5;
|
|
164
|
+
}
|
|
165
|
+
getItemColor(item) {
|
|
166
|
+
return this.color;
|
|
167
|
+
}
|
|
168
|
+
getItemLabel(item) {
|
|
169
|
+
const name = this.getItemMetadata(item, this.label);
|
|
170
|
+
return name ? name : `Missing property '${this.label}'`;
|
|
171
|
+
}
|
|
172
|
+
getAxesLabels() {
|
|
173
|
+
const px = this.getProperty(this.pidx);
|
|
174
|
+
const py = this.getProperty(this.pidy);
|
|
175
|
+
return {
|
|
176
|
+
x: px ? px.name : `Missing property '${this.pidx}'`,
|
|
177
|
+
y: py ? py.name : `Missing property '${this.pidy}'`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getItemMetadata(item, pid) {
|
|
182
|
+
const found = item.metadata.find((m) => m.pid === pid);
|
|
183
|
+
return found ? found.value : null;
|
|
184
|
+
}
|
|
185
|
+
getProperty(pid) {
|
|
186
|
+
const found = this.properties.find((p) => p['@id'] === pid);
|
|
187
|
+
return found ? found : null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
export { ScatterPlot };
|