@cnrs/hecate-views-charts 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,32 @@
1
- # hecate-views
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
+ [![license](https://img.shields.io/badge/license-AGPL3.0-informational?logo=gnu&color=success)](https://www.gnu.org/licenses/agpl-3.0.html)
6
+ [![repostatus](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
7
+ [![pipeline](https://gitlab.huma-num.fr/datasphere/hecate/views/charts/badges/main/pipeline.svg)](https://gitlab.huma-num.fr/datasphere/hecate/views/charts/pipelines/latest)
8
+ [![coverage](https://gitlab.huma-num.fr/datasphere/hecate/views/charts/badges/main/coverage.svg)](https://datasphere.gitpages.huma-num.fr/hecate/views/charts/coverage/)
9
+ [![latest](https://img.shields.io/npm/v/@cnrs/hecate-views-charts.svg)](https://www.npmjs.com/package/@cnrs/hecate-views-charts)
10
+ [![documentation](https://img.shields.io/badge/documentation-jsdoc-blue)](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** is a free Javascript library for visualizing scientific data.
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
- * [Releases](https://www.npmjs.com/package/@cnrs/hecate-views?activeTab=versions)
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
- "name": "@cnrs/hecate-views-charts",
13
- "description": "Views web components available to any web app.",
14
+ "type": "module",
15
+ "main": "src/index.js",
16
+ "exports": {
17
+ ".": "./src/index.js"
18
+ },
19
+ "files": [
20
+ "src/**/*.js"
21
+ ],
22
+ "version": "0.5.0",
14
23
  "keywords": [
15
24
  "dataviz",
16
25
  "research",
@@ -21,17 +30,7 @@
21
30
  "browser",
22
31
  "library"
23
32
  ],
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",
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-c8": [
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
- "To use eslint ECMAScript linter.",
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": "<=12.1.5",
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 for production in `dist` folder.",
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 dependency)",
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
- "lint": "eslint \"src/**/*.js\"",
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 { 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);
1
+ import {
2
+ BarChart,
3
+ Histogram,
4
+ LineChart,
5
+ ScatterChart,
6
+ StackedAreaChart,
7
+ Streamgraph,
8
+ } from './views/index.js'; // eslint-disable-line no-unused-vars
package/src/util.js ADDED
@@ -0,0 +1,13 @@
1
+ function getMetadata(item, pid) {
2
+ for (const meta of item['metadata']) {
3
+ if (pid != null && meta['pid'] == pid) return meta['value'];
4
+ }
5
+ return null;
6
+ }
7
+ function getProperty(db, pid) { // TODO why can't I use await db.getProperty?
8
+ // faster than return db.properties.filter(e => e.id == pid)[0];
9
+ // we use find(...), which stops as soon as it finds our special something
10
+ return db.properties.find(e => e.id == pid);
11
+ }
12
+
13
+ export { getProperty, getMetadata, };
@@ -0,0 +1,6 @@
1
+ import './barchart.js';
2
+ import './histogram.js';
3
+ import './linechart.js';
4
+ import './scatterchart.js';
5
+ import './stackedareachart.js';
6
+ import './streamgraph.js';
@@ -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 HTMLElement{
10
+ class BarChart extends View {
9
11
 
10
- constructor(){
11
- super();
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
- 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
-
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
- * 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
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(rawData, rawSettings){
40
+ prepareData() {
84
41
  var barValues = [];
85
42
  var barLabels = [];
86
- rawData.items.forEach(item => {
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 = this.createShadowDom();
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['@id'] === pid);
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,309 @@
1
+ import { View } from '@cnrs/hecate-views';
2
+ import * as d3 from 'd3';
3
+ import { getProperty, getMetadata, } from '../util.js';
4
+
5
+
6
+
7
+ class CartesianChart extends View {
8
+
9
+ get vertical() { return this.settings.orientation === 'vertical' || false; }
10
+ get hEncoding() { return this.vertical? this.settings.y : this.settings.x; }
11
+ get vEncoding() { return this.vertical? this.settings.x : this.settings.y; }
12
+ get reverseX() { return this.settings.x?.axis?.reverse === true || false; }
13
+ get reverseY() { return this.settings.y?.axis?.reverse === true || false; }
14
+ get normalizeX() { return this.settings.x?.normalize === true || false; }
15
+ get normalizeY() { return this.settings.y?.normalize === true || false; }
16
+ get normalizeH() { return this.hEncoding?.normalize === true || false; }
17
+ get normalizeV() { return this.vEncoding?.normalize === true || false; }
18
+
19
+ getItemX(item) { return getMetadata(item, this.settings.x?.pid); }
20
+ getItemY(item) { return getMetadata(item, this.settings.y?.pid); }
21
+ getItemColor(item) { return getMetadata(item, this.settings.color?.pid); }
22
+
23
+ plotX(item, plot, parsers) {
24
+ return plot.vx(parsers.x(this.getItemX(item))) + plot.padding.left;
25
+ }
26
+ plotY(item, plot, parsers) {
27
+ return plot.vy(parsers.y(this.getItemY(item))) + plot.padding.top;
28
+ }
29
+ plotColor(item, plot, parsers) {
30
+ const color = plot.color(parsers.color(this.getItemColor(item)));
31
+ return color ? color : this.settings.color?.value;
32
+ }
33
+
34
+ renderView() {
35
+ // cleanup
36
+ while (this.viewRoot.lastChild.name !== 'view') { // <slot />
37
+ this.viewRoot.removeChild(this.viewRoot.lastChild);
38
+ }
39
+
40
+ const container = document.createElement('div');
41
+ container.style = 'width: 100%; height: 100%;';
42
+ this.viewRoot.appendChild(container);
43
+
44
+ const xProperty = getProperty(this.db, this.settings.x?.pid);
45
+ const yProperty = getProperty(this.db, this.settings.y?.pid);
46
+ const cProperty = getProperty(this.db, this.settings.color?.pid);
47
+ const xType = xProperty?.type || 'text';
48
+ const yType = yProperty?.type || 'text';
49
+ const cType = cProperty?.type || 'text';
50
+ const parsers = {
51
+ x: getParser(xType),
52
+ y: getParser(yType),
53
+ color: getParser(cType),
54
+ };
55
+ const scales = {
56
+ x: getScale(xType),
57
+ y: getScale(yType),
58
+ color: getScale(cType),
59
+ };
60
+ const plot = {
61
+ width: this.clientWidth,
62
+ height: this.clientHeight,
63
+ };
64
+
65
+ this.prepare(parsers, scales);
66
+
67
+ // Prepare the scales for positional and color encodings.
68
+ // domain = data ; range = render ... cuz a function has:
69
+ // - a domain (its input values),
70
+ // - a codomain (theorical output values), and
71
+ // - a range (effective output values)
72
+ plot.vx = this.createXScale(plot, parsers, scales.x);
73
+ plot.vy = this.createYScale(plot, parsers, scales.y);
74
+ plot.color = this.createColorScale(parsers, scales.color);
75
+ // if this.vertical is true, X axis becomes vertical and Y axis horizontal
76
+ // so we swap them, /but/ they stay named as vx,vy (and not eg. vh,vv)
77
+ // because they really are data scales, and data channels are X,Y
78
+ if (this.vertical) [plot.vx, plot.vy] = [plot.vy, plot.vx];
79
+ // plot.padding is the internal space reserved around the
80
+ // cartesian plotting area, incuding axes, ticks and labels.
81
+ const { hAxis, vAxis, padding } = this.createAxes(plot);
82
+ plot.padding = padding;
83
+
84
+ // Create the SVG container.
85
+ const svgWidth = plot.width + plot.padding.left + plot.padding.right;
86
+ const svgHeight = plot.height + plot.padding.top + plot.padding.bottom;
87
+ const svg = d3.create('svg')
88
+ .attr('width', svgWidth)
89
+ .attr('height', svgHeight)
90
+ .attr('viewBox', [0, 0, svgWidth, svgHeight])
91
+ .attr('style', 'max-width: 100%; height: auto;');
92
+
93
+ // render data
94
+ this.renderData(svg, plot, parsers);
95
+ // render axes
96
+ this.renderHAxis(svg, plot, hAxis);
97
+ this.renderVAxis(svg, plot, vAxis);
98
+
99
+ container.appendChild(svg.node());
100
+ }
101
+
102
+ prepare(parsers, scales) { }
103
+
104
+ renderData(svg, plot, parsers) {
105
+ //const { x, y, color } = this.settings;
106
+ const message = //([x, y, color].every(v => v !== undefined)) ?
107
+ `${this.constructor.name} should implement renderData()`
108
+ // : "Missing settings"
109
+ ;
110
+ svg.append('text')
111
+ .attr('x', plot.width / 2 + plot.padding.left)
112
+ .attr('y', plot.height / 2 + plot.padding.top)
113
+ .attr('text-anchor', 'middle')
114
+ .attr('fill', 'currentColor')
115
+ .text(`${this.constructor.name} should implement renderData()`);
116
+ }
117
+
118
+ renderHAxis(svg, plot, axis) {
119
+ const xOffset = plot.padding.left;
120
+ const yOffset = plot.height + plot.padding.top;
121
+ axis.attr('transform', `translate(${xOffset},${yOffset})`);
122
+ if (this.hEncoding?.axis?.domain === false)
123
+ axis.call(g => g.select('.domain').remove());
124
+ svg.append(() => axis.node());
125
+ }
126
+
127
+ renderVAxis(svg, plot, axis) {
128
+ const xOffset = plot.padding.left;
129
+ const yOffset = plot.padding.top;
130
+ axis.attr('transform', `translate(${xOffset},${yOffset})`);
131
+ if (this.vEncoding?.axis?.domain === false)
132
+ axis.call(g => g.select('.domain').remove());
133
+ svg.append(() => axis.node());
134
+ }
135
+
136
+ createXScale(plot, parsers, scale) {
137
+ let domain = this.createXDomain(parsers);
138
+ if (this.reverseX) domain = [...domain].reverse();
139
+ const range = this.createXRange(plot);
140
+ return scale().domain(domain).range(range);
141
+ }
142
+ createYScale(plot, parsers, scale) {
143
+ let domain = this.createYDomain(parsers);
144
+ if (this.reverseY) domain = [...domain].reverse();
145
+ const range = this.createYRange(plot);
146
+ return scale().domain(domain).range(range);
147
+ }
148
+ createColorScale(parsers, scale) {
149
+ const cats = new Set();
150
+ for (const item of this.db.items) cats.add(this.getItemColor(item));
151
+ const categories = [...cats]; // Set -> Array
152
+ const domain = this.createColorDomain(categories);
153
+ const range = this.createColorRange();
154
+ return scale().domain(domain).range(range);
155
+ }
156
+
157
+ createXDomain(parsers) {
158
+ return d3.extent(this.db.items, item => parsers.x(this.getItemX(item)));
159
+ }
160
+ createYDomain(parsers) {
161
+ return d3.extent(this.db.items, item => parsers.y(this.getItemY(item)));
162
+ }
163
+ createColorDomain(categories) { return categories; }
164
+ createXRange(plot) { return this.vertical ? [plot.height, 0] : [0, plot.width]; }
165
+ createYRange(plot) { return this.vertical ? [0, plot.width] : [plot.height, 0]; }
166
+ createColorRange() { return d3.schemeTableau10; }
167
+ // TODO: provide a fallback palette or generated colors
168
+ // when categories.length > 10 (d3 reuses colors)
169
+
170
+ createAxes(plot) {
171
+ // create a temporary SVG, only for axes bounding box computation
172
+ // these computations allow to deduce necessary paddings
173
+ const tmp = d3.select(this.viewRoot).append('svg')
174
+ .style('position', 'absolute').style('visibility', 'hidden')
175
+ ;
176
+ // create horizontal and vertical axes and add them to temporary svg
177
+ const hAxis = this.createHAxis(tmp, plot);
178
+ const vAxis = this.createVAxis(tmp, plot);
179
+
180
+ const hAxisBBox = hAxis.node().getBBox();
181
+ const vAxisBBox = vAxis.node().getBBox();
182
+
183
+ // space around the cartesian plotting area (inc. axes, ticks and labels).
184
+ const padding = {
185
+ top: -vAxisBBox.y, // overflow of first vertical tick label
186
+ right: hAxisBBox.x + hAxisBBox.width - plot.width, // overflow of last horizontal tick label
187
+ bottom: hAxisBBox.height, // room for horizontal axis, inc. ticks and labels
188
+ left: -vAxisBBox.x, // room for vertical axis, inc. ticks and labels
189
+ };
190
+
191
+ // add axes labels (if any) and update `padding` (if so)
192
+ if (this.hEncoding?.axis?.name) { // create label for horizontal axis `hAxis`
193
+ const hLabel = this.createHAxisLabel(hAxis, this.hEncoding.axis.name);
194
+ const { x,y } = this.createHAxisLabelOffset(plot, padding, hLabel);
195
+ hLabel.attr('x', x).attr('y', y);
196
+ }
197
+ if (this.vEncoding?.axis?.name) { // create label for vertical axis `vAxis`
198
+ const vLabel = this.createVAxisLabel(vAxis, this.vEncoding.axis.name);
199
+ const { x,y } = this.createVAxisLabelOffset(plot, padding, vLabel);
200
+ vLabel.attr('x', x).attr('y', y);
201
+ }
202
+
203
+ // padding computations are done, so delete `tmp` svg ; after that,
204
+ // hAxis,vAxis elements are orphaned, allowing them to be added to the
205
+ // real svg `svg`, below
206
+ tmp.remove();
207
+
208
+ return { hAxis, vAxis, padding };
209
+ }
210
+
211
+ /**
212
+ * Creates the horizontal axis and adds it to the SVG.
213
+ * @param {d3.Selection} svg SVG element.
214
+ * @param {Plot} plot Coordinate system for `svg`.
215
+ * @returns {d3.Selection} X axis SVG element.
216
+ */
217
+ createHAxis(svg, plot) {
218
+ const scale = plot.vx;
219
+ const axis = d3.axisBottom(scale);
220
+ if (this.normalizeH) axis.tickFormat(d3.format(".0%"));
221
+ return svg.append('g').call(axis);
222
+ }
223
+
224
+ createVAxis(svg, plot) {
225
+ const scale = plot.vy;
226
+ const axis = d3.axisLeft(scale);
227
+ if (this.normalizeV) axis.tickFormat(d3.format(".0%"));
228
+ return svg.append('g').call(axis);
229
+ }
230
+
231
+ /**
232
+ * Creates the horizontal axis label, and add it to the horizontal axis.
233
+ * @param {d3.Selection} axis Horizontal axis SVG element (eg. a `<g>`).
234
+ * @param {string} label Horizontal axis label string.
235
+ * @returns {d3.Selection} Horizontal axis label SVG element.
236
+ */
237
+ createHAxisLabel(axis, label) {
238
+ return axis.append('text')
239
+ .attr('dominant-baseline', 'hanging')
240
+ .attr('fill', 'currentColor')
241
+ .attr('text-anchor', 'end')
242
+ .text(label);
243
+ }
244
+
245
+ createVAxisLabel(axis, label) {
246
+ return axis.append('text')
247
+ .attr('dominant-baseline', 'hanging')
248
+ .attr('fill', 'currentColor')
249
+ .attr('text-anchor', 'start')
250
+ .text(label);
251
+ }
252
+
253
+ createHAxisLabelOffset(plot, padding, label) {
254
+ const labelBBox = label.node().getBBox();
255
+ // increase the bottom padding so the label fits inside svg viewBox
256
+ padding.bottom += labelBBox.height;
257
+ // position the label so its top aligns with the ticks labels bottom
258
+ return {
259
+ x: plot.width + padding.right,
260
+ y: padding.bottom -labelBBox.height,
261
+ };
262
+ }
263
+
264
+ createVAxisLabelOffset(plot, padding, label) {
265
+ const labelBBox = label.node().getBBox();
266
+ const yMax = d3.max(plot.vy.domain());
267
+ const yTop = plot.vy(plot.vy.domain()[1]);
268
+ // increase the top padding so the label fits inside svg viewBox
269
+ padding.top = labelBBox.height - yTop;
270
+ // position the label so its bottom aligns with the upper tick label top
271
+ return {
272
+ x: -padding.left,
273
+ y: yTop - labelBBox.height,
274
+ };
275
+ }
276
+ }
277
+
278
+
279
+
280
+ const PARSERS = {
281
+ 'date': d => new Date(d),
282
+ 'number': d => +d,
283
+ 'text': d => d,
284
+ };
285
+ const SCALES = {
286
+ 'date': d3.scaleUtc,
287
+ 'number': d3.scaleLinear,
288
+ 'text': d3.scaleOrdinal,
289
+ };
290
+
291
+ function getParser(type) { return PARSERS[type]; }
292
+ function getScale(type) { return SCALES[type]; }
293
+
294
+ export { CartesianChart };
295
+
296
+ /**
297
+ * Cartesian plotting area.
298
+ *
299
+ * This object defines the coordinate system used to render a chart.
300
+ *
301
+ * @typedef {Object} Plot
302
+ * @property {number} width - Width of the drawable plotting area, excluding padding.
303
+ * @property {number} height - Height of the drawable plotting area, excluding padding.
304
+ * @property {Function} vx - Maps X values from data space to view space.
305
+ * @property {Function} vy - Maps Y values from data space to view space.
306
+ * @property {{top:number,right:number,bottom:number,left:number}} [padding]
307
+ * Internal space reserved for axes, ticks and labels.
308
+ * TODO
309
+ */