@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
package/package.json
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@comment comments": [
|
|
3
|
+
"To track and understand the whys of each part of this package.json, ",
|
|
4
|
+
"in particular dependencies, please explain when you modify this file.",
|
|
5
|
+
"Please include the name of the section you comment after the `@comment`",
|
|
6
|
+
"'annotation' to make sure each key is unique (see the JSON spec).",
|
|
7
|
+
"This is required by JSON validators (like the one built into your IDE).",
|
|
8
|
+
"Plus, running `npm install <whatever> --save` removes duplicate keys.",
|
|
9
|
+
"",
|
|
10
|
+
"package.json documentation: https://docs.npmjs.com/files/package.json"
|
|
11
|
+
],
|
|
12
|
+
"name": "@cnrs/hecate-views-charts",
|
|
13
|
+
"description": "Views web components available to any web app.",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dataviz",
|
|
16
|
+
"research",
|
|
17
|
+
"database",
|
|
18
|
+
"visualization",
|
|
19
|
+
"scientific",
|
|
20
|
+
"javascript",
|
|
21
|
+
"browser",
|
|
22
|
+
"library"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"module": "src/index.js",
|
|
26
|
+
"main": "src/index.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.js",
|
|
29
|
+
"default": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src/**/*.js"
|
|
33
|
+
],
|
|
34
|
+
"version": "0.2.0",
|
|
35
|
+
"license": "AGPL",
|
|
36
|
+
"homepage": "https://datasphere.readthedocs.io/projects/hecate/",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://gitlab.huma-num.fr/datasphere/hecate/views/charts.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://gitlab.huma-num.fr/datasphere/hecate/views/charts/-/issues"
|
|
43
|
+
},
|
|
44
|
+
"main": "src/index.js",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"d3": "latest"
|
|
47
|
+
},
|
|
48
|
+
"@comment devDependencies": {
|
|
49
|
+
"vite": [
|
|
50
|
+
"Bundler for javascript project combines many js source files into a ",
|
|
51
|
+
"single one that is production-ready loadable in the browser.",
|
|
52
|
+
"- ViteJS website: https://vitejs.dev/"
|
|
53
|
+
],
|
|
54
|
+
"vitest": [
|
|
55
|
+
"More or less 'all in one' unit testing framework",
|
|
56
|
+
"@requires vite ≥ 3.0.0 (and node ≥ 14.18)",
|
|
57
|
+
"- Vitest website: https://vitest.dev/"
|
|
58
|
+
],
|
|
59
|
+
"@vitest/coverage-c8": [
|
|
60
|
+
"Compute percentage of code covered by tests.",
|
|
61
|
+
"- Vitest coverage documentation: https://vitest.dev/guide/coverage.html"
|
|
62
|
+
],
|
|
63
|
+
"happy-dom": [
|
|
64
|
+
"By default, tests framework don't have any access to the DOM.",
|
|
65
|
+
"This packages simulates a DOM in the context of tests.",
|
|
66
|
+
"- https://www.npmjs.com/package/happy-dom"
|
|
67
|
+
],
|
|
68
|
+
"eslint": [
|
|
69
|
+
"To use eslint ECMAScript linter.",
|
|
70
|
+
"- ESLint documentation: https://eslint.org/"
|
|
71
|
+
],
|
|
72
|
+
"jsdoc": [
|
|
73
|
+
"Javascript documentation generation tool.",
|
|
74
|
+
"See `gendoc` script in this same file for some usage details.",
|
|
75
|
+
"- JSDoc documentation: https://jsdoc.app/"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"vite": "latest",
|
|
80
|
+
"vitest": "latest",
|
|
81
|
+
"@vitest/coverage-v8": "latest",
|
|
82
|
+
"happy-dom": "<=12.1.5",
|
|
83
|
+
"eslint": "latest",
|
|
84
|
+
"jsdoc": "latest"
|
|
85
|
+
},
|
|
86
|
+
"@comment scripts": {
|
|
87
|
+
"dev": [
|
|
88
|
+
"Starts development server in the local folder: this allows us devs to ",
|
|
89
|
+
"rapidly see the impacts of our modifications, with no page refresh.",
|
|
90
|
+
"@requires vite (dev dependency)"
|
|
91
|
+
],
|
|
92
|
+
"test": [
|
|
93
|
+
"Runs every unit test once, then exits.",
|
|
94
|
+
"@requires vitest (dev dependency)"
|
|
95
|
+
],
|
|
96
|
+
"test:coverage": [
|
|
97
|
+
"Runs every unit test once, with coverage percentage.",
|
|
98
|
+
"@requires vitest @vitest/coverage-v8 (dev dependencies)"
|
|
99
|
+
],
|
|
100
|
+
"test:watch": [
|
|
101
|
+
"Runs every unit test with coverage %, watching for modifications.",
|
|
102
|
+
"@requires vitest @vitest/coverage-v8 (dev dependencies)"
|
|
103
|
+
],
|
|
104
|
+
"build": [
|
|
105
|
+
"Bundles project for production in `dist` folder.",
|
|
106
|
+
"@requires vite (dev dependency)"
|
|
107
|
+
],
|
|
108
|
+
"lint": [
|
|
109
|
+
"Checks code style using ESLint. note: Argument pattern is surrounded with ",
|
|
110
|
+
"double quotes, so its resolution doesn't depend on runtime environment.",
|
|
111
|
+
"@requires eslint (dev dependency)",
|
|
112
|
+
"- ESLint configuration file is `eslint.config.mjs`",
|
|
113
|
+
"- ESLint documentation: https://eslint.org/"
|
|
114
|
+
],
|
|
115
|
+
"doc:html": [
|
|
116
|
+
"Generates developper documentation using JSDoc.",
|
|
117
|
+
"Documentation for each module/class/method is written",
|
|
118
|
+
"in docstring format, directly inside each source file.",
|
|
119
|
+
"@requires jsdoc (dev dependency)",
|
|
120
|
+
"- JSDoc configuration file is `.jsdoc.conf.json`.",
|
|
121
|
+
"- JSDoc config file spec: https://jsdoc.app/about-configuring-jsdoc.html",
|
|
122
|
+
"- JSDoc repo: https://github.com/jsdoc/jsdoc"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
"scripts": {
|
|
126
|
+
"dev": "vite",
|
|
127
|
+
"test": "vitest run",
|
|
128
|
+
"test:coverage": "vitest run --coverage",
|
|
129
|
+
"test:watch": "vitest --coverage",
|
|
130
|
+
"build": "vite build",
|
|
131
|
+
"lint": "eslint \"src/**/*.js\"",
|
|
132
|
+
"doc:html": "./node_modules/jsdoc/jsdoc.js -c .jsdoc.conf.json"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Sample } from './sample.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample web component.
|
|
3
|
+
* It just displays a string.
|
|
4
|
+
*/
|
|
5
|
+
class Sample extends HTMLElement {
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.attachShadow({ mode: 'open' });
|
|
10
|
+
this.data = "placeholder";
|
|
11
|
+
}
|
|
12
|
+
/** Static property returning an array of attributes observed by the browser
|
|
13
|
+
*/
|
|
14
|
+
static get observedAttributes() {
|
|
15
|
+
return ['data'];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* This is called when an observed attribute is
|
|
19
|
+
* defined in the HTML or changed using Javascript.
|
|
20
|
+
*/
|
|
21
|
+
attributeChangedCallback(property, old, now) {
|
|
22
|
+
if (old === now) return; // don't bother to render if there is nothing new
|
|
23
|
+
this[property] = now;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Called when the element is connected to a DOM.
|
|
28
|
+
* Runs any required rendering.
|
|
29
|
+
*/
|
|
30
|
+
connectedCallback() {
|
|
31
|
+
this.shadowRoot.innerHTML += `<p>${this.data}</p>`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export { Sample };
|
package/src/index.js
ADDED
package/src/init.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Sample, } from './components/';
|
|
2
|
+
import { BarChart, Histogram, ScatterPlot, LineChart } from './views/';
|
|
3
|
+
|
|
4
|
+
customElements.define('sample-component', Sample);
|
|
5
|
+
customElements.define('scatter-plot', ScatterPlot);
|
|
6
|
+
customElements.define('bar-chart', BarChart);
|
|
7
|
+
customElements.define('histogram-chart', Histogram);
|
|
8
|
+
customElements.define('line-chart', LineChart);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as d3 from 'd3';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BarChart web component.
|
|
6
|
+
* @see https://en.wikipedia.org/wiki/Bar_chart
|
|
7
|
+
*/
|
|
8
|
+
class BarChart extends HTMLElement{
|
|
9
|
+
|
|
10
|
+
constructor(){
|
|
11
|
+
super();
|
|
12
|
+
this.attachShadow({ mode: 'open' });
|
|
13
|
+
this.hera = {};
|
|
14
|
+
this.settings = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Static property returning an array of attributes observed by the browser
|
|
18
|
+
*/
|
|
19
|
+
static get observedAttributes() {
|
|
20
|
+
return ['data-hera', 'data-settings'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get items() { return this.hera.items || []; }
|
|
24
|
+
get entities() { return this.hera.entities || []; }
|
|
25
|
+
get properties() { return this.hera.properties || []; }
|
|
26
|
+
|
|
27
|
+
get pidx() { return this.settings.x || 'x'; }
|
|
28
|
+
get pidy() { return this.settings.y || 'y'; }
|
|
29
|
+
get color() { return this.settings.color || '#7b4c98'; }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This is called when an observed attribute is
|
|
33
|
+
* defined in the HTML or changed using Javascript.
|
|
34
|
+
* @param {!string} attribute - Attribute name
|
|
35
|
+
* @param old - Previous value for `attribute`
|
|
36
|
+
* @param now - Current value for `attribute`
|
|
37
|
+
*/
|
|
38
|
+
attributeChangedCallback(attribute, old, now) {
|
|
39
|
+
if (old === now) return; // don't bother if there is nothing new
|
|
40
|
+
this[attribute] = now;
|
|
41
|
+
// data-* attributes can only be strings, so we cannot deserialize JSON
|
|
42
|
+
// in them ; so we use plain object fields to store deserialized values
|
|
43
|
+
// we trim the prefix to get the field name, so 'data-x' becomes 'x'
|
|
44
|
+
const fieldName = attribute.split('-').slice(1).join('-');
|
|
45
|
+
this[fieldName] = JSON.parse(now);
|
|
46
|
+
// note that this does not defeat the purpose of using data-* attributes
|
|
47
|
+
// in the first place, as if a name clash should happen in the future,
|
|
48
|
+
// we could edit this class with no impact on the users code
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//js method called everytime the component is added to the dom
|
|
52
|
+
connectedCallback(){
|
|
53
|
+
var dataset = this.prepareData(this.hera, this.settings);
|
|
54
|
+
|
|
55
|
+
//creation of scene
|
|
56
|
+
const margin = { top : 70, right : 30, bottom: 40, left : 80};
|
|
57
|
+
const {sceneWidth, sceneHeight, barWidth, svg} = this.createScene(500, 500, this.items.length, margin);
|
|
58
|
+
|
|
59
|
+
//addition of axises
|
|
60
|
+
var {y_scale, x_scale} = this.createAxisScales(sceneHeight, sceneWidth, dataset);
|
|
61
|
+
this.addAxises(x_scale, y_scale, sceneHeight, sceneWidth, margin, svg)
|
|
62
|
+
|
|
63
|
+
this.displayChart(sceneHeight, barWidth, dataset, y_scale, x_scale, svg);
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sets the shadow dom of the component, currently creates a single div
|
|
69
|
+
* @returns the element that was appended to the shadow dom
|
|
70
|
+
*/
|
|
71
|
+
createShadowDom() {
|
|
72
|
+
const e = document.createElement('div');
|
|
73
|
+
this.shadowRoot.appendChild(e);
|
|
74
|
+
return e;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Prepare the data by giving it the correct format
|
|
79
|
+
* @param {object} rawData - the data as it was passed , it's a js object type
|
|
80
|
+
* @param {object} rawSettings - the settings as it was passed, it's a js object type
|
|
81
|
+
* @returns js object that contains the elements of metadata, each key representing a different pid
|
|
82
|
+
*/
|
|
83
|
+
prepareData(rawData, rawSettings){
|
|
84
|
+
var barValues = [];
|
|
85
|
+
var barLabels = [];
|
|
86
|
+
rawData.items.forEach(item => {
|
|
87
|
+
item.metadata.forEach(meta => {
|
|
88
|
+
if (meta.pid === this.pidy) { // Y axis is height of bars
|
|
89
|
+
barValues.push(parseInt(meta.value));
|
|
90
|
+
}
|
|
91
|
+
if (meta.pid === this.pidx) { // X axis is "label for each bar"
|
|
92
|
+
barLabels.push(meta.value);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const dataset = barLabels.map((label, index) => ({
|
|
97
|
+
label: label,
|
|
98
|
+
value: barValues[index]
|
|
99
|
+
}));
|
|
100
|
+
return dataset;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* creates a svg element that will contain the viewer
|
|
105
|
+
* @param {number} width - desired width for the svg
|
|
106
|
+
* @param {number} height - desired height for the svg
|
|
107
|
+
* @param {number} dataLength - number of elements in data
|
|
108
|
+
* @param {object} margin - to leave space for the axises, a js object containing 4 margins
|
|
109
|
+
* @returns the svg and its height, width and the width of each bars
|
|
110
|
+
*/
|
|
111
|
+
createScene(width, height, dataLength, margin){
|
|
112
|
+
|
|
113
|
+
const sceneWidth = width - margin.left - margin.right; //to set barwidth, the key scenewith in the return is not recognized yet
|
|
114
|
+
const sceneHeight = height - margin.top - margin.bottom;
|
|
115
|
+
const barWidth = sceneWidth/dataLength
|
|
116
|
+
|
|
117
|
+
const divElem = this.createShadowDom();
|
|
118
|
+
const container = d3.select(divElem);
|
|
119
|
+
|
|
120
|
+
//addition of container to the svg
|
|
121
|
+
const svg = container.append("svg")
|
|
122
|
+
.attr("width", sceneWidth + margin.left + margin.right)
|
|
123
|
+
.attr("height", sceneHeight + margin.top + margin.bottom)
|
|
124
|
+
.append("g")
|
|
125
|
+
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
sceneWidth : sceneWidth,
|
|
129
|
+
sceneHeight : sceneHeight,
|
|
130
|
+
barWidth : barWidth,
|
|
131
|
+
svg : svg
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* create the x and y linear scales
|
|
137
|
+
* @param {number} sceneHeight
|
|
138
|
+
* @param {number} sceneWidth
|
|
139
|
+
* @param {object} dataset
|
|
140
|
+
* @returns y_scale and x_scales
|
|
141
|
+
*/
|
|
142
|
+
createAxisScales(sceneHeight, sceneWidth, dataset){
|
|
143
|
+
var y_scale = d3.scaleLinear()
|
|
144
|
+
.range([sceneHeight,0])
|
|
145
|
+
.domain([0,d3.max(dataset.map(function(d) { return d.value; }))]);
|
|
146
|
+
|
|
147
|
+
var x_scale = d3.scaleBand()
|
|
148
|
+
.range([ 0, sceneWidth ])
|
|
149
|
+
.domain(dataset.map(function(d) { return d.label; }))
|
|
150
|
+
.padding(0.2);
|
|
151
|
+
|
|
152
|
+
return{
|
|
153
|
+
y_scale : y_scale,
|
|
154
|
+
x_scale : x_scale
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* add the left and bottom axises, with their Label
|
|
160
|
+
* @param {map} x_scale - linear scale for the x axis
|
|
161
|
+
* @param {map} y_scale - linear scale for the y axis
|
|
162
|
+
* @param {number} sceneHeight - the height of the svg
|
|
163
|
+
* @param {number} sceneWidth - the width of the svg
|
|
164
|
+
* @param {object} margins
|
|
165
|
+
* @param {object} dataset
|
|
166
|
+
* @param {tag} svg
|
|
167
|
+
*/
|
|
168
|
+
addAxises(x_scale, y_scale, sceneHeight, sceneWidth, margins, svg){
|
|
169
|
+
var y_axis = d3.axisLeft(y_scale);
|
|
170
|
+
var x_axis = d3.axisBottom(x_scale);
|
|
171
|
+
const labels = this.getAxesLabels();
|
|
172
|
+
|
|
173
|
+
svg.append("g")
|
|
174
|
+
.call(y_axis)
|
|
175
|
+
/*TO DO : allow the user to customize the appearance of the axis
|
|
176
|
+
the line below removes the line of vertical axis*/
|
|
177
|
+
//.call((g) => g.select(".domain").remove())
|
|
178
|
+
.call((g) => g.append("text")
|
|
179
|
+
.attr("x", -margins.left)
|
|
180
|
+
.attr("y",-10)
|
|
181
|
+
.attr("fill", "currentColor")
|
|
182
|
+
.attr("text-anchor", "start")
|
|
183
|
+
.text(labels.y));
|
|
184
|
+
|
|
185
|
+
svg.append("g")
|
|
186
|
+
.attr("transform", `translate(0,${sceneHeight})`)
|
|
187
|
+
.call(x_axis)
|
|
188
|
+
.call((g) => g.append("text")
|
|
189
|
+
.attr("x", sceneWidth)
|
|
190
|
+
.attr("y", margins.bottom - 4)
|
|
191
|
+
.attr("fill", "currentColor")
|
|
192
|
+
.attr("text-anchor", "end")
|
|
193
|
+
.text(labels.x))
|
|
194
|
+
.selectAll("text");
|
|
195
|
+
//TO DO : this lines allows to rotate the text of the horizontal axis
|
|
196
|
+
//.attr("transform", `translate(-10,0)rotate(-45)`)
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* display the bar and puts it in the svg
|
|
202
|
+
* @param {number} sceneHeight - the height of the svg
|
|
203
|
+
* @param {number} barWidth - the width of each bars
|
|
204
|
+
* @param {object} dataset
|
|
205
|
+
* @param {map} y_scale
|
|
206
|
+
* @param {map} x_scale
|
|
207
|
+
* @param {*} svg
|
|
208
|
+
*/
|
|
209
|
+
displayChart(sceneHeight, barWidth, dataset, y_scale, x_scale, svg){
|
|
210
|
+
svg.selectAll("rect")
|
|
211
|
+
.data(dataset)
|
|
212
|
+
.enter()
|
|
213
|
+
.append("rect")
|
|
214
|
+
.attr("y", function(d) {
|
|
215
|
+
return y_scale(d.value);
|
|
216
|
+
})
|
|
217
|
+
.attr("x", function(d) {
|
|
218
|
+
return x_scale(d.label);
|
|
219
|
+
})
|
|
220
|
+
.attr("height", function(d) {
|
|
221
|
+
return sceneHeight - y_scale(d.value);
|
|
222
|
+
})
|
|
223
|
+
.attr("width", barWidth) //add bar distance in future?
|
|
224
|
+
|
|
225
|
+
.attr("fill", this.color)
|
|
226
|
+
.attr("stroke","black");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getAxesLabels() {
|
|
230
|
+
const px = this.getProperty(this.pidx);
|
|
231
|
+
const py = this.getProperty(this.pidy);
|
|
232
|
+
return {
|
|
233
|
+
x: px ? px.name : `Missing property '${this.pidx}'`,
|
|
234
|
+
y: py ? py.name : `Missing property '${this.pidy}'`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getProperty(pid) {
|
|
239
|
+
const found = this.properties.find((p) => p['@id'] === pid);
|
|
240
|
+
return found ? found : null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
export { BarChart };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as d3 from 'd3';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Histogram web component.
|
|
6
|
+
* @see https://en.wikipedia.org/wiki/Histogram
|
|
7
|
+
*/
|
|
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 || []; }
|
|
28
|
+
|
|
29
|
+
get pidx() { return this.settings.x || 'x'; }
|
|
30
|
+
get pidy() { return this.settings.y || 'y'; }
|
|
31
|
+
get color() { return this.settings.color || '#7b4c98'; }
|
|
32
|
+
|
|
33
|
+
get padding() {
|
|
34
|
+
return this.settings.padding
|
|
35
|
+
|| { top: 20, right: 20, bottom: 30, left: 40 };
|
|
36
|
+
}
|
|
37
|
+
|
|
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`
|
|
44
|
+
*/
|
|
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
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Called when the element is connected to a DOM.
|
|
60
|
+
* Runs any required rendering.
|
|
61
|
+
*/
|
|
62
|
+
connectedCallback(){
|
|
63
|
+
|
|
64
|
+
this.width = this.parentElement.clientWidth;
|
|
65
|
+
this.height = this.parentElement.clientHeight;
|
|
66
|
+
|
|
67
|
+
const container = document.createElement('div');
|
|
68
|
+
this.shadowRoot.appendChild(container);
|
|
69
|
+
|
|
70
|
+
// group items into "bins", so that scene creation is easier
|
|
71
|
+
const bins = this.prepareData();
|
|
72
|
+
// create viewport, ie. d3 "scale" functions for axes
|
|
73
|
+
const { vx, vy } = this.createViewport(bins);
|
|
74
|
+
// create a SVG container for the whole scene
|
|
75
|
+
const scene = d3.select(container).append('svg')
|
|
76
|
+
.attr('width', this.width)
|
|
77
|
+
.attr('height', this.height)
|
|
78
|
+
.attr('viewBox', [0, 0, this.width, this.height])
|
|
79
|
+
.attr('style', 'max-width: 100%; height: auto;')
|
|
80
|
+
;
|
|
81
|
+
// create callback for visual feedback when hovering on each histogram bar
|
|
82
|
+
const { mouseover, mouseleave } = this.createHoverCallbacks();
|
|
83
|
+
// create histogram bars
|
|
84
|
+
scene.append('g')
|
|
85
|
+
.attr('fill', this.color)
|
|
86
|
+
.selectAll()
|
|
87
|
+
.data(bins)
|
|
88
|
+
.join('rect')
|
|
89
|
+
.attr('x', (d) => vx(d.x0) + 1)
|
|
90
|
+
.attr('width', (d) => vx(d.x1) - vx(d.x0) - 1)
|
|
91
|
+
.attr('y', (d) => vy(d.length))
|
|
92
|
+
.attr('height', (d) => vy(0) - vy(d.length))
|
|
93
|
+
.on('mouseover', mouseover)
|
|
94
|
+
.on('mouseleave', mouseleave)
|
|
95
|
+
.append('title')
|
|
96
|
+
.text((bin) => this.getBinLabel(bin))
|
|
97
|
+
;
|
|
98
|
+
// add X axis and Y axis to scene
|
|
99
|
+
this.addAxes(scene, vx, vy);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Prepare the data by organize so that scene creation is simpler.
|
|
104
|
+
* For this, we need to turn quantitative values into consecutive,
|
|
105
|
+
* non-overlapping intervals called "bins", which we can in turn use
|
|
106
|
+
* to group values in nice histogram bars.
|
|
107
|
+
* @return Bin generator
|
|
108
|
+
* @see https://d3js.org/d3-array/bin
|
|
109
|
+
*/
|
|
110
|
+
prepareData() {
|
|
111
|
+
var barValues = [];
|
|
112
|
+
var classValues = [];
|
|
113
|
+
this.items.forEach(item => {
|
|
114
|
+
item.metadata.forEach(meta => {
|
|
115
|
+
if (meta.pid === this.pidx) {
|
|
116
|
+
barValues.push(parseInt(meta.value));
|
|
117
|
+
}
|
|
118
|
+
if (meta.pid === this.pidy){
|
|
119
|
+
classValues.push(meta.value);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
const dataset = classValues.map((label, index) => ({
|
|
124
|
+
class: label,
|
|
125
|
+
value: barValues[index],
|
|
126
|
+
}));
|
|
127
|
+
// Now, "bin" the dataset:
|
|
128
|
+
const bins = d3.bin()
|
|
129
|
+
//.thresholds(40)
|
|
130
|
+
// if thresholds are specified as a number, the domain will be uniformly
|
|
131
|
+
// divided into approximately that many bins: so if .thresholds(40) is
|
|
132
|
+
// called, the resulting Histogram will have _appoximately_ 40 bars;
|
|
133
|
+
// if thresholds is not specified, the current threshold generator will
|
|
134
|
+
// be used, and by default it implements Sturges’ formula
|
|
135
|
+
.value((d) => d.value)(dataset);
|
|
136
|
+
return bins;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates callbacks for changing each bar color when hovering onto.
|
|
141
|
+
*/
|
|
142
|
+
createHoverCallbacks(msg) {
|
|
143
|
+
return {
|
|
144
|
+
mouseover: function(d) {
|
|
145
|
+
d3.select(this).style('opacity', .5)
|
|
146
|
+
},
|
|
147
|
+
mouseleave: function(d) {
|
|
148
|
+
d3.select(this).style('opacity', 1)
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates two scale functions. vx (resp. vy) is used to convert x (resp. y)
|
|
155
|
+
* to a number between 0 and the width (resp. height) of the viewer.
|
|
156
|
+
* @param {Array<Object>} bins - Array containing bins
|
|
157
|
+
* @returns {Viewport} This viewer current viewport
|
|
158
|
+
*/
|
|
159
|
+
createViewport(bins){
|
|
160
|
+
return {
|
|
161
|
+
vx: d3.scaleLinear()
|
|
162
|
+
.domain([bins[0].x0, bins[bins.length - 1].x1])
|
|
163
|
+
.range([this.padding.left, this.width - this.padding.right]),
|
|
164
|
+
vy: d3.scaleLinear()
|
|
165
|
+
.domain([0, d3.max(bins, (d) => d.length)])
|
|
166
|
+
.range([this.height - this.padding.bottom, this.padding.top]),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getBinLabel(bin) {
|
|
171
|
+
const py = this.getProperty(this.pidy);
|
|
172
|
+
const name = py ? py.name : `Missing property '${this.pidy}'`;
|
|
173
|
+
return `No. of ${name}: ${bin.length}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Add X and Y axes to the SVG scene.
|
|
178
|
+
*/
|
|
179
|
+
addAxes(svg, vx, vy) {
|
|
180
|
+
const labels = this.getAxesLabels();
|
|
181
|
+
// add X-axis and label
|
|
182
|
+
svg.append('g')
|
|
183
|
+
.attr('transform', `translate(0,${this.height - this.padding.bottom})`)
|
|
184
|
+
.call(d3.axisBottom(vx).ticks(this.width / 80).tickSizeOuter(0))
|
|
185
|
+
.call((g) => g.append('text')
|
|
186
|
+
.attr('x', this.width)
|
|
187
|
+
.attr('y', this.padding.bottom - 4)
|
|
188
|
+
.attr('fill', 'currentColor')
|
|
189
|
+
.attr('text-anchor', 'end')
|
|
190
|
+
.text(labels.x)
|
|
191
|
+
);
|
|
192
|
+
// add Y-axis and label
|
|
193
|
+
svg.append('g')
|
|
194
|
+
.attr('transform', `translate(${this.padding.left},0)`)
|
|
195
|
+
.call(d3.axisLeft(vy).ticks(this.height / 40))
|
|
196
|
+
.call((g) => g.append('text')
|
|
197
|
+
.attr('x', -this.padding.left)
|
|
198
|
+
.attr('y', 10)
|
|
199
|
+
.attr('fill', 'currentColor')
|
|
200
|
+
.attr('text-anchor', 'start')
|
|
201
|
+
.text(`Frequency (#${labels.y})`)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getAxesLabels() {
|
|
206
|
+
const px = this.getProperty(this.pidx);
|
|
207
|
+
const py = this.getProperty(this.pidy);
|
|
208
|
+
return {
|
|
209
|
+
x: px ? px.name : `Missing property '${this.pidx}'`,
|
|
210
|
+
y: py ? py.name : `Missing property '${this.pidy}'`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
getProperty(pid) {
|
|
215
|
+
const found = this.properties.find((p) => p['@id'] === pid);
|
|
216
|
+
return found ? found : null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
export { Histogram };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { BarChart } from './barchart.js';
|
|
2
|
+
export { Histogram } from './histogram.js';
|
|
3
|
+
export { LineChart } from './linechart.js';
|
|
4
|
+
export { ScatterPlot } from './scatterplot.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This object is a wrapper for three arrays
|
|
8
|
+
* @typedef {Object} Hera
|
|
9
|
+
* @property {Array<Object>} items - Items list
|
|
10
|
+
* @property {Array<Object>} entities - Entities list
|
|
11
|
+
* @property {Array<Object>} properties - Properties list
|
|
12
|
+
*/
|