@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.
@@ -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 };