@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
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<script src="../dist/renderengine.umd.js"></script>
|
|
18
18
|
<script src="../dist/renderengine-workspace.umd.js"></script>
|
|
19
19
|
<script>
|
|
20
|
-
const { RectangleTemplate, StadiumTemplate, DiamondTemplate } = window.RenderEngine;
|
|
20
|
+
const { RectangleTemplate, StadiumTemplate, DiamondTemplate, ParallelogramTemplate, Edge } = window.RenderEngine;
|
|
21
21
|
const { el, registerBuiltinParts, DiagramEditorPart } = window.RenderEngineWorkspace;
|
|
22
22
|
|
|
23
23
|
const ws = document.body.appendChild(el({
|
|
@@ -35,16 +35,21 @@
|
|
|
35
35
|
ws.registerPerspective('Diagrams', { type: 'part', partId: 'models' });
|
|
36
36
|
ws.activatePerspective('Diagrams');
|
|
37
37
|
|
|
38
|
-
// A small flowchart to start from.
|
|
38
|
+
// A small flowchart to start from: its shapes, and the lines between them.
|
|
39
39
|
const editorPart = ws.getPart('editor');
|
|
40
|
-
const { templates } = editorPart.model.currentView;
|
|
40
|
+
const { templates, edges } = editorPart.model.currentView;
|
|
41
41
|
for (const t of [
|
|
42
42
|
new StadiumTemplate('start', 'Order received', { x: 160, y: 80, fillColor: '#dcfce7' }),
|
|
43
43
|
new RectangleTemplate('check', 'Check stock', { x: 160, y: 200, fillColor: '#dbeafe' }),
|
|
44
44
|
new DiamondTemplate('decide', 'In stock?', { x: 160, y: 330, fillColor: '#fef9c3' }),
|
|
45
45
|
new RectangleTemplate('ship', 'Ship order', { x: 400, y: 330, fillColor: '#dbeafe' }),
|
|
46
|
-
new
|
|
46
|
+
new ParallelogramTemplate('reorder', 'Reorder stock', { x: 160, y: 470, fillColor: '#ede9fe' }),
|
|
47
|
+
new StadiumTemplate('end', 'Done', { x: 400, y: 470, fillColor: '#dcfce7' }),
|
|
47
48
|
]) templates.set(t.id, t);
|
|
49
|
+
for (const [id, from, to, label = ''] of [
|
|
50
|
+
['e1', 'start', 'check'], ['e2', 'check', 'decide'], ['e3', 'decide', 'ship', 'yes'],
|
|
51
|
+
['e4', 'decide', 'reorder', 'no'], ['e5', 'ship', 'end'], ['e6', 'reorder', 'ship'],
|
|
52
|
+
]) edges.set(id, new Edge(id, from, to, { endMarker: 'arrow', label }));
|
|
48
53
|
editorPart.editor.render();
|
|
49
54
|
</script>
|
|
50
55
|
</body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peter.prib/renderengine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "ArchiMate modeller, diagram editor and chart library for the browser, with no runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"diagram": "node scripts/open.mjs examples/diagram-editor.html",
|
|
29
29
|
"test": "node --test --test-timeout=10000 ./test/*.js ./test/charting/*.js ./test/charting/charts/*.js ./test/charting/diagrams/*.js ./test/charting/shapes/*.js",
|
|
30
30
|
"build": "rollup -c",
|
|
31
|
-
"prepublishOnly": "npm test && npm run build"
|
|
31
|
+
"prepublishOnly": "npm test && npm run build",
|
|
32
|
+
"screenshots": "node scripts/screenshots/screenshots.mjs"
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"archimate",
|
package/src/chartCatalog.js
CHANGED
|
@@ -461,18 +461,18 @@ export const CHART_CATALOG = [
|
|
|
461
461
|
key: 'gauge-dial', label: 'Gauge (Dial)', method: 'dialGauge', category: 'gauge',
|
|
462
462
|
coordSystem: COORD_SYSTEM.RADIAL, title: true, legend: false, colorMode: COLOR_MODE.SINGLE, camera: CAMERA_MODE.NONE,
|
|
463
463
|
axes: [
|
|
464
|
-
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] },
|
|
464
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
465
465
|
],
|
|
466
|
-
dataShape: 'number (
|
|
466
|
+
dataShape: 'number, {value, target?}, or [{value, label?, color?}] (a needle each) — no category axis; the only role IS the value (0-100 unless min/max are given)',
|
|
467
467
|
streamShape: 'none', // no array/window at all — update by calling dialGauge(newValue) again directly
|
|
468
468
|
},
|
|
469
469
|
{
|
|
470
470
|
key: 'gauge-bar', label: 'Gauge (Bar)', method: 'barGauge', category: 'gauge',
|
|
471
471
|
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.SINGLE, camera: CAMERA_MODE.NONE,
|
|
472
472
|
axes: [
|
|
473
|
-
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] },
|
|
473
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
474
474
|
],
|
|
475
|
-
dataShape: 'number (0-100)',
|
|
475
|
+
dataShape: 'number, or {value, target?} (0-100 unless min/max are given)',
|
|
476
476
|
streamShape: 'none', // no array/window at all — update by calling barGauge(newValue) again directly
|
|
477
477
|
},
|
|
478
478
|
{
|
|
@@ -485,6 +485,51 @@ export const CHART_CATALOG = [
|
|
|
485
485
|
dataShape: 'number or {value, target?, ranges?:Array<{max, label?, color?}>}',
|
|
486
486
|
streamShape: 'none',
|
|
487
487
|
},
|
|
488
|
+
{
|
|
489
|
+
key: 'gauge-arc', label: 'Gauge (Arc)', method: 'arcGauge', category: 'gauge',
|
|
490
|
+
coordSystem: COORD_SYSTEM.RADIAL, title: true, legend: false, colorMode: COLOR_MODE.SINGLE, camera: CAMERA_MODE.NONE,
|
|
491
|
+
axes: [
|
|
492
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
493
|
+
],
|
|
494
|
+
dataShape: 'number, or {value, target?} (0-100 unless min/max are given) — a half circle by default, any startAngle/sweep',
|
|
495
|
+
streamShape: 'none',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
key: 'gauge-ring', label: 'Gauge (Ring)', method: 'ringGauge', category: 'gauge',
|
|
499
|
+
coordSystem: COORD_SYSTEM.RADIAL, title: true, legend: false, colorMode: COLOR_MODE.SINGLE, camera: CAMERA_MODE.NONE,
|
|
500
|
+
axes: [
|
|
501
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
502
|
+
],
|
|
503
|
+
dataShape: 'number, or {value, target?} (0-100 unless min/max are given)',
|
|
504
|
+
streamShape: 'none',
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
key: 'gauge-rings', label: 'Gauge (Rings)', method: 'multiRingGauge', category: 'gauge',
|
|
508
|
+
coordSystem: COORD_SYSTEM.RADIAL, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
|
|
509
|
+
axes: [
|
|
510
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
511
|
+
],
|
|
512
|
+
dataShape: 'Array<number|{value, label?, color?, min?, max?}> — one ring per value, outer first, each on its own scale',
|
|
513
|
+
streamShape: 'none',
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
key: 'gauge-thermometer', label: 'Gauge (Thermometer)', method: 'thermometerGauge', category: 'gauge',
|
|
517
|
+
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.SINGLE, camera: CAMERA_MODE.NONE,
|
|
518
|
+
axes: [
|
|
519
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
520
|
+
],
|
|
521
|
+
dataShape: 'number, or {value, target?} (0-100 unless min/max are given)',
|
|
522
|
+
streamShape: 'none',
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
key: 'kpi-tile', label: 'KPI Tile', method: 'kpiTile', category: 'gauge',
|
|
526
|
+
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.SINGLE, camera: CAMERA_MODE.NONE,
|
|
527
|
+
axes: [
|
|
528
|
+
{ role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.FILL_PERCENT, required: true, range: [0, 100] }, // the default; the min/max options change it
|
|
529
|
+
],
|
|
530
|
+
dataShape: 'number, or {value?, previous?, history?:number[]} — the figure, its change since the previous value, a sparkline of the history',
|
|
531
|
+
streamShape: 'none',
|
|
532
|
+
},
|
|
488
533
|
{
|
|
489
534
|
key: 'bar3D', label: 'Bar (3D)', method: 'barChart3D', category: '3d',
|
|
490
535
|
coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: false, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.ISOMETRIC,
|
package/src/charting/Gauge.js
CHANGED
|
@@ -38,6 +38,12 @@ export class Gauge {
|
|
|
38
38
|
* Angles are in radians.
|
|
39
39
|
*/
|
|
40
40
|
static arcPath(cx, cy, r, startAngle, endAngle) {
|
|
41
|
+
// A whole circle starts and ends on one point, which an SVG arc draws as nothing: two halves.
|
|
42
|
+
if (endAngle - startAngle >= 2 * Math.PI - 1e-9) {
|
|
43
|
+
const mid = startAngle + Math.PI;
|
|
44
|
+
const at = (a) => `${cx + r * Math.cos(a)} ${cy + r * Math.sin(a)}`;
|
|
45
|
+
return `M ${at(startAngle)} A ${r} ${r} 0 0 1 ${at(mid)} A ${r} ${r} 0 0 1 ${at(startAngle)}`;
|
|
46
|
+
}
|
|
41
47
|
const largeArcFlag = endAngle - startAngle <= Math.PI ? '0' : '1';
|
|
42
48
|
const start = {
|
|
43
49
|
x: cx + r * Math.cos(startAngle),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { GaugeChart } from './GaugeChart.js';
|
|
2
|
+
import { ArcTrack } from '../gauges/ArcTrack.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ArcGauge — an arc filled to the value, its number in the middle: a half
|
|
6
|
+
* circle by default, or any start angle and sweep.
|
|
7
|
+
*
|
|
8
|
+
* Options: cx, cy, r (100, 100, 70), thickness (16), startAngle and sweep in
|
|
9
|
+
* degrees (180 and 180: a half circle over the top), cap ('round' ends), and
|
|
10
|
+
* every GaugeChart option (min, max, bands, target, ticks, ...). Without
|
|
11
|
+
* ticks the scale's two ends are labelled.
|
|
12
|
+
*/
|
|
13
|
+
export class ArcGauge extends GaugeChart {
|
|
14
|
+
static chartType = 'arcGauge';
|
|
15
|
+
|
|
16
|
+
static LAYOUT = { cx: 100, cy: 100, r: 70, thickness: 16, startAngle: 180, sweep: 180 };
|
|
17
|
+
|
|
18
|
+
track() { return new ArcTrack(this.layout); }
|
|
19
|
+
|
|
20
|
+
_render() {
|
|
21
|
+
const { title, cap } = this.options;
|
|
22
|
+
const track = this.track();
|
|
23
|
+
const capOption = cap ? { cap } : {};
|
|
24
|
+
this.drawBackground(track, capOption);
|
|
25
|
+
this.drawValue(track, capOption);
|
|
26
|
+
this.drawTarget(track);
|
|
27
|
+
if (this.options.ticks) this.drawTicks(track);
|
|
28
|
+
else this.drawEnds(track);
|
|
29
|
+
this.drawLabels(track);
|
|
30
|
+
this._renderTitle(title, track.cx, this.titleY(track), { textAnchor: 'middle', fontSize: '12px', fill: '#747d8c' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The value's number in the middle, a little above the centre for an open arc. */
|
|
34
|
+
drawLabels(track) {
|
|
35
|
+
const { cx, cy, r } = track;
|
|
36
|
+
this._renderValueLabel(cx, cy - r * 0.15, { fontSize: `${Math.max(12, Math.round(r * 0.32))}px`, fill: '#2f3542', fontWeight: 'bold' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Below the arc's lowest point. */
|
|
40
|
+
titleY(track) {
|
|
41
|
+
const lowest = Math.max(track.point(0).y, track.point(1).y, track.cy);
|
|
42
|
+
return lowest + track.thickness / 2 + 18;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The scale's min and max under the arc's two ends. */
|
|
46
|
+
drawEnds(track) {
|
|
47
|
+
const below = track.thickness / 2 + 2;
|
|
48
|
+
for (const f of [0, 1]) {
|
|
49
|
+
const at = track.point(f);
|
|
50
|
+
this.renderer.text(this.scale.tickLabel(this.scale.valueAt(f), 1), at.x, at.y + below + 8, {
|
|
51
|
+
textAnchor: 'middle', verticalAlign: 'middle', fontSize: '10px', fill: '#747d8c',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import { GaugeChart } from './GaugeChart.js';
|
|
2
|
+
import { HorizontalTrack } from '../gauges/HorizontalTrack.js';
|
|
3
|
+
import { VerticalTrack } from '../gauges/VerticalTrack.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
* BarGauge — a
|
|
6
|
+
* BarGauge — a straight bar filled to the value, left to right or (with
|
|
7
|
+
* `orientation: 'vertical'`) bottom to top.
|
|
8
|
+
*
|
|
9
|
+
* Options: x, y, width, height (the bar's box; 200 × 30 across, 30 × 200 up),
|
|
10
|
+
* orientation, and every GaugeChart option (min, max, bands, target, ticks, ...).
|
|
5
11
|
*/
|
|
6
12
|
export class BarGauge extends GaugeChart {
|
|
7
13
|
static chartType = 'barGauge';
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
+
get vertical() { return this.options.orientation === 'vertical'; }
|
|
16
|
+
|
|
17
|
+
track() {
|
|
18
|
+
const { x = 10, y = 10 } = this.options;
|
|
19
|
+
const { width = this.vertical ? 30 : 200, height = this.vertical ? 200 : 30 } = this.options;
|
|
20
|
+
return this.vertical
|
|
21
|
+
? new VerticalTrack({ x, y, length: height, thickness: width, radius: 4 })
|
|
22
|
+
: new HorizontalTrack({ x, y, length: width, thickness: height, radius: 4 });
|
|
23
|
+
}
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
25
|
+
_render() {
|
|
26
|
+
const { x = 10, y = 10, title } = this.options;
|
|
27
|
+
const track = this.track();
|
|
28
|
+
this._renderTitle(title, x, y - (this.vertical ? 8 : 5));
|
|
29
|
+
this.drawBackground(track);
|
|
30
|
+
this.drawValue(track);
|
|
31
|
+
this.drawTarget(track);
|
|
32
|
+
this.drawTicks(track);
|
|
33
|
+
if (this.vertical) {
|
|
34
|
+
// A vertical bar is too narrow for its label: it goes underneath.
|
|
35
|
+
const below = track.point(0, 0);
|
|
36
|
+
this._renderValueLabel(below.x, below.y + 14, { fontWeight: 'bold', fill: '#2f3542' });
|
|
37
|
+
} else {
|
|
38
|
+
const { x: cx, y: cy } = track.center;
|
|
39
|
+
this._renderValueLabel(cx, cy, { fontWeight: 'bold' });
|
|
40
|
+
}
|
|
20
41
|
}
|
|
21
42
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GaugeChart } from './GaugeChart.js';
|
|
2
|
+
import { HorizontalTrack } from '../gauges/HorizontalTrack.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -8,82 +9,77 @@ import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
|
8
9
|
* Data may be a plain number (the actual value) or an object
|
|
9
10
|
* `{ value: number, target?: number, ranges?: Array<{max:number, label?, color?}> }`.
|
|
10
11
|
*
|
|
11
|
-
* Options:
|
|
12
|
-
*
|
|
13
|
-
* ranges?: Range[] — qualitative ranges (overrides data.ranges)
|
|
14
|
-
* min?: number = 0 — scale minimum
|
|
15
|
-
* max?: number = 100 — scale maximum
|
|
12
|
+
* Options (with every GaugeChart option: min, max, bands, target, ticks, ...):
|
|
13
|
+
* ranges?: Range[] — the bands, by their older name (overrides data.ranges)
|
|
16
14
|
* barColor?: string — color of the actual-value bar
|
|
17
15
|
* targetColor?: string = #333 — color of the target marker
|
|
18
16
|
* rangeColors?: string[] — fallback palette for ranges without their own color
|
|
19
|
-
* title?: string
|
|
20
17
|
* showLabels?: boolean = true — value/target/range labels
|
|
21
18
|
*/
|
|
22
|
-
export class BulletGauge extends
|
|
19
|
+
export class BulletGauge extends GaugeChart {
|
|
23
20
|
static chartType = 'bullet';
|
|
21
|
+
static defaultColor = '#5470c6';
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const options = this.options;
|
|
28
|
-
const box = ChartBox.fromRenderer(this.renderer);
|
|
29
|
-
const {
|
|
30
|
-
x = box.x, y = box.y, width = box.width, height = box.height * 0.4,
|
|
31
|
-
min = 0, max = 100,
|
|
32
|
-
target: optTarget, targetColor = '#333',
|
|
33
|
-
barColor = '#5470c6',
|
|
34
|
-
ranges: optRanges, rangeColors = ['#ff6b6b', '#ffd93d', '#6bcb77'],
|
|
35
|
-
title, showLabels = true,
|
|
36
|
-
} = options;
|
|
23
|
+
/** Its numbers as they are ("65", not "65.0%"). */
|
|
24
|
+
defaultFormat() { return (v) => String(v); }
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
26
|
+
/** The data's ranges, else poor / satisfactory / good at 60% and 80% of the scale. */
|
|
27
|
+
defaultBands() {
|
|
28
|
+
const max = this.options.max ?? 100;
|
|
29
|
+
return this.data?.bands ?? this.data?.ranges ?? [
|
|
41
30
|
{ max: max * 0.6, label: 'Poor' },
|
|
42
31
|
{ max: max * 0.8, label: 'Satisfactory' },
|
|
43
|
-
{ max
|
|
32
|
+
{ max, label: 'Good' },
|
|
44
33
|
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The bar keeps one colour (`barColor`): the bands behind it are the comparison. */
|
|
37
|
+
valueColor(value) {
|
|
38
|
+
const { barColor, color } = this.options;
|
|
39
|
+
return barColor ?? (color != null ? super.valueColor(value) : this.constructor.defaultColor);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get box() {
|
|
43
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
44
|
+
const { x = box.x, y = box.y, width = box.width, height = box.height * 0.4 } = this.options;
|
|
45
|
+
return { x, y, width, height };
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
+
track() {
|
|
49
|
+
const { x, y, width, height } = this.box;
|
|
50
|
+
return new HorizontalTrack({ x, y, length: width, thickness: height });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_render() {
|
|
54
|
+
const { title, showLabels = true, ticks } = this.options;
|
|
55
|
+
const { x, y, width, height } = this.box;
|
|
56
|
+
const track = this.track();
|
|
48
57
|
|
|
49
58
|
new ChartLabels({ title }).draw(this.renderer, { x, y, width, height });
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.renderer.rect(rx, y, rw, height, color, null, { opacity: 0.35 });
|
|
59
|
-
if (showLabels && range.label) {
|
|
60
|
-
this.renderer.text(range.label, rx + rw / 2, y + height + 14, {
|
|
61
|
-
textAnchor: 'middle', fontSize: '10px', fill: '#666',
|
|
62
|
-
});
|
|
60
|
+
this.drawBackground(track);
|
|
61
|
+
if (showLabels) {
|
|
62
|
+
const below = y + height + (ticks ? 28 : 14);
|
|
63
|
+
for (const band of this.bands.list) {
|
|
64
|
+
if (!band.label) continue;
|
|
65
|
+
const mid = track.point((this.scale.fraction(band.from) + this.scale.fraction(band.to)) / 2);
|
|
66
|
+
this.renderer.text(band.label, mid.x, below, { textAnchor: 'middle', fontSize: '10px', fill: '#666' });
|
|
63
67
|
}
|
|
64
|
-
|
|
65
|
-
});
|
|
68
|
+
}
|
|
66
69
|
|
|
67
|
-
//
|
|
70
|
+
// The actual value: a bar half as thick, centred on the bands.
|
|
68
71
|
const barHeight = height * 0.5;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.renderer.rect(actualX, barY, actualW, barHeight, barColor, null, {});
|
|
73
|
-
|
|
74
|
-
// Draw target marker (vertical line)
|
|
75
|
-
if (target != null) {
|
|
76
|
-
const tx = toX(target);
|
|
77
|
-
this.renderer.path(`M${tx},${y - 2} L${tx},${y + height + 2}`, 'none', { color: targetColor, width: 2 });
|
|
78
|
-
}
|
|
72
|
+
this.drawValue(track, { thickness: barHeight });
|
|
73
|
+
this.drawTarget(track, { overhang: 2 });
|
|
74
|
+
this.drawTicks(track);
|
|
79
75
|
|
|
80
76
|
if (showLabels) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
77
|
+
const { x: cx, y: cy } = track.center;
|
|
78
|
+
this._renderValueLabel(cx, cy, { fontSize: '11px', fontWeight: 'bold' });
|
|
79
|
+
const { target } = this.gaugeValue;
|
|
84
80
|
if (target != null) {
|
|
85
|
-
this.renderer.text(
|
|
86
|
-
textAnchor: 'start', fontSize: '10px', fill: targetColor,
|
|
81
|
+
this.renderer.text(this.scale.label(target), track.point(this.scale.fraction(target)).x + 5, y - 8, {
|
|
82
|
+
textAnchor: 'start', fontSize: '10px', fill: this.options.targetColor ?? '#333',
|
|
87
83
|
});
|
|
88
84
|
}
|
|
89
85
|
}
|
|
@@ -1,37 +1,65 @@
|
|
|
1
|
-
import { Gauge } from '../Gauge.js';
|
|
2
1
|
import { GaugeChart } from './GaugeChart.js';
|
|
2
|
+
import { ArcTrack } from '../gauges/ArcTrack.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* DialGauge —
|
|
5
|
+
* DialGauge — an arc filled to the value, with a needle pointing at it.
|
|
6
|
+
*
|
|
7
|
+
* Data: a value, or several (`[{ value, label?, color? }]`, e.g. actual and
|
|
8
|
+
* budget): a needle each, the first also filling the arc and labelled in
|
|
9
|
+
* the middle, and a legend of them all under the title.
|
|
10
|
+
* Options: cx, cy, r (100, 80, 60), thickness (12), startAngle and sweep in
|
|
11
|
+
* degrees (135 and 270: bottom-left round to bottom-right), needleColors,
|
|
12
|
+
* and every GaugeChart option (min, max, bands, target, ticks, segments, ...).
|
|
6
13
|
*/
|
|
7
14
|
export class DialGauge extends GaugeChart {
|
|
8
15
|
static chartType = 'dialGauge';
|
|
16
|
+
static LAYOUT = { cx: 100, cy: 80, r: 60, thickness: 12, startAngle: 135, sweep: 270 };
|
|
17
|
+
static NEEDLE_COLORS = ['#2f3542', '#ff4757', '#1e90ff', '#ffa502'];
|
|
18
|
+
|
|
19
|
+
track() { return new ArcTrack(this.layout); }
|
|
20
|
+
|
|
21
|
+
/** One per value, the first the main one: {value, label, color}. */
|
|
22
|
+
get needles() {
|
|
23
|
+
const { needleColors = DialGauge.NEEDLE_COLORS } = this.options;
|
|
24
|
+
const values = this.gaugeValue.values.length ? this.gaugeValue.values : [{ value: this.gaugeValue.value }];
|
|
25
|
+
return values.map((v, i) => ({ ...v, value: this.scale.clamp(v.value), color: v.color ?? needleColors[i % needleColors.length] }));
|
|
26
|
+
}
|
|
9
27
|
|
|
10
28
|
_render() {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.renderer.
|
|
23
|
-
|
|
24
|
-
this.renderer.path(Gauge.arcPath(cx, cy, r, startAngle * rad, needleAngle * rad), 'none', { color: resolvedColor, width: 12, cap: 'round' });
|
|
25
|
-
|
|
26
|
-
// Needle
|
|
27
|
-
const needleRad = needleAngle * rad;
|
|
28
|
-
const nx = cx + (r - 8) * Math.cos(needleRad);
|
|
29
|
-
const ny = cy + (r - 8) * Math.sin(needleRad);
|
|
30
|
-
this.renderer.path(`M${cx},${cy} L${nx},${ny}`, 'none', { color: '#2f3542', width: 3, cap: 'round' });
|
|
31
|
-
this.renderer.circle(cx, cy, 5, '#2f3542');
|
|
32
|
-
|
|
33
|
-
// Text
|
|
29
|
+
const { title } = this.options;
|
|
30
|
+
const track = this.track();
|
|
31
|
+
const { cx, cy, r } = track;
|
|
32
|
+
|
|
33
|
+
this.drawBackground(track, { cap: 'round' });
|
|
34
|
+
this.drawValue(track, { cap: 'round' });
|
|
35
|
+
this.drawTarget(track);
|
|
36
|
+
this.drawTicks(track);
|
|
37
|
+
const needles = this.needles;
|
|
38
|
+
// The main needle last, so it is on top.
|
|
39
|
+
for (const needle of [...needles].reverse()) this.drawNeedle(track, needle);
|
|
40
|
+
this.renderer.circle(cx, cy, 5, needles[0].color);
|
|
41
|
+
|
|
34
42
|
this._renderTitle(title, cx, cy + r + 25, { textAnchor: 'middle', fill: '#747d8c', textTransform: 'uppercase' });
|
|
35
|
-
this.
|
|
43
|
+
this._renderValueLabel(cx, cy + r - 15, { fontSize: '18px', fill: '#2f3542', fontWeight: 'bold' });
|
|
44
|
+
if (needles.length > 1) this.drawLegend(needles, cx, cy + r + (title ? 42 : 25));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A needle from the centre to just inside the arc. */
|
|
48
|
+
drawNeedle(track, { value, color }) {
|
|
49
|
+
const tip = track.point(this.scale.fraction(value), -8);
|
|
50
|
+
this.renderer.path(`M${track.cx},${track.cy} L${tip.x},${tip.y}`, 'none', { color, width: 3, cap: 'round' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Each needle's label and value on one line, centred. */
|
|
54
|
+
drawLegend(needles, cx, y) {
|
|
55
|
+
const text = needles.map((n, i) => `${n.label ?? `#${i + 1}`} ${this.scale.label(n.value)}`);
|
|
56
|
+
const font = { fontSize: '11px' };
|
|
57
|
+
const width = (t) => (this.renderer.measureText?.(t, font) ?? t.length * 6) + 22;
|
|
58
|
+
let x = cx - text.reduce((sum, t) => sum + width(t), 0) / 2;
|
|
59
|
+
needles.forEach((needle, i) => {
|
|
60
|
+
this.renderer.circle(x + 4, y, 4, needle.color);
|
|
61
|
+
this.renderer.text(text[i], x + 12, y, { ...font, textAnchor: 'start', verticalAlign: 'middle', fill: '#2f3542' });
|
|
62
|
+
x += width(text[i]);
|
|
63
|
+
});
|
|
36
64
|
}
|
|
37
65
|
}
|