@cnrs/hecate-views-charts 0.4.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/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "files": [
20
20
  "src/**/*.js"
21
21
  ],
22
- "version": "0.4.0",
22
+ "version": "0.5.0",
23
23
  "keywords": [
24
24
  "dataviz",
25
25
  "research",
package/src/init.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  BarChart,
3
3
  Histogram,
4
- ScatterPlot,
5
4
  LineChart,
5
+ ScatterChart,
6
6
  StackedAreaChart,
7
7
  Streamgraph,
8
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, };
package/src/views/all.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import './barchart.js';
2
2
  import './histogram.js';
3
3
  import './linechart.js';
4
- import './scatterplot.js';
4
+ import './scatterchart.js';
5
5
  import './stackedareachart.js';
6
+ import './streamgraph.js';
@@ -1,5 +1,6 @@
1
1
  import { View } from '@cnrs/hecate-views';
2
2
  import * as d3 from 'd3';
3
+ import { getProperty, getMetadata, } from '../util.js';
3
4
 
4
5
 
5
6
 
@@ -17,7 +18,18 @@ class CartesianChart extends View {
17
18
 
18
19
  getItemX(item) { return getMetadata(item, this.settings.x?.pid); }
19
20
  getItemY(item) { return getMetadata(item, this.settings.y?.pid); }
20
- getItemCategory(item) { return getMetadata(item, this.settings.category?.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
+ }
21
33
 
22
34
  renderView() {
23
35
  // cleanup
@@ -31,7 +43,7 @@ class CartesianChart extends View {
31
43
 
32
44
  const xProperty = getProperty(this.db, this.settings.x?.pid);
33
45
  const yProperty = getProperty(this.db, this.settings.y?.pid);
34
- const cProperty = getProperty(this.db, this.settings.category?.pid);
46
+ const cProperty = getProperty(this.db, this.settings.color?.pid);
35
47
  const xType = xProperty?.type || 'text';
36
48
  const yType = yProperty?.type || 'text';
37
49
  const cType = cProperty?.type || 'text';
@@ -90,8 +102,8 @@ class CartesianChart extends View {
90
102
  prepare(parsers, scales) { }
91
103
 
92
104
  renderData(svg, plot, parsers) {
93
- //const { x, y, category } = this.settings;
94
- const message = //([x, y, c].every(v => v !== undefined)) ?
105
+ //const { x, y, color } = this.settings;
106
+ const message = //([x, y, color].every(v => v !== undefined)) ?
95
107
  `${this.constructor.name} should implement renderData()`
96
108
  // : "Missing settings"
97
109
  ;
@@ -135,15 +147,19 @@ class CartesianChart extends View {
135
147
  }
136
148
  createColorScale(parsers, scale) {
137
149
  const cats = new Set();
138
- for (const item of this.db.items) cats.add(this.getItemCategory(item));
150
+ for (const item of this.db.items) cats.add(this.getItemColor(item));
139
151
  const categories = [...cats]; // Set -> Array
140
152
  const domain = this.createColorDomain(categories);
141
153
  const range = this.createColorRange();
142
154
  return scale().domain(domain).range(range);
143
155
  }
144
156
 
145
- createXDomain(parsers) { return [0, 1]; }
146
- createYDomain(parsers) { return [0, 1]; }
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
+ }
147
163
  createColorDomain(categories) { return categories; }
148
164
  createXRange(plot) { return this.vertical ? [plot.height, 0] : [0, plot.width]; }
149
165
  createYRange(plot) { return this.vertical ? [0, plot.width] : [plot.height, 0]; }
@@ -275,16 +291,6 @@ const SCALES = {
275
291
  function getParser(type) { return PARSERS[type]; }
276
292
  function getScale(type) { return SCALES[type]; }
277
293
 
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
294
  export { CartesianChart };
289
295
 
290
296
  /**
@@ -1,7 +1,7 @@
1
1
  export * from './barchart.js';
2
2
  export * from './histogram.js';
3
3
  export * from './linechart.js';
4
- export * from './scatterplot.js';
4
+ export * from './scatterchart.js';
5
5
  export * from './stackedareachart.js';
6
6
  export * from './streamgraph.js';
7
7
 
@@ -1,4 +1,4 @@
1
- import { View } from '@cnrs/hecate-views';
1
+ import { CartesianChart } from './cartesianchart.js';
2
2
  import * as d3 from 'd3';
3
3
 
4
4
 
@@ -7,192 +7,91 @@ import * as d3 from 'd3';
7
7
  * LineChart web component.
8
8
  * @see https://en.wikipedia.org/wiki/Line_chart
9
9
  */
10
- class LineChart extends View {
11
-
12
- get pidx() { return this.settings.x || 'x'; }
13
- get pidy() { return this.settings.y || 'y'; }
14
-
15
- get padding() {
16
- return this.settings.padding
17
- || { top: 70, right: 30, bottom: 40, left: 80};
18
- }
19
-
20
- renderView() {
21
- // cleanup
22
- while (this.viewRoot.lastChild.name !== 'view') { // <slot />
23
- this.viewRoot.removeChild(this.viewRoot.lastChild);
10
+ class LineChart extends CartesianChart {
11
+
12
+ get style() { return this.settings.style; }
13
+ get curve() { return CURVES[this.settings.mark?.curve] ?? d3.curveMonotoneX; }
14
+
15
+ prepare() {
16
+ const map = new Map();
17
+ if (this.settings.color?.pid) {
18
+ // mode: multiple path lines
19
+ this._multiple = true;
20
+ for (const item of this.db.items) {
21
+ const key = this.getItemColor(item);
22
+ const values = map.get(key) || [];
23
+ values.push(item);
24
+ map.set(key, values);
25
+ }
26
+ } else {
27
+ // mode: single path line
28
+ this._multiple = false;
29
+ const key = this.settings.mark?.tooltip || null;
30
+ map.set(key, this.db.items);
24
31
  }
25
-
26
- var dataset = this.prepareData();
27
-
28
- var {sceneWidth, sceneHeight, svg} = this.createscene(this.padding, 600, 1200);
29
-
30
- var {x_scale, y_scale} = this.createAxisScales(sceneHeight, sceneWidth, dataset)
31
- this.addAxises(sceneWidth, sceneHeight, this.padding, x_scale, y_scale, svg);
32
-
33
- this.displayChart(x_scale, y_scale, dataset, svg);
32
+ this._items_by_c = map;
34
33
  }
35
34
 
36
- /**
37
- * Prepare the data by giving it the correct format
38
- * Creates a javascript object that contains the elements of metadata, each key representing a different pid
39
- * @param {Hera} data - Hera-formatted data parameter
40
- * @param {Object} settings - Settings parameter
41
- * @returns {Object} JS object containing the elements of metadata,
42
- * x (resp. y) key contains the value for x (resp. y) axis
43
- */
44
- prepareData() {
45
- var yValues = [];
46
- var xValues = [];
47
- this.db.items.forEach(item => {
48
- item.metadata.forEach(meta => {
49
- if (meta.pid === this.pidy) {
50
- yValues.push(parseInt(meta.value));
51
- }
52
- if (meta.pid === this.pidx){
53
- xValues.push(parseInt(meta.value));
54
- }
55
- });
56
- });
57
- const dataset = xValues.map((x, index) => ({
58
- x: x,
59
- y: yValues[index]
60
- }));
61
- return dataset;
62
- }
63
-
64
- /**
65
- * Creates a svg element that will contain the viewer.
66
- * @param {Object} margin - Space for the axes
67
- * @param {number} width - Desired int width for the svg
68
- * @param {number} height - Desired int height for the svg
69
- * @returns The svg and its height, width and the width of each bars
70
- */
71
- createscene(margin, height, width){
72
- const lineWidth = width - margin.left - margin.right;
73
- const lineHeight = height - margin.top - margin.bottom;
74
-
75
- // create svg and append it
76
- const divElem = document.createElement('div');
77
- this.viewRoot.appendChild(divElem);
78
- const container = d3.select(divElem);
79
35
 
80
- const svg = container.append("svg")
81
- .attr("width", lineWidth + margin.left + margin.right)
82
- .attr("height", lineHeight + margin.top + margin.left)
83
- .append("g")
84
- .attr("transform", `translate(${margin.left},${margin.top})`);
85
- return {
86
- sceneWidth : lineWidth,
87
- sceneHeight : lineHeight,
88
- svg : svg
89
- }
90
- }
91
-
92
- /**
93
- * creates and sets x and y linear scales
94
- * @param {*} sceneHeight
95
- * @param {*} sceneWidth
96
- * @param {*} dataset
97
- * @return x and y linear scales
98
- */
99
- createAxisScales(sceneHeight, sceneWidth, dataset){
100
- //set x and y scales
101
- var x_scale = d3.scaleLinear()
102
- .range([0,sceneWidth])
103
- .domain([0,d3.max(dataset, d => d.x)]);
104
- var y_scale = d3.scaleLinear()
105
- .range([sceneHeight,0])
106
- .domain([0,d3.max(dataset, d => d.y)]);
107
-
108
- return{
109
- x_scale : x_scale,
110
- y_scale : y_scale
111
- }
112
-
113
- }
114
-
115
-
116
- /**
117
- * add the left and bottom axises, with their Label
118
- * @param sceneWidth - Scene width
119
- * @param x_scale - Linear scale for the X axis
120
- * @param y_scale - Linear scale for the Y axis
121
- * @param {*} sceneHeight
122
- * @param {*} sceneWidth
123
- * @param {*} margin
124
- * @param {*} svg
125
- */
126
- addAxises(sceneWidth, sceneHeight, margin, x_scale, y_scale, svg)
127
- {
128
- //add axises
129
- var x_axis = d3.axisBottom(x_scale);
130
- var y_axis = d3.axisLeft(y_scale);
131
- const labels = this.getAxesLabels();
132
-
133
- //x axis
134
- svg.append("g")
135
- .attr("transform", `translate(0,${sceneHeight})`)
136
- .call(x_axis)
137
- .call((g) => g.append("text")
138
- .attr("x", sceneWidth)
139
- .attr("y", margin.bottom - 4)
140
- .attr("fill", "currentColor")
141
- .attr("text-anchor", "end")
142
- .text(labels.x));
143
-
144
-
145
- //y axis
146
- svg.append("g")
147
- .call(y_axis)
148
- .call((g) => g.append("text")
149
- .attr("x", -margin.left)
150
- .attr("y",-10)
151
- .attr("fill", "currentColor")
152
- .attr("text-anchor", "start")
153
- .text(labels.y));
36
+ renderData(svg, plot, parsers) {
37
+ const line = d3.line()
38
+ .curve(this.curve)
39
+ .x(item => this.plotX(item, plot, parsers))
40
+ .y(item => this.plotY(item, plot, parsers))
41
+ ;
154
42
 
155
- }
156
- /**
157
- * display the line chart
158
- * @param {*} x_scale
159
- * @param {*} y_scale
160
- * @param {*} dataset
161
- * @param {*} svg
162
- */
163
- displayChart(x_scale, y_scale, dataset, svg){
164
- //draw lines
165
- const line = d3.line()
166
- .x(d => x_scale(d.x))
167
- .y(d => y_scale(d.y));
168
-
169
- //add line path to svg
170
- svg.append("path")
171
- .datum(dataset)
172
- .attr("fill", "none")
173
- .attr("stroke", this.color)
174
- .attr("stroke-width", 1)
175
- .attr("d", line);
176
- }
177
-
178
- getAxesLabels() {
179
- const px = this.getProperty(this.pidx);
180
- const py = this.getProperty(this.pidy);
181
- return {
182
- x: px ? px.name : `Missing property '${this.pidx}'`,
183
- y: py ? py.name : `Missing property '${this.pidy}'`,
184
- };
43
+ for (const [key, items] of this._items_by_c) {
44
+ const color = this._multiple ?
45
+ plot.color(parsers.color(key))
46
+ : this.settings.color?.value || '#7b4c98';
47
+ this.renderPath(svg, items, line, color, key);
48
+ }
185
49
  }
186
50
 
187
- getProperty(pid) {
188
- const found = this.db.properties.find((p) => p['id'] === pid);
189
- return found ? found : null;
51
+ renderPath(svg, items, line, color, text=null) {
52
+ const klass = 'series';
53
+ //add line path to svg
54
+ const path = svg.append('path')
55
+ .datum(items)
56
+ .attr('class', klass)
57
+ .attr('fill', 'none')
58
+ .attr('stroke', color)
59
+ .attr('stroke-width', this.style?.normal?.strokeWidth || 2)
60
+ .attr('opacity', this.style?.normal?.opacity || 2)
61
+ .attr('d', line)
62
+ ;
63
+ // add tooltip appearing when line path is hovered, if any
64
+ if (text) path.append('title').text(text);
65
+ path // embiggen hovered line path to make it more visible
66
+ .on('mouseenter', (event) => {
67
+ svg.selectAll(`.${klass}`)
68
+ .attr('stroke-width', this.style?.nothover?.strokeWidth || 2)
69
+ .attr('opacity', this.style?.nothover?.opacity || 0.25);
70
+ d3.select(event.currentTarget)
71
+ .attr('stroke-width', this.style?.hover?.strokeWidth || 4)
72
+ .attr('opacity', this.style?.hover?.opacity || 1)
73
+ .raise();
74
+ }) // make non-hovered line path less visible
75
+ .on('mouseleave', (event) => {
76
+ svg.selectAll(`.${klass}`)
77
+ .attr('stroke-width', this.style?.normal?.strokeWidth || 2)
78
+ .attr('opacity', this.style?.normal?.opacity || 1);
79
+ });
190
80
  }
191
81
 
192
82
  }
193
83
 
194
84
 
195
85
 
86
+ const CURVES = {
87
+ 'basis': d3.curveBasis,
88
+ 'cardinal': d3.curveCardinal,
89
+ 'catmullrom': d3.curveCatmullRom,
90
+ 'monotoneX': d3.curveMonotoneX,
91
+ 'linear': d3.curveLinear,
92
+ 'step': d3.curveStep,
93
+ };
94
+
196
95
  customElements.define('hecate-linechart', LineChart);
197
96
 
198
97
  export { LineChart };
@@ -0,0 +1,87 @@
1
+ import { CartesianChart } from './cartesianchart.js';
2
+ import * as d3 from 'd3';
3
+
4
+
5
+
6
+ /**
7
+ * Scatterplot web component.
8
+ * @see https://en.wikipedia.org/wiki/Scatter_plot
9
+ * @see https://scottmurray.org/tutorials/d3/
10
+ */
11
+ class ScatterChart extends CartesianChart {
12
+
13
+ prepare() {
14
+ const map = new Map();
15
+ if (this.settings.color?.pid) {
16
+ // mode: multiple path lines
17
+ this._multiple = true;
18
+ for (const item of this.db.items) {
19
+ const key = this.getItemColor(item);
20
+ const values = map.get(key) || [];
21
+ values.push(item);
22
+ map.set(key, values);
23
+ }
24
+ } else {
25
+ // mode: single path line
26
+ this._multiple = false;
27
+ const key = this.settings.mark?.tooltip || null;
28
+ map.set(key, this.db.items);
29
+ }
30
+ this._items_by_c = map;
31
+ }
32
+
33
+ renderData(svg, plot, parsers) {
34
+ for (const [key, items] of this._items_by_c) {
35
+ this.renderGroup(svg, items, key, plot, parsers);
36
+ }
37
+ }
38
+
39
+ renderGroup(svg, items, key, plot, parsers) {
40
+ const klass = 'series';
41
+ const group = svg.append('g').attr('class', klass);//.attr('data-key', key);
42
+ // create a dot for each item in items
43
+ const color = this._multiple ?
44
+ plot.color(parsers.color(key))
45
+ : this.settings.color?.value || '#7b4c98';
46
+ const circles = group.selectAll('circle')
47
+ .data(items)
48
+ .enter()
49
+ .append('circle')
50
+ .attr('cx', item => this.plotX(item, plot, parsers))
51
+ .attr('cy', item => this.plotY(item, plot, parsers))
52
+ .attr('r', item => this.getItemRadius(item))
53
+ .attr('fill', item => color)
54
+ ;
55
+ circles.append('title').text(item => this.getItemLabel(item));
56
+
57
+ group // embiggen hovered line path to make it more visible
58
+ .on('mouseenter', (event) => {
59
+ svg.selectAll(`.${klass}`)
60
+ .attr('opacity', this.style?.nothover?.opacity || 0.25);
61
+ d3.select(event.currentTarget)
62
+ .attr('opacity', this.style?.hover?.opacity || 1)
63
+ .raise();
64
+ }) // make non-hovered line path less visible
65
+ .on('mouseleave', (event) => {
66
+ svg.selectAll(`.${klass}`)
67
+ .attr('opacity', this.style?.normal?.opacity || 1);
68
+ });
69
+ }
70
+
71
+ getItemRadius(item) { // eslint-disable-line no-unused-vars
72
+ return 5;
73
+ }
74
+ getItemLabel(item) {
75
+ const x = this.getItemX(item);
76
+ const y = this.getItemY(item);
77
+ const c = this.getItemColor(item);
78
+ const mc = c ? ` for ${c}` : '';
79
+ return `${y} for ${x}${mc}`
80
+ }
81
+ }
82
+
83
+
84
+
85
+ customElements.define('hecate-scatterchart', ScatterChart);
86
+
87
+ export { ScatterChart };
@@ -46,25 +46,25 @@ class StackedAreaChart extends CartesianChart {
46
46
 
47
47
  prepare(parsers, scales) {
48
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
- // …
49
+ // c1 c2 c3
50
+ // x1 y11 y12 y13
51
+ // x2 y21 y32 y33
52
+ // x3 y31 y32 y33
53
+ // …
54
54
  this._table_pivot = new Map();
55
55
  for (const item of this.db.items) {
56
56
  const x = asKey(parsers.x(this.getItemX(item)));
57
57
  const y = parsers.y(this.getItemY(item));
58
- const category = this.getItemCategory(item);
58
+ const c = this.getItemColor(item);
59
59
  if (!this._table_pivot.has(x)) this._table_pivot.set(x, new Map());
60
- this._table_pivot.get(x).set(category, y);
60
+ this._table_pivot.get(x).set(c, y);
61
61
  }
62
- // (2) compute categories
63
- // [ category1, category2, category 3, … ]
62
+ // (2) compute colors (ie. categories)
63
+ // [ c1, c2, c3, … ]
64
64
  const cats = new Set();
65
65
  for (const [, values] of this._table_pivot) {
66
- for (const category of values.keys()) {
67
- cats.add(category);
66
+ for (const c of values.keys()) {
67
+ cats.add(c);
68
68
  }
69
69
  }
70
70
  this._categories = [...cats];
@@ -74,7 +74,7 @@ class StackedAreaChart extends CartesianChart {
74
74
  .offset(this.createStackOffset())
75
75
  .order(this.createStackOrder())
76
76
  .keys(this._categories)
77
- .value(([, values], category) => values.get(category) ?? 0)(rows);
77
+ .value(([, values], c) => values.get(c) ?? 0)(rows);
78
78
  }
79
79
 
80
80
  createStackOffset() {
@@ -1,180 +0,0 @@
1
- import { View } from '@cnrs/hecate-views';
2
- import * as d3 from 'd3';
3
-
4
-
5
-
6
- /**
7
- * Scatterplot web component.
8
- * @see https://en.wikipedia.org/wiki/Scatter_plot
9
- * @see https://scottmurray.org/tutorials/d3/
10
- */
11
- class ScatterPlot extends View {
12
-
13
- get pidx() { return this.settings.x || 'x'; }
14
- get pidy() { return this.settings.y || 'y'; }
15
- get pidlabel() { return this.settings.label || 'label'; }
16
-
17
- get padding() {
18
- return this.settings.padding
19
- || { top: 3, right: 0, bottom: 10, left: 30 };
20
- }
21
-
22
- renderView() {
23
- // cleanup
24
- while (this.viewRoot.lastChild.name !== 'view') { // <slot />
25
- this.viewRoot.removeChild(this.viewRoot.lastChild);
26
- }
27
-
28
- this.width = this.parentElement.clientWidth;
29
- this.height = this.parentElement.clientHeight;
30
- const svgWidth = this.width + this.padding.right + this.padding.left;
31
- const svgHeight = this.height + this.padding.top + this.padding.bottom;
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
-
44
- const container = document.createElement('div');
45
- this.viewRoot.appendChild(container);
46
- const svg = d3.select(container)
47
- .append('svg')
48
- .attr('viewBox', `0 0 ${svgWidth} ${svgHeight}`)
49
- ;
50
- const viewport = this.viewport;
51
-
52
- // create a dot for each item in this.items
53
- svg.selectAll('circle')
54
- .data(this.db.items)
55
- .enter()
56
- .append('circle')
57
- .attr('cx', (item) => viewport.vx(this.getItemX(item)))
58
- .attr('cy', (item) => viewport.vy(this.getItemY(item)))
59
- .attr('r', (item) => this.getItemRadius(item))
60
- .attr('fill', (item) => this.getItemColor(item))
61
- .append('title')
62
- .text((item) => this.getItemLabel(item))
63
- ;
64
- svg.selectAll('circle')
65
- .on('click', (event) => console.log(event));
66
-
67
- this.addAxes(svg, viewport);
68
- }
69
-
70
-
71
- addAxes(svg, viewport) {
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);
78
- svg.append('g')
79
- .attr('transform', `translate(0,${this.height-this.padding.top})`)
80
- .call(xAxis)
81
- .call((g) => g.append('text')
82
- .attr('x', this.width)
83
- .attr('y', 0)
84
- .attr('fill', 'black')
85
- .attr('text-anchor', 'end')
86
- .text(label))
87
- ;
88
- }
89
- addYAxis(svg, viewport, label) {
90
- var yAxis = d3.axisLeft(viewport.vy).ticks(2);
91
- svg.append('g')
92
- .attr('transform', `translate(${this.padding.left},0)`)
93
- .call(yAxis)
94
- .call((g) => g.append('text')
95
- .attr('x', 0)
96
- .attr('y', '1em')
97
- .attr('fill', 'currentColor')
98
- .attr('text-anchor', 'start')
99
- .text(label))
100
- ;
101
- }
102
-
103
- /**
104
- * @callback scale
105
- * @property {number} n - A number inside the scale's domain
106
- * @see https://d3js.org/d3-scale
107
- */
108
-
109
- /**
110
- * This object is a wrapper for two d3.scale objects, which can be used to
111
- * convert coordinates from the graph space to this viewer viewport space.
112
- * @typedef {Object} Viewport
113
- * @property {scale} vx - A scale returning the x (horizontal) coordinate
114
- * @property {scale} vy - A scale returning the y (vertical) coordinate
115
- * @see https://d3js.org/d3-scale
116
- */
117
-
118
- /**
119
- * Property returning this element viewport.
120
- * The viewport is composed of two scale functions, which can be used to
121
- * convert x (resp. y) to a number between 0 and the width (resp. height)
122
- * of the viewer.
123
- * @returns {Viewport} This viewer current viewport
124
- */
125
- get viewport() {
126
- const xMax = d3.max(this.db.items, (item) => this.getItemX(item));
127
- const yMax = d3.max(this.db.items, (item) => this.getItemY(item));
128
- // a scale is a function, domain is its input and range is its output
129
- const vx = d3.scaleLinear()
130
- .domain([0, xMax])
131
- .range([this.padding.left, this.width-this.padding.right]);
132
- const vy = d3.scaleLinear()
133
- .domain([0, yMax])
134
- .range([this.height-this.padding.top, this.padding.bottom]);
135
- // y is inverted because having the origin (0;0) of the scatter graph
136
- // in the bottom left corner feels more natural than the top left origin
137
- // which is standard in SVG.
138
- return { vx, vy };
139
- }
140
-
141
- getItemX(item) {
142
- return this.getItemMetadata(item, this.pidx);
143
- }
144
- getItemY(item) {
145
- return this.getItemMetadata(item, this.pidy);
146
- }
147
- getItemRadius(item) { // eslint-disable-line no-unused-vars
148
- return 5;
149
- }
150
- getItemColor(item) { // eslint-disable-line no-unused-vars
151
- return this.color;
152
- }
153
- getItemLabel(item) {
154
- const name = this.getItemMetadata(item, this.pidlabel);
155
- return name ? name : `Missing property '${this.pidlabel}'`;
156
- }
157
- getAxesLabels() {
158
- const px = this.getProperty(this.pidx);
159
- const py = this.getProperty(this.pidy);
160
- return {
161
- x: px ? px.name : `Missing property '${this.pidx}'`,
162
- y: py ? py.name : `Missing property '${this.pidy}'`,
163
- };
164
- }
165
-
166
- getItemMetadata(item, pid) {
167
- const found = item.metadata.find((m) => m.pid === pid);
168
- return found ? found.value : null;
169
- }
170
- getProperty(pid) {
171
- const found = this.db.properties.find((p) => p['id'] === pid);
172
- return found ? found : null;
173
- }
174
- }
175
-
176
-
177
-
178
- customElements.define('hecate-scatterplot', ScatterPlot);
179
-
180
- export { ScatterPlot };
File without changes