@cnrs/hecate-views-charts 0.2.0 → 0.4.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/views/all.js +5 -0
- package/src/views/barchart.js +37 -63
- package/src/views/cartesianchart.js +303 -0
- package/src/views/histogram.js +19 -51
- package/src/views/index.js +6 -4
- package/src/views/linechart.js +24 -60
- package/src/views/scatterplot.js +49 -62
- package/src/views/stackedareachart.js +126 -0
- package/src/views/stackedchart.js +0 -0
- package/src/views/streamgraph.js +39 -0
- package/src/components/index.js +0 -1
- package/src/components/sample.js +0 -37
package/README.md
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hecate ”charts” views
|
|
2
2
|
|
|
3
3
|
<a href='https://datasphere.readthedocs.io/projects/hecate/'><img src='https://gitlab.huma-num.fr/datasphere/doc/assets/-/raw/main/banners/HECATE.png' width='100%'></a>
|
|
4
4
|
|
|
5
|
+
[](https://www.gnu.org/licenses/agpl-3.0.html)
|
|
6
|
+
[](https://www.repostatus.org/#active)
|
|
7
|
+
[](https://gitlab.huma-num.fr/datasphere/hecate/views/charts/pipelines/latest)
|
|
8
|
+
[](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/coverage/)
|
|
9
|
+
[](https://www.npmjs.com/package/@cnrs/hecate-views-charts)
|
|
10
|
+
[](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/)
|
|
11
|
+
|
|
5
12
|
**Hecate** is a free web application for managing scientific databases.
|
|
6
13
|
|
|
7
|
-
**hecate-views
|
|
8
|
-
Views are specific glances on subsets of your scientific database : lists, charts, maps, and so on.
|
|
14
|
+
[**@cnrs/hecate-views-charts**](https://www.npmjs.com/package/@cnrs/hecate-views-charts) is a free Javascript library for visualizing scientific data.
|
|
15
|
+
[Views](https://datasphere.readthedocs.io/projects/hecate/en/latest/design/views.html) are specific glances on subsets of your scientific database : lists, charts, maps, timelines, thumbnails, and so on.
|
|
9
16
|
Each view takes HERA-formatted data ([10.5281/zenodo.12795179](http://doi.org/10.5281/zenodo.12795179)) as its input, and can be easily configured to suit your needs.
|
|
10
17
|
|
|
18
|
+
This package provides the following views:
|
|
19
|
+
|
|
20
|
+
* [BarChart](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/BarChart.html), usable as a `hecate-barchart` HTML element;
|
|
21
|
+
* [Histogram](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/Histogram.html), usable as a `hecate-histogram` HTML element;
|
|
22
|
+
* [LineChart](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/LineChart.html), usable as a `hecate-linechart` HTML element;
|
|
23
|
+
* [ScatterPlot](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/ScatterPlot.html), usable as a `hecate-scatterplot` HTML element;
|
|
24
|
+
* [StackedAreaChart](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/StackedAreaChart.html), usable as a `hecate-stackedareachart` HTML element;
|
|
25
|
+
* [Streamgraph](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/Streamgraph.html), usable as a `hecate-streamgraph` HTML element.
|
|
26
|
+
|
|
11
27
|
## Resources
|
|
12
28
|
|
|
13
29
|
* [Documentation](https://datasphere.readthedocs.io/projects/hecate/)
|
|
14
|
-
* [
|
|
30
|
+
* [API Documentation](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/doc/)
|
|
31
|
+
* [Releases](https://www.npmjs.com/package/@cnrs/hecate-views-charts?activeTab=versions)
|
|
15
32
|
* [Getting help](https://gitlab.huma-num.fr/datasphere/hecate/views/charts/-/issues/)
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
+
"name": "@cnrs/hecate-views-charts",
|
|
3
|
+
"description": "Views web components available to any web app.",
|
|
2
4
|
"@comment comments": [
|
|
3
5
|
"To track and understand the whys of each part of this package.json, ",
|
|
4
6
|
"in particular dependencies, please explain when you modify this file.",
|
|
@@ -9,8 +11,15 @@
|
|
|
9
11
|
"",
|
|
10
12
|
"package.json documentation: https://docs.npmjs.com/files/package.json"
|
|
11
13
|
],
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "src/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src/**/*.js"
|
|
21
|
+
],
|
|
22
|
+
"version": "0.4.0",
|
|
14
23
|
"keywords": [
|
|
15
24
|
"dataviz",
|
|
16
25
|
"research",
|
|
@@ -21,17 +30,7 @@
|
|
|
21
30
|
"browser",
|
|
22
31
|
"library"
|
|
23
32
|
],
|
|
24
|
-
"
|
|
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",
|
|
33
|
+
"author": "The Hecate contributors",
|
|
35
34
|
"license": "AGPL",
|
|
36
35
|
"homepage": "https://datasphere.readthedocs.io/projects/hecate/",
|
|
37
36
|
"repository": {
|
|
@@ -41,8 +40,8 @@
|
|
|
41
40
|
"bugs": {
|
|
42
41
|
"url": "https://gitlab.huma-num.fr/datasphere/hecate/views/charts/-/issues"
|
|
43
42
|
},
|
|
44
|
-
"main": "src/index.js",
|
|
45
43
|
"dependencies": {
|
|
44
|
+
"@cnrs/hecate-views": "^0.3.6",
|
|
46
45
|
"d3": "latest"
|
|
47
46
|
},
|
|
48
47
|
"@comment devDependencies": {
|
|
@@ -56,7 +55,7 @@
|
|
|
56
55
|
"@requires vite ≥ 3.0.0 (and node ≥ 14.18)",
|
|
57
56
|
"- Vitest website: https://vitest.dev/"
|
|
58
57
|
],
|
|
59
|
-
"@vitest/coverage-
|
|
58
|
+
"@vitest/coverage-v8": [
|
|
60
59
|
"Compute percentage of code covered by tests.",
|
|
61
60
|
"- Vitest coverage documentation: https://vitest.dev/guide/coverage.html"
|
|
62
61
|
],
|
|
@@ -66,9 +65,20 @@
|
|
|
66
65
|
"- https://www.npmjs.com/package/happy-dom"
|
|
67
66
|
],
|
|
68
67
|
"eslint": [
|
|
69
|
-
"
|
|
68
|
+
"A linter for analysing ECMAScript code (among others).",
|
|
70
69
|
"- ESLint documentation: https://eslint.org/"
|
|
71
70
|
],
|
|
71
|
+
"@eslint/js": [
|
|
72
|
+
"ESLint plugin specialised in the javascript language.",
|
|
73
|
+
"Needed since javascript-specifics have been separated from the core ESLint.",
|
|
74
|
+
"Referred in the ESLint config file (eslint.config.mjs).",
|
|
75
|
+
"- https://www.npmjs.com/package/@eslint/js"
|
|
76
|
+
],
|
|
77
|
+
"globals": [
|
|
78
|
+
"A collection of identifiers used by ESLint.",
|
|
79
|
+
"Referred in the ESLint config file (eslint.config.mjs).",
|
|
80
|
+
"- https://www.npmjs.com/package/globals"
|
|
81
|
+
],
|
|
72
82
|
"jsdoc": [
|
|
73
83
|
"Javascript documentation generation tool.",
|
|
74
84
|
"See `gendoc` script in this same file for some usage details.",
|
|
@@ -79,8 +89,10 @@
|
|
|
79
89
|
"vite": "latest",
|
|
80
90
|
"vitest": "latest",
|
|
81
91
|
"@vitest/coverage-v8": "latest",
|
|
82
|
-
"happy-dom": "
|
|
92
|
+
"happy-dom": "latest",
|
|
83
93
|
"eslint": "latest",
|
|
94
|
+
"@eslint/js": "latest",
|
|
95
|
+
"globals": "latest",
|
|
84
96
|
"jsdoc": "latest"
|
|
85
97
|
},
|
|
86
98
|
"@comment scripts": {
|
|
@@ -102,13 +114,25 @@
|
|
|
102
114
|
"@requires vitest @vitest/coverage-v8 (dev dependencies)"
|
|
103
115
|
],
|
|
104
116
|
"build": [
|
|
105
|
-
"Bundles project
|
|
117
|
+
"Bundles project in `dist` folder. Please note that:",
|
|
118
|
+
"- we use vite default settings, so don't need any `vite.config.mjs`.",
|
|
119
|
+
"- the result of this build is of no use, as this `package.json` file",
|
|
120
|
+
" says the NPM package contents are `src/**/*.js`, not `dist/*.js`.",
|
|
121
|
+
" We do that because our code is already ESM, Node and modern browsers",
|
|
122
|
+
" compatible, with no need for transpiling (no TypeScript here).",
|
|
123
|
+
"In other words, this command isn't really useful, though I'll keep it",
|
|
124
|
+
"to differentiate it from `buid:cdn`, which IS used.",
|
|
125
|
+
"@requires vite (dev dependency)"
|
|
126
|
+
],
|
|
127
|
+
"build:cdn": [
|
|
128
|
+
"Bundles project for production CDNs in `cdn/dist` folder.",
|
|
129
|
+
"Delivery to NPM must then be made using `cdn/package.json`.",
|
|
106
130
|
"@requires vite (dev dependency)"
|
|
107
131
|
],
|
|
108
132
|
"lint": [
|
|
109
133
|
"Checks code style using ESLint. note: Argument pattern is surrounded with ",
|
|
110
134
|
"double quotes, so its resolution doesn't depend on runtime environment.",
|
|
111
|
-
"@requires eslint (dev
|
|
135
|
+
"@requires eslint and @eslint/js (dev dependencies)",
|
|
112
136
|
"- ESLint configuration file is `eslint.config.mjs`",
|
|
113
137
|
"- ESLint documentation: https://eslint.org/"
|
|
114
138
|
],
|
|
@@ -128,7 +152,8 @@
|
|
|
128
152
|
"test:coverage": "vitest run --coverage",
|
|
129
153
|
"test:watch": "vitest --coverage",
|
|
130
154
|
"build": "vite build",
|
|
131
|
-
"
|
|
155
|
+
"build:cdn": "vite build --config cdn/vite.cdn.config.mjs",
|
|
156
|
+
"lint": "exec eslint \"src/**/*.js\"",
|
|
132
157
|
"doc:html": "./node_modules/jsdoc/jsdoc.js -c .jsdoc.conf.json"
|
|
133
158
|
}
|
|
134
159
|
}
|
package/src/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {
|
|
2
|
+
BarChart,
|
|
3
|
+
Histogram,
|
|
4
|
+
ScatterPlot,
|
|
5
|
+
LineChart,
|
|
6
|
+
StackedAreaChart,
|
|
7
|
+
Streamgraph,
|
|
8
|
+
} from './views/index.js'; // eslint-disable-line no-unused-vars
|
package/src/views/all.js
ADDED
package/src/views/barchart.js
CHANGED
|
@@ -1,89 +1,46 @@
|
|
|
1
|
+
import { View } from '@cnrs/hecate-views';
|
|
1
2
|
import * as d3 from 'd3';
|
|
2
3
|
|
|
3
4
|
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* BarChart web component.
|
|
6
8
|
* @see https://en.wikipedia.org/wiki/Bar_chart
|
|
7
9
|
*/
|
|
8
|
-
class BarChart extends
|
|
10
|
+
class BarChart extends View {
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
this.attachShadow({ mode: 'open' });
|
|
13
|
-
this.hera = {};
|
|
14
|
-
this.settings = {};
|
|
15
|
-
}
|
|
12
|
+
get pidx() { return this.settings.x || 'x'; }
|
|
13
|
+
get pidy() { return this.settings.y || 'y'; }
|
|
16
14
|
|
|
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
15
|
|
|
23
|
-
get items() { return this.hera.items || []; }
|
|
24
|
-
get entities() { return this.hera.entities || []; }
|
|
25
|
-
get properties() { return this.hera.properties || []; }
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
17
|
+
renderView() {
|
|
18
|
+
// cleanup
|
|
19
|
+
while (this.viewRoot.lastChild.name !== 'view') { // <slot />
|
|
20
|
+
this.viewRoot.removeChild(this.viewRoot.lastChild);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var dataset = this.prepareData();
|
|
55
24
|
//creation of scene
|
|
56
25
|
const margin = { top : 70, right : 30, bottom: 40, left : 80};
|
|
57
|
-
const {sceneWidth, sceneHeight, barWidth, svg} = this.createScene(500, 500, this.items.length, margin);
|
|
26
|
+
const {sceneWidth, sceneHeight, barWidth, svg} = this.createScene(500, 500, this.db.items.length, margin);
|
|
58
27
|
|
|
59
28
|
//addition of axises
|
|
60
29
|
var {y_scale, x_scale} = this.createAxisScales(sceneHeight, sceneWidth, dataset);
|
|
61
30
|
this.addAxises(x_scale, y_scale, sceneHeight, sceneWidth, margin, svg)
|
|
62
31
|
|
|
63
32
|
this.displayChart(sceneHeight, barWidth, dataset, y_scale, x_scale, svg);
|
|
64
|
-
|
|
65
33
|
}
|
|
66
34
|
|
|
67
35
|
/**
|
|
68
|
-
*
|
|
69
|
-
* @
|
|
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
|
|
36
|
+
* Prepare the data by giving it the correct format.
|
|
37
|
+
* @param {object} db HERA-formatted data as it was passed.
|
|
38
|
+
* @returns {object} Metadata ; each key representing a different pid.
|
|
82
39
|
*/
|
|
83
|
-
prepareData(
|
|
40
|
+
prepareData() {
|
|
84
41
|
var barValues = [];
|
|
85
42
|
var barLabels = [];
|
|
86
|
-
|
|
43
|
+
this.db.items.forEach(item => {
|
|
87
44
|
item.metadata.forEach(meta => {
|
|
88
45
|
if (meta.pid === this.pidy) { // Y axis is height of bars
|
|
89
46
|
barValues.push(parseInt(meta.value));
|
|
@@ -114,7 +71,8 @@ class BarChart extends HTMLElement{
|
|
|
114
71
|
const sceneHeight = height - margin.top - margin.bottom;
|
|
115
72
|
const barWidth = sceneWidth/dataLength
|
|
116
73
|
|
|
117
|
-
const divElem =
|
|
74
|
+
const divElem = document.createElement('div');
|
|
75
|
+
this.viewRoot.appendChild(divElem);
|
|
118
76
|
const container = d3.select(divElem);
|
|
119
77
|
|
|
120
78
|
//addition of container to the svg
|
|
@@ -236,11 +194,27 @@ class BarChart extends HTMLElement{
|
|
|
236
194
|
}
|
|
237
195
|
|
|
238
196
|
getProperty(pid) {
|
|
239
|
-
const found = this.properties.find((p) => p['
|
|
197
|
+
const found = this.db.properties.find((p) => p['id'] === pid);
|
|
240
198
|
return found ? found : null;
|
|
241
199
|
}
|
|
200
|
+
|
|
242
201
|
}
|
|
243
202
|
|
|
244
203
|
|
|
245
204
|
|
|
205
|
+
/** Returns if a string is a valid color.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} value Color (or not).
|
|
208
|
+
* @returns {boolean} `true` if `value` is a valid color, `false` if not.
|
|
209
|
+
*/
|
|
210
|
+
function isColor(value) {
|
|
211
|
+
var style = new Option().style;
|
|
212
|
+
style.color = value; // if value invalid, it won't be assigned
|
|
213
|
+
return style.color == value.toLowerCase();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
customElements.define('hecate-barchart', BarChart);
|
|
219
|
+
|
|
246
220
|
export { BarChart };
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { View } from '@cnrs/hecate-views';
|
|
2
|
+
import * as d3 from 'd3';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CartesianChart extends View {
|
|
7
|
+
|
|
8
|
+
get vertical() { return this.settings.orientation === 'vertical' || false; }
|
|
9
|
+
get hEncoding() { return this.vertical? this.settings.y : this.settings.x; }
|
|
10
|
+
get vEncoding() { return this.vertical? this.settings.x : this.settings.y; }
|
|
11
|
+
get reverseX() { return this.settings.x?.axis?.reverse === true || false; }
|
|
12
|
+
get reverseY() { return this.settings.y?.axis?.reverse === true || false; }
|
|
13
|
+
get normalizeX() { return this.settings.x?.normalize === true || false; }
|
|
14
|
+
get normalizeY() { return this.settings.y?.normalize === true || false; }
|
|
15
|
+
get normalizeH() { return this.hEncoding?.normalize === true || false; }
|
|
16
|
+
get normalizeV() { return this.vEncoding?.normalize === true || false; }
|
|
17
|
+
|
|
18
|
+
getItemX(item) { return getMetadata(item, this.settings.x?.pid); }
|
|
19
|
+
getItemY(item) { return getMetadata(item, this.settings.y?.pid); }
|
|
20
|
+
getItemCategory(item) { return getMetadata(item, this.settings.category?.pid); }
|
|
21
|
+
|
|
22
|
+
renderView() {
|
|
23
|
+
// cleanup
|
|
24
|
+
while (this.viewRoot.lastChild.name !== 'view') { // <slot />
|
|
25
|
+
this.viewRoot.removeChild(this.viewRoot.lastChild);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const container = document.createElement('div');
|
|
29
|
+
container.style = 'width: 100%; height: 100%;';
|
|
30
|
+
this.viewRoot.appendChild(container);
|
|
31
|
+
|
|
32
|
+
const xProperty = getProperty(this.db, this.settings.x?.pid);
|
|
33
|
+
const yProperty = getProperty(this.db, this.settings.y?.pid);
|
|
34
|
+
const cProperty = getProperty(this.db, this.settings.category?.pid);
|
|
35
|
+
const xType = xProperty?.type || 'text';
|
|
36
|
+
const yType = yProperty?.type || 'text';
|
|
37
|
+
const cType = cProperty?.type || 'text';
|
|
38
|
+
const parsers = {
|
|
39
|
+
x: getParser(xType),
|
|
40
|
+
y: getParser(yType),
|
|
41
|
+
color: getParser(cType),
|
|
42
|
+
};
|
|
43
|
+
const scales = {
|
|
44
|
+
x: getScale(xType),
|
|
45
|
+
y: getScale(yType),
|
|
46
|
+
color: getScale(cType),
|
|
47
|
+
};
|
|
48
|
+
const plot = {
|
|
49
|
+
width: this.clientWidth,
|
|
50
|
+
height: this.clientHeight,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.prepare(parsers, scales);
|
|
54
|
+
|
|
55
|
+
// Prepare the scales for positional and color encodings.
|
|
56
|
+
// domain = data ; range = render ... cuz a function has:
|
|
57
|
+
// - a domain (its input values),
|
|
58
|
+
// - a codomain (theorical output values), and
|
|
59
|
+
// - a range (effective output values)
|
|
60
|
+
plot.vx = this.createXScale(plot, parsers, scales.x);
|
|
61
|
+
plot.vy = this.createYScale(plot, parsers, scales.y);
|
|
62
|
+
plot.color = this.createColorScale(parsers, scales.color);
|
|
63
|
+
// if this.vertical is true, X axis becomes vertical and Y axis horizontal
|
|
64
|
+
// so we swap them, /but/ they stay named as vx,vy (and not eg. vh,vv)
|
|
65
|
+
// because they really are data scales, and data channels are X,Y
|
|
66
|
+
if (this.vertical) [plot.vx, plot.vy] = [plot.vy, plot.vx];
|
|
67
|
+
// plot.padding is the internal space reserved around the
|
|
68
|
+
// cartesian plotting area, incuding axes, ticks and labels.
|
|
69
|
+
const { hAxis, vAxis, padding } = this.createAxes(plot);
|
|
70
|
+
plot.padding = padding;
|
|
71
|
+
|
|
72
|
+
// Create the SVG container.
|
|
73
|
+
const svgWidth = plot.width + plot.padding.left + plot.padding.right;
|
|
74
|
+
const svgHeight = plot.height + plot.padding.top + plot.padding.bottom;
|
|
75
|
+
const svg = d3.create('svg')
|
|
76
|
+
.attr('width', svgWidth)
|
|
77
|
+
.attr('height', svgHeight)
|
|
78
|
+
.attr('viewBox', [0, 0, svgWidth, svgHeight])
|
|
79
|
+
.attr('style', 'max-width: 100%; height: auto;');
|
|
80
|
+
|
|
81
|
+
// render data
|
|
82
|
+
this.renderData(svg, plot, parsers);
|
|
83
|
+
// render axes
|
|
84
|
+
this.renderHAxis(svg, plot, hAxis);
|
|
85
|
+
this.renderVAxis(svg, plot, vAxis);
|
|
86
|
+
|
|
87
|
+
container.appendChild(svg.node());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
prepare(parsers, scales) { }
|
|
91
|
+
|
|
92
|
+
renderData(svg, plot, parsers) {
|
|
93
|
+
//const { x, y, category } = this.settings;
|
|
94
|
+
const message = //([x, y, c].every(v => v !== undefined)) ?
|
|
95
|
+
`${this.constructor.name} should implement renderData()`
|
|
96
|
+
// : "Missing settings"
|
|
97
|
+
;
|
|
98
|
+
svg.append('text')
|
|
99
|
+
.attr('x', plot.width / 2 + plot.padding.left)
|
|
100
|
+
.attr('y', plot.height / 2 + plot.padding.top)
|
|
101
|
+
.attr('text-anchor', 'middle')
|
|
102
|
+
.attr('fill', 'currentColor')
|
|
103
|
+
.text(`${this.constructor.name} should implement renderData()`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
renderHAxis(svg, plot, axis) {
|
|
107
|
+
const xOffset = plot.padding.left;
|
|
108
|
+
const yOffset = plot.height + plot.padding.top;
|
|
109
|
+
axis.attr('transform', `translate(${xOffset},${yOffset})`);
|
|
110
|
+
if (this.hEncoding?.axis?.domain === false)
|
|
111
|
+
axis.call(g => g.select('.domain').remove());
|
|
112
|
+
svg.append(() => axis.node());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
renderVAxis(svg, plot, axis) {
|
|
116
|
+
const xOffset = plot.padding.left;
|
|
117
|
+
const yOffset = plot.padding.top;
|
|
118
|
+
axis.attr('transform', `translate(${xOffset},${yOffset})`);
|
|
119
|
+
if (this.vEncoding?.axis?.domain === false)
|
|
120
|
+
axis.call(g => g.select('.domain').remove());
|
|
121
|
+
svg.append(() => axis.node());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
createXScale(plot, parsers, scale) {
|
|
125
|
+
let domain = this.createXDomain(parsers);
|
|
126
|
+
if (this.reverseX) domain = [...domain].reverse();
|
|
127
|
+
const range = this.createXRange(plot);
|
|
128
|
+
return scale().domain(domain).range(range);
|
|
129
|
+
}
|
|
130
|
+
createYScale(plot, parsers, scale) {
|
|
131
|
+
let domain = this.createYDomain(parsers);
|
|
132
|
+
if (this.reverseY) domain = [...domain].reverse();
|
|
133
|
+
const range = this.createYRange(plot);
|
|
134
|
+
return scale().domain(domain).range(range);
|
|
135
|
+
}
|
|
136
|
+
createColorScale(parsers, scale) {
|
|
137
|
+
const cats = new Set();
|
|
138
|
+
for (const item of this.db.items) cats.add(this.getItemCategory(item));
|
|
139
|
+
const categories = [...cats]; // Set -> Array
|
|
140
|
+
const domain = this.createColorDomain(categories);
|
|
141
|
+
const range = this.createColorRange();
|
|
142
|
+
return scale().domain(domain).range(range);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
createXDomain(parsers) { return [0, 1]; }
|
|
146
|
+
createYDomain(parsers) { return [0, 1]; }
|
|
147
|
+
createColorDomain(categories) { return categories; }
|
|
148
|
+
createXRange(plot) { return this.vertical ? [plot.height, 0] : [0, plot.width]; }
|
|
149
|
+
createYRange(plot) { return this.vertical ? [0, plot.width] : [plot.height, 0]; }
|
|
150
|
+
createColorRange() { return d3.schemeTableau10; }
|
|
151
|
+
// TODO: provide a fallback palette or generated colors
|
|
152
|
+
// when categories.length > 10 (d3 reuses colors)
|
|
153
|
+
|
|
154
|
+
createAxes(plot) {
|
|
155
|
+
// create a temporary SVG, only for axes bounding box computation
|
|
156
|
+
// these computations allow to deduce necessary paddings
|
|
157
|
+
const tmp = d3.select(this.viewRoot).append('svg')
|
|
158
|
+
.style('position', 'absolute').style('visibility', 'hidden')
|
|
159
|
+
;
|
|
160
|
+
// create horizontal and vertical axes and add them to temporary svg
|
|
161
|
+
const hAxis = this.createHAxis(tmp, plot);
|
|
162
|
+
const vAxis = this.createVAxis(tmp, plot);
|
|
163
|
+
|
|
164
|
+
const hAxisBBox = hAxis.node().getBBox();
|
|
165
|
+
const vAxisBBox = vAxis.node().getBBox();
|
|
166
|
+
|
|
167
|
+
// space around the cartesian plotting area (inc. axes, ticks and labels).
|
|
168
|
+
const padding = {
|
|
169
|
+
top: -vAxisBBox.y, // overflow of first vertical tick label
|
|
170
|
+
right: hAxisBBox.x + hAxisBBox.width - plot.width, // overflow of last horizontal tick label
|
|
171
|
+
bottom: hAxisBBox.height, // room for horizontal axis, inc. ticks and labels
|
|
172
|
+
left: -vAxisBBox.x, // room for vertical axis, inc. ticks and labels
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// add axes labels (if any) and update `padding` (if so)
|
|
176
|
+
if (this.hEncoding?.axis?.name) { // create label for horizontal axis `hAxis`
|
|
177
|
+
const hLabel = this.createHAxisLabel(hAxis, this.hEncoding.axis.name);
|
|
178
|
+
const { x,y } = this.createHAxisLabelOffset(plot, padding, hLabel);
|
|
179
|
+
hLabel.attr('x', x).attr('y', y);
|
|
180
|
+
}
|
|
181
|
+
if (this.vEncoding?.axis?.name) { // create label for vertical axis `vAxis`
|
|
182
|
+
const vLabel = this.createVAxisLabel(vAxis, this.vEncoding.axis.name);
|
|
183
|
+
const { x,y } = this.createVAxisLabelOffset(plot, padding, vLabel);
|
|
184
|
+
vLabel.attr('x', x).attr('y', y);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// padding computations are done, so delete `tmp` svg ; after that,
|
|
188
|
+
// hAxis,vAxis elements are orphaned, allowing them to be added to the
|
|
189
|
+
// real svg `svg`, below
|
|
190
|
+
tmp.remove();
|
|
191
|
+
|
|
192
|
+
return { hAxis, vAxis, padding };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Creates the horizontal axis and adds it to the SVG.
|
|
197
|
+
* @param {d3.Selection} svg SVG element.
|
|
198
|
+
* @param {Plot} plot Coordinate system for `svg`.
|
|
199
|
+
* @returns {d3.Selection} X axis SVG element.
|
|
200
|
+
*/
|
|
201
|
+
createHAxis(svg, plot) {
|
|
202
|
+
const scale = plot.vx;
|
|
203
|
+
const axis = d3.axisBottom(scale);
|
|
204
|
+
if (this.normalizeH) axis.tickFormat(d3.format(".0%"));
|
|
205
|
+
return svg.append('g').call(axis);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
createVAxis(svg, plot) {
|
|
209
|
+
const scale = plot.vy;
|
|
210
|
+
const axis = d3.axisLeft(scale);
|
|
211
|
+
if (this.normalizeV) axis.tickFormat(d3.format(".0%"));
|
|
212
|
+
return svg.append('g').call(axis);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Creates the horizontal axis label, and add it to the horizontal axis.
|
|
217
|
+
* @param {d3.Selection} axis Horizontal axis SVG element (eg. a `<g>`).
|
|
218
|
+
* @param {string} label Horizontal axis label string.
|
|
219
|
+
* @returns {d3.Selection} Horizontal axis label SVG element.
|
|
220
|
+
*/
|
|
221
|
+
createHAxisLabel(axis, label) {
|
|
222
|
+
return axis.append('text')
|
|
223
|
+
.attr('dominant-baseline', 'hanging')
|
|
224
|
+
.attr('fill', 'currentColor')
|
|
225
|
+
.attr('text-anchor', 'end')
|
|
226
|
+
.text(label);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
createVAxisLabel(axis, label) {
|
|
230
|
+
return axis.append('text')
|
|
231
|
+
.attr('dominant-baseline', 'hanging')
|
|
232
|
+
.attr('fill', 'currentColor')
|
|
233
|
+
.attr('text-anchor', 'start')
|
|
234
|
+
.text(label);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
createHAxisLabelOffset(plot, padding, label) {
|
|
238
|
+
const labelBBox = label.node().getBBox();
|
|
239
|
+
// increase the bottom padding so the label fits inside svg viewBox
|
|
240
|
+
padding.bottom += labelBBox.height;
|
|
241
|
+
// position the label so its top aligns with the ticks labels bottom
|
|
242
|
+
return {
|
|
243
|
+
x: plot.width + padding.right,
|
|
244
|
+
y: padding.bottom -labelBBox.height,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
createVAxisLabelOffset(plot, padding, label) {
|
|
249
|
+
const labelBBox = label.node().getBBox();
|
|
250
|
+
const yMax = d3.max(plot.vy.domain());
|
|
251
|
+
const yTop = plot.vy(plot.vy.domain()[1]);
|
|
252
|
+
// increase the top padding so the label fits inside svg viewBox
|
|
253
|
+
padding.top = labelBBox.height - yTop;
|
|
254
|
+
// position the label so its bottom aligns with the upper tick label top
|
|
255
|
+
return {
|
|
256
|
+
x: -padding.left,
|
|
257
|
+
y: yTop - labelBBox.height,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
const PARSERS = {
|
|
265
|
+
'date': d => new Date(d),
|
|
266
|
+
'number': d => +d,
|
|
267
|
+
'text': d => d,
|
|
268
|
+
};
|
|
269
|
+
const SCALES = {
|
|
270
|
+
'date': d3.scaleUtc,
|
|
271
|
+
'number': d3.scaleLinear,
|
|
272
|
+
'text': d3.scaleOrdinal,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function getParser(type) { return PARSERS[type]; }
|
|
276
|
+
function getScale(type) { return SCALES[type]; }
|
|
277
|
+
|
|
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
|
+
export { CartesianChart };
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Cartesian plotting area.
|
|
292
|
+
*
|
|
293
|
+
* This object defines the coordinate system used to render a chart.
|
|
294
|
+
*
|
|
295
|
+
* @typedef {Object} Plot
|
|
296
|
+
* @property {number} width - Width of the drawable plotting area, excluding padding.
|
|
297
|
+
* @property {number} height - Height of the drawable plotting area, excluding padding.
|
|
298
|
+
* @property {Function} vx - Maps X values from data space to view space.
|
|
299
|
+
* @property {Function} vy - Maps Y values from data space to view space.
|
|
300
|
+
* @property {{top:number,right:number,bottom:number,left:number}} [padding]
|
|
301
|
+
* Internal space reserved for axes, ticks and labels.
|
|
302
|
+
* TODO
|
|
303
|
+
*/
|
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 './scatterplot.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,80 +1,38 @@
|
|
|
1
|
+
import { View } from '@cnrs/hecate-views';
|
|
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
|
-
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 || []; }
|
|
10
|
+
class LineChart extends View {
|
|
27
11
|
|
|
28
12
|
get pidx() { return this.settings.x || 'x'; }
|
|
29
13
|
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
14
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
15
|
+
get padding() {
|
|
16
|
+
return this.settings.padding
|
|
17
|
+
|| { top: 70, right: 30, bottom: 40, left: 80};
|
|
54
18
|
}
|
|
55
19
|
|
|
56
|
-
|
|
20
|
+
renderView() {
|
|
21
|
+
// cleanup
|
|
22
|
+
while (this.viewRoot.lastChild.name !== 'view') { // <slot />
|
|
23
|
+
this.viewRoot.removeChild(this.viewRoot.lastChild);
|
|
24
|
+
}
|
|
25
|
+
|
|
57
26
|
var dataset = this.prepareData();
|
|
58
27
|
|
|
59
|
-
|
|
60
|
-
var {sceneWidth, sceneHeight, svg} = this.createscene(margin, 600, 1200);
|
|
28
|
+
var {sceneWidth, sceneHeight, svg} = this.createscene(this.padding, 600, 1200);
|
|
61
29
|
|
|
62
30
|
var {x_scale, y_scale} = this.createAxisScales(sceneHeight, sceneWidth, dataset)
|
|
63
|
-
this.addAxises(sceneWidth, sceneHeight,
|
|
31
|
+
this.addAxises(sceneWidth, sceneHeight, this.padding, x_scale, y_scale, svg);
|
|
64
32
|
|
|
65
33
|
this.displayChart(x_scale, y_scale, dataset, svg);
|
|
66
34
|
}
|
|
67
35
|
|
|
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
36
|
/**
|
|
79
37
|
* Prepare the data by giving it the correct format
|
|
80
38
|
* Creates a javascript object that contains the elements of metadata, each key representing a different pid
|
|
@@ -83,10 +41,10 @@ class LineChart extends HTMLElement{
|
|
|
83
41
|
* @returns {Object} JS object containing the elements of metadata,
|
|
84
42
|
* x (resp. y) key contains the value for x (resp. y) axis
|
|
85
43
|
*/
|
|
86
|
-
prepareData(){
|
|
44
|
+
prepareData() {
|
|
87
45
|
var yValues = [];
|
|
88
46
|
var xValues = [];
|
|
89
|
-
this.items.forEach(item => {
|
|
47
|
+
this.db.items.forEach(item => {
|
|
90
48
|
item.metadata.forEach(meta => {
|
|
91
49
|
if (meta.pid === this.pidy) {
|
|
92
50
|
yValues.push(parseInt(meta.value));
|
|
@@ -115,7 +73,8 @@ class LineChart extends HTMLElement{
|
|
|
115
73
|
const lineHeight = height - margin.top - margin.bottom;
|
|
116
74
|
|
|
117
75
|
// create svg and append it
|
|
118
|
-
const divElem =
|
|
76
|
+
const divElem = document.createElement('div');
|
|
77
|
+
this.viewRoot.appendChild(divElem);
|
|
119
78
|
const container = d3.select(divElem);
|
|
120
79
|
|
|
121
80
|
const svg = container.append("svg")
|
|
@@ -226,9 +185,14 @@ class LineChart extends HTMLElement{
|
|
|
226
185
|
}
|
|
227
186
|
|
|
228
187
|
getProperty(pid) {
|
|
229
|
-
const found = this.properties.find((p) => p['
|
|
188
|
+
const found = this.db.properties.find((p) => p['id'] === pid);
|
|
230
189
|
return found ? found : null;
|
|
231
190
|
}
|
|
232
191
|
|
|
233
192
|
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
customElements.define('hecate-linechart', LineChart);
|
|
197
|
+
|
|
234
198
|
export { LineChart };
|
package/src/views/scatterplot.js
CHANGED
|
@@ -1,87 +1,61 @@
|
|
|
1
|
+
import { View } from '@cnrs/hecate-views';
|
|
1
2
|
import * as d3 from 'd3';
|
|
2
3
|
|
|
3
4
|
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Scatterplot web component.
|
|
6
8
|
* @see https://en.wikipedia.org/wiki/Scatter_plot
|
|
7
9
|
* @see https://scottmurray.org/tutorials/d3/
|
|
8
10
|
*/
|
|
9
|
-
class ScatterPlot extends
|
|
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 || []; }
|
|
11
|
+
class ScatterPlot extends View {
|
|
29
12
|
|
|
30
13
|
get pidx() { return this.settings.x || 'x'; }
|
|
31
14
|
get pidy() { return this.settings.y || 'y'; }
|
|
32
|
-
get
|
|
33
|
-
get color() { return this.settings.color || '#7b4c98'; }
|
|
15
|
+
get pidlabel() { return this.settings.label || 'label'; }
|
|
34
16
|
|
|
35
17
|
get padding() {
|
|
36
18
|
return this.settings.padding
|
|
37
19
|
|| { top: 3, right: 0, bottom: 10, left: 30 };
|
|
38
20
|
}
|
|
39
21
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
}
|
|
22
|
+
renderView() {
|
|
23
|
+
// cleanup
|
|
24
|
+
while (this.viewRoot.lastChild.name !== 'view') { // <slot />
|
|
25
|
+
this.viewRoot.removeChild(this.viewRoot.lastChild);
|
|
26
|
+
}
|
|
59
27
|
|
|
60
|
-
/**
|
|
61
|
-
* Called when the element is connected to a DOM.
|
|
62
|
-
* Runs any required rendering.
|
|
63
|
-
*/
|
|
64
|
-
connectedCallback() {
|
|
65
28
|
this.width = this.parentElement.clientWidth;
|
|
66
29
|
this.height = this.parentElement.clientHeight;
|
|
67
30
|
const svgWidth = this.width + this.padding.right + this.padding.left;
|
|
68
31
|
const svgHeight = this.height + this.padding.top + this.padding.bottom;
|
|
69
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
|
+
|
|
70
44
|
const container = document.createElement('div');
|
|
71
|
-
this.
|
|
45
|
+
this.viewRoot.appendChild(container);
|
|
72
46
|
const svg = d3.select(container)
|
|
73
47
|
.append('svg')
|
|
74
48
|
.attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`)
|
|
75
49
|
;
|
|
76
|
-
const
|
|
50
|
+
const viewport = this.viewport;
|
|
77
51
|
|
|
78
52
|
// create a dot for each item in this.items
|
|
79
53
|
svg.selectAll('circle')
|
|
80
|
-
.data(this.items)
|
|
54
|
+
.data(this.db.items)
|
|
81
55
|
.enter()
|
|
82
56
|
.append('circle')
|
|
83
|
-
.attr('cx', (item) => vx(this.getItemX(item)))
|
|
84
|
-
.attr('cy', (item) => vy(this.getItemY(item)))
|
|
57
|
+
.attr('cx', (item) => viewport.vx(this.getItemX(item)))
|
|
58
|
+
.attr('cy', (item) => viewport.vy(this.getItemY(item)))
|
|
85
59
|
.attr('r', (item) => this.getItemRadius(item))
|
|
86
60
|
.attr('fill', (item) => this.getItemColor(item))
|
|
87
61
|
.append('title')
|
|
@@ -90,9 +64,17 @@ class ScatterPlot extends HTMLElement {
|
|
|
90
64
|
svg.selectAll('circle')
|
|
91
65
|
.on('click', (event) => console.log(event));
|
|
92
66
|
|
|
93
|
-
|
|
94
|
-
|
|
67
|
+
this.addAxes(svg, viewport);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
addAxes(svg, viewport) {
|
|
95
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);
|
|
96
78
|
svg.append('g')
|
|
97
79
|
.attr('transform', `translate(0,${this.height-this.padding.top})`)
|
|
98
80
|
.call(xAxis)
|
|
@@ -101,8 +83,11 @@ class ScatterPlot extends HTMLElement {
|
|
|
101
83
|
.attr('y', 0)
|
|
102
84
|
.attr('fill', 'black')
|
|
103
85
|
.attr('text-anchor', 'end')
|
|
104
|
-
.text(
|
|
86
|
+
.text(label))
|
|
105
87
|
;
|
|
88
|
+
}
|
|
89
|
+
addYAxis(svg, viewport, label) {
|
|
90
|
+
var yAxis = d3.axisLeft(viewport.vy).ticks(2);
|
|
106
91
|
svg.append('g')
|
|
107
92
|
.attr('transform', `translate(${this.padding.left},0)`)
|
|
108
93
|
.call(yAxis)
|
|
@@ -111,7 +96,7 @@ class ScatterPlot extends HTMLElement {
|
|
|
111
96
|
.attr('y', '1em')
|
|
112
97
|
.attr('fill', 'currentColor')
|
|
113
98
|
.attr('text-anchor', 'start')
|
|
114
|
-
.text(
|
|
99
|
+
.text(label))
|
|
115
100
|
;
|
|
116
101
|
}
|
|
117
102
|
|
|
@@ -138,8 +123,8 @@ class ScatterPlot extends HTMLElement {
|
|
|
138
123
|
* @returns {Viewport} This viewer current viewport
|
|
139
124
|
*/
|
|
140
125
|
get viewport() {
|
|
141
|
-
const xMax = d3.max(this.items, (item) => this.getItemX(item));
|
|
142
|
-
const yMax = d3.max(this.items, (item) => this.getItemY(item));
|
|
126
|
+
const xMax = d3.max(this.db.items, (item) => this.getItemX(item));
|
|
127
|
+
const yMax = d3.max(this.db.items, (item) => this.getItemY(item));
|
|
143
128
|
// a scale is a function, domain is its input and range is its output
|
|
144
129
|
const vx = d3.scaleLinear()
|
|
145
130
|
.domain([0, xMax])
|
|
@@ -159,15 +144,15 @@ class ScatterPlot extends HTMLElement {
|
|
|
159
144
|
getItemY(item) {
|
|
160
145
|
return this.getItemMetadata(item, this.pidy);
|
|
161
146
|
}
|
|
162
|
-
getItemRadius(item) {
|
|
147
|
+
getItemRadius(item) { // eslint-disable-line no-unused-vars
|
|
163
148
|
return 5;
|
|
164
149
|
}
|
|
165
|
-
getItemColor(item) {
|
|
150
|
+
getItemColor(item) { // eslint-disable-line no-unused-vars
|
|
166
151
|
return this.color;
|
|
167
152
|
}
|
|
168
153
|
getItemLabel(item) {
|
|
169
|
-
const name = this.getItemMetadata(item, this.
|
|
170
|
-
return name ? name : `Missing property '${this.
|
|
154
|
+
const name = this.getItemMetadata(item, this.pidlabel);
|
|
155
|
+
return name ? name : `Missing property '${this.pidlabel}'`;
|
|
171
156
|
}
|
|
172
157
|
getAxesLabels() {
|
|
173
158
|
const px = this.getProperty(this.pidx);
|
|
@@ -183,11 +168,13 @@ class ScatterPlot extends HTMLElement {
|
|
|
183
168
|
return found ? found.value : null;
|
|
184
169
|
}
|
|
185
170
|
getProperty(pid) {
|
|
186
|
-
const found = this.properties.find((p) => p['
|
|
171
|
+
const found = this.db.properties.find((p) => p['id'] === pid);
|
|
187
172
|
return found ? found : null;
|
|
188
173
|
}
|
|
189
174
|
}
|
|
190
175
|
|
|
191
176
|
|
|
192
177
|
|
|
178
|
+
customElements.define('hecate-scatterplot', ScatterPlot);
|
|
179
|
+
|
|
193
180
|
export { ScatterPlot };
|
|
@@ -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
|
+
// category1 category2 category3 …
|
|
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 category = this.getItemCategory(item);
|
|
59
|
+
if (!this._table_pivot.has(x)) this._table_pivot.set(x, new Map());
|
|
60
|
+
this._table_pivot.get(x).set(category, y);
|
|
61
|
+
}
|
|
62
|
+
// (2) compute categories
|
|
63
|
+
// [ category1, category2, category 3, … ]
|
|
64
|
+
const cats = new Set();
|
|
65
|
+
for (const [, values] of this._table_pivot) {
|
|
66
|
+
for (const category of values.keys()) {
|
|
67
|
+
cats.add(category);
|
|
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], category) => values.get(category) ?? 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 };
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { StackedAreaChart } from './stackedareachart.js';
|
|
2
|
+
import * as d3 from 'd3';
|
|
3
|
+
|
|
4
|
+
class Streamgraph extends StackedAreaChart {
|
|
5
|
+
|
|
6
|
+
createYDomain(parsers) {
|
|
7
|
+
return d3.extent(this._stacked_series.flat(2))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
createStackOffset() { return d3.stackOffsetWiggle; }
|
|
11
|
+
createStackOrder() { return d3.stackOrderInsideOut; }
|
|
12
|
+
|
|
13
|
+
renderVAxis(svg, plot, axis) {
|
|
14
|
+
const xOffset = plot.padding.left;
|
|
15
|
+
const yOffset = plot.padding.top;
|
|
16
|
+
// append the y-axis, while extending 0% & 100% ticks to become top
|
|
17
|
+
// and bottom ticker "main" lines. Add its label, too.
|
|
18
|
+
axis.attr('transform', `translate(${xOffset},${yOffset})`)
|
|
19
|
+
.call(
|
|
20
|
+
d3.axisLeft(plot.vy)
|
|
21
|
+
.ticks(plot.height / 80)
|
|
22
|
+
.tickFormat((d) => Math.abs(d).toLocaleString('en-US'))
|
|
23
|
+
)
|
|
24
|
+
.call(g => g.select('.domain').remove())
|
|
25
|
+
.call(
|
|
26
|
+
g => g.selectAll('.tick line')
|
|
27
|
+
.clone()
|
|
28
|
+
.attr('x2', plot.width)
|
|
29
|
+
.attr('stroke-opacity', 0.1)
|
|
30
|
+
);
|
|
31
|
+
svg.append(() => axis.node());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
customElements.define('hecate-streamgraph', Streamgraph);
|
|
38
|
+
|
|
39
|
+
export { Streamgraph };
|
package/src/components/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { Sample } from './sample.js';
|
package/src/components/sample.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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 };
|