@peter.prib/renderengine 0.1.0 → 0.2.1
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 +10 -1
- package/REFERENCE.md +42 -3
- package/dist/renderengine-workspace.umd.js +1079 -124
- package/dist/renderengine.umd.js +1079 -124
- package/examples/diagram-editor.html +9 -4
- package/package.json +3 -2
- package/src/chartCatalog.js +49 -4
- package/src/charting/Gauge.js +6 -0
- package/src/charting/charts/ArcGauge.js +55 -0
- package/src/charting/charts/BarGauge.js +32 -11
- package/src/charting/charts/BulletGauge.js +52 -56
- package/src/charting/charts/DialGauge.js +54 -26
- package/src/charting/charts/GaugeChart.js +141 -13
- package/src/charting/charts/KpiTile.js +72 -0
- package/src/charting/charts/MultiRingGauge.js +54 -0
- package/src/charting/charts/RingGauge.js +23 -0
- package/src/charting/charts/ThermometerGauge.js +43 -0
- package/src/charting/gauges/ArcTrack.js +51 -0
- package/src/charting/gauges/GaugeAnimation.js +51 -0
- package/src/charting/gauges/GaugeBands.js +48 -0
- package/src/charting/gauges/GaugeScale.js +87 -0
- package/src/charting/gauges/GaugeTrack.js +44 -0
- package/src/charting/gauges/GaugeValue.js +61 -0
- package/src/charting/gauges/HorizontalTrack.js +14 -0
- package/src/charting/gauges/LinearTrack.js +29 -0
- package/src/charting/gauges/Sparkline.js +42 -0
- package/src/charting/gauges/VerticalTrack.js +17 -0
- package/src/charting/templates/Template.js +0 -6
- package/src/index.js +84 -12
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GaugeValue — what a gauge's data says: the value, and what it is compared
|
|
3
|
+
* with.
|
|
4
|
+
*
|
|
5
|
+
* Data is a plain number, or `{ value, target?, previous?, history?, values? }`;
|
|
6
|
+
* options of the same names win over the data's. `values` is for gauges that
|
|
7
|
+
* show several (one ring each): numbers, or `{ value, label?, color?, min?, max? }`;
|
|
8
|
+
* an array as the data is read as `values`.
|
|
9
|
+
*/
|
|
10
|
+
export class GaugeValue {
|
|
11
|
+
constructor({ value = 0, target = null, previous = null, history = [], values = [] } = {}) {
|
|
12
|
+
this.value = Number(value) || 0;
|
|
13
|
+
this.target = target == null ? null : Number(target);
|
|
14
|
+
this.history = (history ?? []).map(Number).filter(Number.isFinite);
|
|
15
|
+
// The value before this one: given, or the one before the last in the history.
|
|
16
|
+
this.previous = previous != null ? Number(previous) : (this.history.length > 1 ? this.history.at(-2) : null);
|
|
17
|
+
this.values = (values ?? []).map(v => (typeof v === 'object' && v !== null ? { ...v, value: Number(v.value) || 0 } : { value: Number(v) || 0 }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The change from the previous value, or null without one. */
|
|
21
|
+
get change() { return this.previous == null ? null : this.value - this.previous; }
|
|
22
|
+
|
|
23
|
+
/** The change as a fraction of the previous value (0.1 is up 10%), or null. */
|
|
24
|
+
get changeRatio() { return this.previous ? this.change / Math.abs(this.previous) : null; }
|
|
25
|
+
|
|
26
|
+
/** As gauge data again (`{ value, target, previous, history, values }`). */
|
|
27
|
+
toData() {
|
|
28
|
+
const { value, target, previous, history, values } = this;
|
|
29
|
+
return { value, target, previous, history: [...history], values: values.map(v => ({ ...v })) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The data part of the way (t, 0-1) from one value to another: the value,
|
|
34
|
+
* the target and each of several values in between (one that is new
|
|
35
|
+
* starts from the first value); the rest as `to` has them.
|
|
36
|
+
*/
|
|
37
|
+
static between(from, to, t) {
|
|
38
|
+
const mix = (a, b) => (a == null || b == null ? b : a + (b - a) * t);
|
|
39
|
+
const data = to.toData();
|
|
40
|
+
data.value = mix(from.value, to.value);
|
|
41
|
+
data.target = mix(from.target, to.target);
|
|
42
|
+
data.values = to.values.map((v, i) => ({ ...v, value: mix(from.values[i]?.value ?? from.value, v.value) }));
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static from(data, options = {}) {
|
|
47
|
+
const given = Array.isArray(data) ? { values: data } : (typeof data === 'object' && data !== null ? data : { value: data });
|
|
48
|
+
const pick = (key) => options[key] ?? given[key];
|
|
49
|
+
const values = pick('values') ?? [];
|
|
50
|
+
const history = pick('history') ?? [];
|
|
51
|
+
// Without a value: the first of several, else the last of the history.
|
|
52
|
+
const first = values.length ? (typeof values[0] === 'object' ? values[0].value : values[0]) : history.at(-1);
|
|
53
|
+
return new GaugeValue({
|
|
54
|
+
value: given.value ?? first ?? 0,
|
|
55
|
+
target: pick('target'),
|
|
56
|
+
previous: pick('previous'),
|
|
57
|
+
history,
|
|
58
|
+
values,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LinearTrack } from './LinearTrack.js';
|
|
2
|
+
|
|
3
|
+
/** A track left to right. */
|
|
4
|
+
export class HorizontalTrack extends LinearTrack {
|
|
5
|
+
point(fraction, offset = 0) {
|
|
6
|
+
return { x: this.x + fraction * this.length, y: this.y + this.thickness / 2 + offset };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
rectangle(from, to, thickness) {
|
|
10
|
+
return { x: this.x + from * this.length, y: this.y + (this.thickness - thickness) / 2, width: (to - from) * this.length, height: thickness };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get center() { return this.point(0.5); }
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { GaugeTrack } from './GaugeTrack.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LinearTrack — a straight gauge track: a box `length` long and `thickness`
|
|
5
|
+
* across, its segments drawn as rectangles. A subclass gives the direction.
|
|
6
|
+
*/
|
|
7
|
+
export class LinearTrack extends GaugeTrack {
|
|
8
|
+
/**
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {number} options.x - The box's left
|
|
11
|
+
* @param {number} options.y - The box's top
|
|
12
|
+
* @param {number} options.length
|
|
13
|
+
* @param {number} options.thickness
|
|
14
|
+
* @param {number} [options.radius=0] - Corner radius of the drawn segments
|
|
15
|
+
*/
|
|
16
|
+
constructor({ x, y, length, thickness, radius = 0 }) {
|
|
17
|
+
super(thickness);
|
|
18
|
+
if (new.target === LinearTrack) throw new TypeError('LinearTrack is abstract — use HorizontalTrack or VerticalTrack');
|
|
19
|
+
Object.assign(this, { x, y, length, radius });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The rectangle covering from one fraction to another, `thickness` across and centred. */
|
|
23
|
+
rectangle(from, to, thickness) { throw new Error(`${this.constructor.name} must implement rectangle()`); }
|
|
24
|
+
|
|
25
|
+
draw(renderer, from, to, color, { thickness = this.thickness } = {}) {
|
|
26
|
+
const r = this.rectangle(Math.min(from, to), Math.max(from, to), thickness);
|
|
27
|
+
renderer.rect(r.x, r.y, r.width, r.height, color, null, this.radius ? { rx: this.radius, ry: this.radius } : {});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sparkline — a small line of a series' recent values, fitted to a box:
|
|
3
|
+
* the first value at the left, the last at the right, the lowest at the
|
|
4
|
+
* bottom and the highest at the top.
|
|
5
|
+
*/
|
|
6
|
+
export class Sparkline {
|
|
7
|
+
/**
|
|
8
|
+
* @param {number[]} values - At least two to draw a line
|
|
9
|
+
* @param {{x:number, y:number, width:number, height:number}} box
|
|
10
|
+
*/
|
|
11
|
+
constructor(values, box) {
|
|
12
|
+
this.values = values.map(Number).filter(Number.isFinite);
|
|
13
|
+
this.box = box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Each value's point in the box (none for fewer than two values). */
|
|
17
|
+
get points() {
|
|
18
|
+
const { values, box } = this;
|
|
19
|
+
if (values.length < 2) return [];
|
|
20
|
+
const lo = Math.min(...values);
|
|
21
|
+
const hi = Math.max(...values);
|
|
22
|
+
const span = (hi - lo) || 1;
|
|
23
|
+
// A flat series sits across the middle.
|
|
24
|
+
const flat = hi === lo;
|
|
25
|
+
return values.map((v, i) => ({
|
|
26
|
+
x: box.x + (i / (values.length - 1)) * box.width,
|
|
27
|
+
y: flat ? box.y + box.height / 2 : box.y + box.height - ((v - lo) / span) * box.height,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The line as a path `d` (empty for fewer than two values). */
|
|
32
|
+
get d() { return this.points.map((p, i) => `${i ? 'L' : 'M'}${p.x},${p.y}`).join(' '); }
|
|
33
|
+
|
|
34
|
+
/** Draw the line and a dot on its last value. */
|
|
35
|
+
draw(renderer, { color = '#5352ed', width = 1.5 } = {}) {
|
|
36
|
+
const points = this.points;
|
|
37
|
+
if (!points.length) return;
|
|
38
|
+
renderer.path(this.d, 'none', { color, width });
|
|
39
|
+
const last = points.at(-1);
|
|
40
|
+
renderer.circle(last.x, last.y, 2.5, color);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LinearTrack } from './LinearTrack.js';
|
|
2
|
+
|
|
3
|
+
/** A track bottom to top. */
|
|
4
|
+
export class VerticalTrack extends LinearTrack {
|
|
5
|
+
point(fraction, offset = 0) {
|
|
6
|
+
return { x: this.x + this.thickness / 2 + offset, y: this.y + this.length - fraction * this.length };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
rectangle(from, to, thickness) {
|
|
10
|
+
return { x: this.x + (this.thickness - thickness) / 2, y: this.y + this.length - to * this.length, width: thickness, height: (to - from) * this.length };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get center() { return this.point(0.5); }
|
|
14
|
+
|
|
15
|
+
/** Labels beside a vertical track start at their point. */
|
|
16
|
+
anchorAt() { return 'start'; }
|
|
17
|
+
}
|
|
@@ -170,20 +170,14 @@ export class Template {
|
|
|
170
170
|
};
|
|
171
171
|
const style = this.textStyle;
|
|
172
172
|
const font = TextStyle.svgAttributes(style, { defaultPixels: 12 });
|
|
173
|
-
const zLine = `Z:${this.zIndex || 0}`;
|
|
174
173
|
if (TextStyle.placesText(style)) {
|
|
175
174
|
// Placed in the shape's box by its alignment and position (centred, middle where unset).
|
|
176
175
|
const at = TextStyle.block(style, this, 1, { pixels: font['font-size'], defaults: { textAlign: 'center', textPosition: 'middle' } });
|
|
177
176
|
const placed = { 'text-anchor': at.textAnchor, 'dominant-baseline': 'auto' };
|
|
178
177
|
if (this.name) text({ x: at.x, y: at.baseline, ...font, ...placed, fill: this.textColor }, this.name);
|
|
179
|
-
// Under the name, or over it when the name is at the bottom (so it stays in the shape).
|
|
180
|
-
const zY = style.textPosition === 'bottom' ? at.baseline - font['font-size'] - 2 : at.baseline + 12;
|
|
181
|
-
text({ x: at.x, y: zY, 'font-size': 10, fill: '#666', ...placed }, zLine);
|
|
182
178
|
return;
|
|
183
179
|
}
|
|
184
180
|
if (this.name) text({ x, y, ...font, fill: this.textColor }, this.name);
|
|
185
|
-
// Below the name, however large it is set.
|
|
186
|
-
text({ x, y: y + Math.max(16, font['font-size'] / 2 + 10), 'font-size': 10, fill: '#666' }, zLine);
|
|
187
181
|
}
|
|
188
182
|
|
|
189
183
|
serialize() {
|
package/src/index.js
CHANGED
|
@@ -39,6 +39,13 @@ import { WaterfallChart } from './charting/charts/WaterfallChart.js';
|
|
|
39
39
|
import { BarGauge } from './charting/charts/BarGauge.js';
|
|
40
40
|
import { BulletGauge } from './charting/charts/BulletGauge.js';
|
|
41
41
|
import { DialGauge } from './charting/charts/DialGauge.js';
|
|
42
|
+
import { ArcGauge } from './charting/charts/ArcGauge.js';
|
|
43
|
+
import { RingGauge } from './charting/charts/RingGauge.js';
|
|
44
|
+
import { MultiRingGauge } from './charting/charts/MultiRingGauge.js';
|
|
45
|
+
import { KpiTile } from './charting/charts/KpiTile.js';
|
|
46
|
+
import { ThermometerGauge } from './charting/charts/ThermometerGauge.js';
|
|
47
|
+
import { GaugeAnimation } from './charting/gauges/GaugeAnimation.js';
|
|
48
|
+
import { GaugeValue } from './charting/gauges/GaugeValue.js';
|
|
42
49
|
import { BarChart3D } from './charting/charts/BarChart3D.js';
|
|
43
50
|
import { BubbleChart3D } from './charting/charts/BubbleChart3D.js';
|
|
44
51
|
import { BarChart3DOrbit } from './charting/charts/BarChart3DOrbit.js';
|
|
@@ -1657,14 +1664,11 @@ export class RenderEngine {
|
|
|
1657
1664
|
|
|
1658
1665
|
/**
|
|
1659
1666
|
* Renders a bar gauge.
|
|
1660
|
-
* @param {number} value The value to display (0-100).
|
|
1667
|
+
* @param {number|{value:number, target?:number}} value The value to display (0-100 unless options.min/max are given).
|
|
1661
1668
|
* @param {object} [options] Charting options.
|
|
1662
1669
|
*/
|
|
1663
1670
|
barGauge(value, options = {}) {
|
|
1664
|
-
|
|
1665
|
-
this._chart3D = false; // leaving 3D mode — clears the SVG-zoom guard added for bubbleChart3D
|
|
1666
|
-
this._chartState = { type: 'barGauge', data: value, options };
|
|
1667
|
-
this._renderChart(new BarGauge(this.renderer, value, options));
|
|
1671
|
+
this._renderGauge(BarGauge, 'barGauge', value, options);
|
|
1668
1672
|
}
|
|
1669
1673
|
|
|
1670
1674
|
/**
|
|
@@ -1674,22 +1678,90 @@ export class RenderEngine {
|
|
|
1674
1678
|
* @param {object} [options] Charting options.
|
|
1675
1679
|
*/
|
|
1676
1680
|
bulletGauge(data, options = {}) {
|
|
1677
|
-
|
|
1678
|
-
this._chart3D = false;
|
|
1679
|
-
this._chartState = { type: 'bullet', data, options };
|
|
1680
|
-
this._renderChart(new BulletGauge(this.renderer, data, options));
|
|
1681
|
+
this._renderGauge(BulletGauge, 'bullet', data, options);
|
|
1681
1682
|
}
|
|
1682
1683
|
|
|
1683
1684
|
/**
|
|
1684
1685
|
* Renders a dial gauge.
|
|
1685
|
-
* @param {number} value The value to display (0-100).
|
|
1686
|
+
* @param {number|{value:number, target?:number}} value The value to display (0-100 unless options.min/max are given).
|
|
1686
1687
|
* @param {object} [options] Charting options.
|
|
1687
1688
|
*/
|
|
1688
1689
|
dialGauge(value, options = {}) {
|
|
1690
|
+
this._renderGauge(DialGauge, 'dialGauge', value, options);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Renders an arc gauge: a half circle (or any start angle and sweep) filled to the value.
|
|
1695
|
+
* @param {number|{value:number, target?:number}} value The value (0-100 unless options.min/max are given).
|
|
1696
|
+
* @param {object} [options] The gauge options (see REFERENCE.md, Gauges), cx, cy, r, thickness, startAngle, sweep, cap.
|
|
1697
|
+
*/
|
|
1698
|
+
arcGauge(value, options = {}) {
|
|
1699
|
+
this._renderGauge(ArcGauge, 'arcGauge', value, options);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/**
|
|
1703
|
+
* Renders a progress ring, its value in the middle.
|
|
1704
|
+
* @param {number|{value:number, target?:number}} value The value (0-100 unless options.min/max are given).
|
|
1705
|
+
* @param {object} [options] The gauge options, cx, cy, r, thickness.
|
|
1706
|
+
*/
|
|
1707
|
+
ringGauge(value, options = {}) {
|
|
1708
|
+
this._renderGauge(RingGauge, 'ringGauge', value, options);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Renders several values as rings one inside the other, with a legend.
|
|
1713
|
+
* @param {Array<number|{value:number, label?:string, color?:string, min?:number, max?:number}>} values Outer ring first.
|
|
1714
|
+
* @param {object} [options] cx, cy, r, thickness, gap, colors, legend, min, max, format, units, title.
|
|
1715
|
+
*/
|
|
1716
|
+
multiRingGauge(values, options = {}) {
|
|
1717
|
+
this._renderGauge(MultiRingGauge, 'multiRingGauge', values, options);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/**
|
|
1721
|
+
* Renders a KPI tile: a figure, its change since the previous value, and a sparkline of its history.
|
|
1722
|
+
* @param {number|{value?:number, previous?:number, history?:number[]}} data
|
|
1723
|
+
* @param {object} [options] x, y, width, height, format, units, change ('percent'|'value'), upIsGood, bands, sparkColor, background, title.
|
|
1724
|
+
*/
|
|
1725
|
+
kpiTile(data, options = {}) {
|
|
1726
|
+
this._renderGauge(KpiTile, 'kpiTile', data, options);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Renders a thermometer: a tube filled from a bulb up to the value.
|
|
1731
|
+
* @param {number|{value:number, target?:number}} value The value (0-100 unless options.min/max are given).
|
|
1732
|
+
* @param {object} [options] The gauge options, x, y, width, height, bulb.
|
|
1733
|
+
*/
|
|
1734
|
+
thermometerGauge(value, options = {}) {
|
|
1735
|
+
this._renderGauge(ThermometerGauge, 'thermometerGauge', value, options);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Every gauge method draws through here. With `options.animate` (true:
|
|
1740
|
+
* 400ms, or milliseconds) a gauge of the same type already showing eases
|
|
1741
|
+
* from the value it shows to the new one; a newer call stops an older
|
|
1742
|
+
* animation and starts from where it got to.
|
|
1743
|
+
* @private
|
|
1744
|
+
*/
|
|
1745
|
+
_renderGauge(GaugeType, type, data, options = {}) {
|
|
1689
1746
|
if (!this.renderer) return;
|
|
1690
1747
|
this._chart3D = false; // leaving 3D mode — clears the SVG-zoom guard added for bubbleChart3D
|
|
1691
|
-
this._chartState = { type
|
|
1692
|
-
this.
|
|
1748
|
+
this._chartState = { type, data, options };
|
|
1749
|
+
this._gaugeAnimation?.stop();
|
|
1750
|
+
const shown = this._gaugeShown?.type === type ? this._gaugeShown.data : null;
|
|
1751
|
+
const draw = (frame) => {
|
|
1752
|
+
this._gaugeShown = { type, data: frame };
|
|
1753
|
+
this._renderChart(new GaugeType(this.renderer, frame, options));
|
|
1754
|
+
};
|
|
1755
|
+
if (!options.animate || shown == null) {
|
|
1756
|
+
draw(data);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
this._gaugeAnimation = new GaugeAnimation({
|
|
1760
|
+
from: GaugeValue.from(shown, options),
|
|
1761
|
+
to: GaugeValue.from(data, options),
|
|
1762
|
+
duration: options.animate === true ? 400 : Number(options.animate),
|
|
1763
|
+
draw,
|
|
1764
|
+
}).start();
|
|
1693
1765
|
}
|
|
1694
1766
|
|
|
1695
1767
|
_arcPath(cx, cy, r, startAngle, endAngle) {
|