bezel-charts 1.1.0 → 1.2.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 +51 -9
- package/dist/bezel-charts.iife.js +9 -9
- package/dist/bezel-charts.js +272 -44
- package/dist/bezel-charts.min.js +9 -9
- package/package.json +4 -4
- package/src/charts/bars.js +16 -5
- package/src/charts/donut.js +9 -1
- package/src/charts/gauge.js +8 -0
- package/src/charts/heatmap.js +22 -5
- package/src/charts/line.js +27 -5
- package/src/charts/progress.js +9 -0
- package/src/charts/ring.js +7 -0
- package/src/charts/scatter.js +20 -4
- package/src/charts/spark.js +5 -0
- package/src/core/util.js +57 -6
- package/src/element.js +114 -16
- package/src/index.js +1 -1
- package/src/render.js +14 -1
package/src/charts/scatter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r2, clamp, num, has, list, series, bool, fmt, niceScale, text, small, hit, palette, emptyState, parseNotes, goalLine, legend, axisWidth,
|
|
1
|
+
import { r2, clamp, num, has, list, series, bool, fmt, niceScale, text, small, hit, palette, emptyState, parseNotes, goalLine, legend, axisWidth, styleOf, marker, SHAPES } from '../core/util.js';
|
|
2
2
|
import { tile } from '../core/tile.js';
|
|
3
3
|
|
|
4
4
|
/** Least-squares fit. Returns slope, intercept and Pearson's r, or null when x has no spread. */
|
|
@@ -32,12 +32,16 @@ export default function scatter(o, th) {
|
|
|
32
32
|
var nameOf = function (j) { return names[j] || 'Group ' + (j + 1); };
|
|
33
33
|
var xo = { decimals: o.decimals, compact: o.compact, lang: o.lang };
|
|
34
34
|
var notes = parseNotes(o, labels);
|
|
35
|
+
// Each group can have its own point shape; "none" in the list keeps that group's circles.
|
|
36
|
+
var markOf = styleOf(o.markers, SHAPES, 'circle'), bg = o.background || th.tile;
|
|
37
|
+
var shapeOf = function (j) { var m = markOf ? markOf(j) : 'circle'; return m === 'none' ? 'circle' : m; };
|
|
38
|
+
var key = markOf && function (j, x, y) { return marker(shapeOf(j), x + 4, y + 6, 4, ' fill="' + pal[j % pal.length] + '"'); };
|
|
35
39
|
|
|
36
40
|
return tile(o, th, h, function (x0, y0, w, h) {
|
|
37
41
|
if (!count) return emptyState(th, x0, y0, w, h);
|
|
38
42
|
var s = '', top = y0;
|
|
39
43
|
if (multi) {
|
|
40
|
-
var lg = legend(groups.map(function (_, j) { return nameOf(j); }), pal, th, x0, top, w);
|
|
44
|
+
var lg = legend(groups.map(function (_, j) { return nameOf(j); }), pal, th, x0, top, w, key);
|
|
41
45
|
s += lg.svg;
|
|
42
46
|
top += lg.height;
|
|
43
47
|
}
|
|
@@ -94,8 +98,8 @@ export default function scatter(o, th) {
|
|
|
94
98
|
var cx = PX(xv), cy = PY(gr.ys[i]);
|
|
95
99
|
var tip = (labels[idx] ? labels[idx] + '\n' : multi ? nameOf(j) + '\n' : '') +
|
|
96
100
|
(has(o.xLabel) ? o.xLabel : 'x') + ': ' + fmt(xv, xo) + '\n' + (has(o.yLabel) ? o.yLabel : 'y') + ': ' + fmt(gr.ys[i], o);
|
|
97
|
-
s +=
|
|
98
|
-
(o.animate ? ' class="bzc-a" style="animation-delay:' + r2(Math.min(0.8, idx * 0.012)) + 's"' : '')
|
|
101
|
+
s += marker(shapeOf(j), cx, cy, r, ' fill="' + c + '" fill-opacity=".85" stroke="' + bg + '" stroke-width="1.5"' +
|
|
102
|
+
(o.animate ? ' class="bzc-a" style="animation-delay:' + r2(Math.min(0.8, idx * 0.012)) + 's"' : ''));
|
|
99
103
|
s += hit(cx - 8, cy - 8, 16, 16, tip);
|
|
100
104
|
idx++;
|
|
101
105
|
});
|
|
@@ -113,3 +117,15 @@ export default function scatter(o, th) {
|
|
|
113
117
|
return s;
|
|
114
118
|
}, { summary: count + ' points' + (multi ? ' in ' + groups.length + ' groups' : '') });
|
|
115
119
|
}
|
|
120
|
+
|
|
121
|
+
scatter.table = function (o) {
|
|
122
|
+
var X = series(o.x), Y = series(o.y), names = list(o.series), labels = list(o.labels), multi = X.length > 1 || names.length > 0;
|
|
123
|
+
var xo = { decimals: o.decimals, compact: o.compact, lang: o.lang }, rows = [], idx = 0;
|
|
124
|
+
X.forEach(function (xs, g) {
|
|
125
|
+
var ys = Y[g] || [];
|
|
126
|
+
for (var i = 0; i < Math.min(xs.length, ys.length); i++, idx++) {
|
|
127
|
+
rows.push([labels[idx] || '#' + (idx + 1)].concat(multi ? [names[g] || 'Group ' + (g + 1)] : [], [fmt(xs[i], xo), fmt(ys[i], o)]));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return { head: ['Point'].concat(multi ? ['Group'] : [], [has(o.xLabel) ? o.xLabel : 'x', has(o.yLabel) ? o.yLabel : 'y']), rows: rows };
|
|
131
|
+
};
|
package/src/charts/spark.js
CHANGED
|
@@ -39,3 +39,8 @@ export default function spark(o, th) {
|
|
|
39
39
|
return s;
|
|
40
40
|
}, { stat: st, summary: v.length ? 'latest ' + fmt(v[v.length - 1], o) : 'no data' });
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
spark.table = function (o) {
|
|
44
|
+
var labels = list(o.labels);
|
|
45
|
+
return { head: ['Label', 'Value'], rows: nums(o.values).map(function (n, i) { return [labels[i] || '#' + (i + 1), fmt(n, o)]; }) };
|
|
46
|
+
};
|
package/src/core/util.js
CHANGED
|
@@ -123,20 +123,71 @@ export function statOf(v, o, fallback) {
|
|
|
123
123
|
return { val: map[k], delta: delta };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
/**
|
|
127
|
+
* A wrapping row of coloured dots and series names. Returns the SVG and the height it used.
|
|
128
|
+
* key(k, x, y) draws a different swatch, such as a dashed line, and kw is the room it takes before the name.
|
|
129
|
+
*/
|
|
130
|
+
export function legend(names, pal, th, x0, y0, w, key, kw) {
|
|
131
|
+
var x = x0, y = y0, s = '', off = kw || 13;
|
|
129
132
|
names.forEach(function (nm, k) {
|
|
130
|
-
var wd =
|
|
133
|
+
var wd = off + tw(nm, 12) + 16;
|
|
131
134
|
if (x > x0 && x + wd - 16 > x0 + w) { x = x0; y += 20; }
|
|
132
|
-
s += '<circle cx="' + r2(x + 4) + '" cy="' + r2(y + 6) + '" r="4" fill="' + pal[k % pal.length] + '"/>' +
|
|
133
|
-
text(x +
|
|
135
|
+
s += (key ? key(k, x, y) : '<circle cx="' + r2(x + 4) + '" cy="' + r2(y + 6) + '" r="4" fill="' + pal[k % pal.length] + '"/>') +
|
|
136
|
+
text(x + off, y + 10, nm, 'fill="' + th.fg + '" font-family="' + SANS + '" font-size="12"');
|
|
134
137
|
x += wd;
|
|
135
138
|
});
|
|
136
139
|
return { svg: s, height: y - y0 + 24 };
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
/** Line dashes, marker shapes and bar patterns, so series differ by more than colour. "auto" hands them out in this order. */
|
|
143
|
+
export var DASHES = { solid: '', dashed: '7 5', dotted: '1 5', dashdot: '9 4 1 4' };
|
|
144
|
+
export var SHAPES = ['circle', 'square', 'triangle', 'diamond'];
|
|
145
|
+
export var PATTERNS = ['solid', 'stripes', 'dots', 'grid'];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Reads dash, markers or patterns. The bare attribute or "auto" hands out every style in turn; a list sets one per series,
|
|
149
|
+
* repeating like colours do. Returns a function from series index to style name, or null when the option is off.
|
|
150
|
+
*/
|
|
151
|
+
export function styleOf(v, names, off) {
|
|
152
|
+
if (v == null || v === false || v === 'none' || v === 'false') return null;
|
|
153
|
+
var l = v === true || v === '' || v === 'auto' ? names : list(v).map(function (s) { return names.indexOf(s) < 0 ? off : s; });
|
|
154
|
+
if (!l.length) return null;
|
|
155
|
+
return function (k) { return l[k % l.length]; };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A point marker centred on x, y. attrs carries the fill and stroke. */
|
|
159
|
+
export function marker(kind, x, y, r, attrs) {
|
|
160
|
+
var a = r * 1.15, b = r * 0.85;
|
|
161
|
+
if (kind === 'square') return '<rect x="' + r2(x - b) + '" y="' + r2(y - b) + '" width="' + r2(2 * b) + '" height="' + r2(2 * b) + '" rx="1"' + attrs + '/>';
|
|
162
|
+
if (kind === 'triangle') return '<path d="M' + r2(x) + ',' + r2(y - a) + 'L' + r2(x + a) + ',' + r2(y + b) + 'L' + r2(x - a) + ',' + r2(y + b) + 'Z"' + attrs + '/>';
|
|
163
|
+
if (kind === 'diamond') return '<path d="M' + r2(x) + ',' + r2(y - a * 1.1) + 'L' + r2(x + a) + ',' + r2(y) + 'L' + r2(x) + ',' + r2(y + a * 1.1) + 'L' + r2(x - a) + ',' + r2(y) + 'Z"' + attrs + '/>';
|
|
164
|
+
return '<circle cx="' + r2(x) + '" cy="' + r2(y) + '" r="' + r2(r) + '"' + attrs + '/>';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A fill pattern in colour c, with stripes, dots or a grid drawn in the tile colour so they show on any colour. */
|
|
168
|
+
export function pattern(id, kind, c, bg) {
|
|
169
|
+
var ink = ' stroke="' + bg + '" stroke-opacity=".55" stroke-width="1.5"';
|
|
170
|
+
return '<pattern id="' + id + '" width="6" height="6" patternUnits="userSpaceOnUse"' + (kind === 'dots' ? '' : ' patternTransform="rotate(45)"') +
|
|
171
|
+
'><rect width="6" height="6" fill="' + c + '"/>' +
|
|
172
|
+
(kind === 'dots' ? '<circle cx="3" cy="3" r="1.3" fill="' + bg + '" fill-opacity=".55"/>' : '<path d="M3,0V6' + (kind === 'grid' ? 'M0,3H6' : '') + '"' + ink + '/>') +
|
|
173
|
+
'</pattern>';
|
|
174
|
+
}
|
|
175
|
+
|
|
139
176
|
/** Value axis: gridlines plus tick labels on the left. Returns the axis width it reserved. */
|
|
140
177
|
export function axisWidth(ticks) {
|
|
141
178
|
return Math.max.apply(null, ticks.map(function (t) { return tw(t, 10, true); })) + 10;
|
|
142
179
|
}
|
|
180
|
+
|
|
181
|
+
/** The data table for charts with labels and one or more series. total adds a column adding each row up. */
|
|
182
|
+
export function seriesTable(o, total) {
|
|
183
|
+
var S = series(o.values), names = list(o.series), labels = list(o.labels), rows = [];
|
|
184
|
+
var n = Math.max.apply(null, [0].concat(S.map(function (s) { return s.length; })));
|
|
185
|
+
for (var i = 0; i < n; i++) {
|
|
186
|
+
var row = [labels[i] || '#' + (i + 1)], t = 0;
|
|
187
|
+
S.forEach(function (s) { row.push(i < s.length ? fmt(s[i], o) : ''); t += i < s.length ? s[i] : 0; });
|
|
188
|
+
if (total) row.push(fmt(t, o));
|
|
189
|
+
rows.push(row);
|
|
190
|
+
}
|
|
191
|
+
var head = S.length > 1 || names.length ? S.map(function (_, k) { return names[k] || 'Series ' + (k + 1); }) : ['Value'];
|
|
192
|
+
return { head: ['Label'].concat(head, total ? ['Total'] : []), rows: rows };
|
|
193
|
+
}
|
package/src/element.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The custom element shared by every chart tag. */
|
|
2
|
-
import { render, toStatic } from './render.js';
|
|
2
|
+
import { render, table, toStatic } from './render.js';
|
|
3
3
|
import { MONO, SANS, esc, clamp } from './core/util.js';
|
|
4
4
|
import { THEMES } from './core/themes.js';
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ export const ATTRIBUTES = [
|
|
|
7
7
|
'type', 'values', 'labels', 'series', 'heading', 'prefix', 'suffix', 'decimals', 'compact', 'color', 'theme', 'frame',
|
|
8
8
|
'width', 'height', 'goal', 'goal-label', 'notes', 'show-values', 'stat', 'delta', 'delta-label', 'max', 'min', 'value',
|
|
9
9
|
'zones', 'horizontal', 'straight', 'zero', 'start', 'lang', 'corners', 'background', 'center-label', 'highlight',
|
|
10
|
-
'x', 'y', 'x-label', 'y-label', 'trend', 'static', 'no-tooltips', 'stacked', 'angle-labels',
|
|
10
|
+
'x', 'y', 'x-label', 'y-label', 'trend', 'static', 'no-tooltips', 'stacked', 'angle-labels', 'dash', 'markers', 'patterns',
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
const HOST_CSS =
|
|
@@ -15,7 +15,15 @@ const HOST_CSS =
|
|
|
15
15
|
'svg{display:block;max-width:100%;height:auto;overflow:visible}' +
|
|
16
16
|
`.tip{position:absolute;left:0;top:0;pointer-events:none;background:#0b0b10;color:#f2f1f7;font:600 12px/1.5 ${MONO};` +
|
|
17
17
|
'padding:7px 10px;border-radius:10px;white-space:pre;opacity:0;transform:translate(-50%,calc(-100% - 12px));transition:opacity .12s;z-index:3;' +
|
|
18
|
-
'border:1px solid #2a2935;box-shadow:0 8px 24px rgba(0,0,0,.35)}.tip.on{opacity:1}'
|
|
18
|
+
'border:1px solid #2a2935;box-shadow:0 8px 24px rgba(0,0,0,.35)}.tip.on{opacity:1}' +
|
|
19
|
+
// The focus ring is white with a black edge, so it shows on light and dark pages alike. Then the outline on the
|
|
20
|
+
// value the arrow keys are on, and text only screen readers get.
|
|
21
|
+
'.c{outline:none}.c:focus-visible{outline:2px solid #fff;outline-offset:3px;box-shadow:0 0 0 7px #000}.k{stroke:currentColor;stroke-width:2;stroke-opacity:1}' +
|
|
22
|
+
'.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}' +
|
|
23
|
+
'@media (forced-colors:active){.c:focus-visible{outline-color:Highlight}}';
|
|
24
|
+
|
|
25
|
+
// Arrow keys move one value along; on a heatmap, left and right move a week (one column) and up and down a day.
|
|
26
|
+
const STEPS = { ArrowLeft: -1, ArrowUp: -1, ArrowRight: 1, ArrowDown: 1 };
|
|
19
27
|
|
|
20
28
|
const darkQuery = () => (typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null);
|
|
21
29
|
|
|
@@ -55,10 +63,16 @@ export class BezelChart extends Base {
|
|
|
55
63
|
|
|
56
64
|
connectedCallback() {
|
|
57
65
|
if (!this.shadowRoot) {
|
|
58
|
-
|
|
66
|
+
// The chart, the tooltip, the screen reader announcements and the data table stay in place between redraws,
|
|
67
|
+
// so a focused chart keeps its focus when it resizes or changes theme.
|
|
68
|
+
this.attachShadow({ mode: 'open' }).innerHTML = `<style>${HOST_CSS}</style><div class="c" role="group" aria-roledescription="chart"></div>` +
|
|
69
|
+
'<p class="sr" id="h">Use the arrow keys to step through the values.</p><div class="tip" aria-hidden="true"></div><div class="sr" aria-live="polite"></div><div class="sr"></div>';
|
|
70
|
+
[, this.#box, , this.#tipEl, this.#live, this.#table] = this.shadowRoot.children;
|
|
59
71
|
this.shadowRoot.addEventListener('pointermove', (e) => this.#tip(e));
|
|
60
72
|
this.shadowRoot.addEventListener('pointerdown', (e) => this.#tip(e));
|
|
61
|
-
this.addEventListener('pointerleave', () => this.
|
|
73
|
+
this.addEventListener('pointerleave', () => this.#tipEl.classList.remove('on'));
|
|
74
|
+
this.#box.addEventListener('keydown', (e) => this.#key(e));
|
|
75
|
+
this.#box.addEventListener('blur', () => this.#clear());
|
|
62
76
|
}
|
|
63
77
|
live.add(this);
|
|
64
78
|
this.#dark = darkQuery();
|
|
@@ -122,6 +136,11 @@ export class BezelChart extends Base {
|
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
#ro = null;
|
|
139
|
+
#box = null;
|
|
140
|
+
#tipEl = null;
|
|
141
|
+
#live = null;
|
|
142
|
+
#table = null;
|
|
143
|
+
#at = -1;
|
|
125
144
|
#dark = null;
|
|
126
145
|
#width = 0;
|
|
127
146
|
#rendered = false;
|
|
@@ -137,33 +156,112 @@ export class BezelChart extends Base {
|
|
|
137
156
|
this.#width = w > 0 ? w : 360;
|
|
138
157
|
const o = this.options;
|
|
139
158
|
o.animate = !this.#rendered && !this.hasAttribute('static');
|
|
140
|
-
let svg;
|
|
159
|
+
let svg, data = null;
|
|
141
160
|
try {
|
|
142
161
|
svg = render(this.chartType, o);
|
|
162
|
+
data = table(this.chartType, o);
|
|
143
163
|
} catch (err) {
|
|
144
164
|
svg = `<p style="font:13px ${SANS};color:#d63f7c">${esc(err.message)}</p>`;
|
|
145
165
|
}
|
|
146
166
|
// The live chart has its own tooltips, so drop the <title> browsers would show as a second one.
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
// toSVG() keeps the <title> for static images.
|
|
168
|
+
const box = this.#box;
|
|
169
|
+
box.innerHTML = svg.replace(/<title>[\s\S]*?<\/title>/, '');
|
|
170
|
+
// The focusable wrapper carries the chart's name, so screen readers hear it once, not twice.
|
|
171
|
+
const img = box.querySelector('svg');
|
|
172
|
+
if (img) {
|
|
173
|
+
box.setAttribute('aria-label', img.getAttribute('aria-label'));
|
|
174
|
+
img.removeAttribute('role');
|
|
175
|
+
img.removeAttribute('aria-label');
|
|
176
|
+
img.setAttribute('aria-hidden', 'true');
|
|
177
|
+
} else {
|
|
178
|
+
box.removeAttribute('aria-label');
|
|
179
|
+
}
|
|
180
|
+
// Charts with tooltips take keyboard focus too, so keyboard users get the same exact values as mouse users.
|
|
181
|
+
const keys = !!img && !this.hasAttribute('no-tooltips');
|
|
182
|
+
if (keys) {
|
|
183
|
+
box.setAttribute('tabindex', '0');
|
|
184
|
+
box.setAttribute('aria-describedby', 'h');
|
|
185
|
+
} else {
|
|
186
|
+
box.removeAttribute('tabindex');
|
|
187
|
+
box.removeAttribute('aria-describedby');
|
|
188
|
+
}
|
|
189
|
+
box.style.color = THEMES[o.theme === 'light' || o.theme === 'dark' ? o.theme : o.autoTheme].fg;
|
|
190
|
+
box.style.borderRadius = `${o.frame === 'none' ? 4 : clamp(+o.corners || 22, 0, 60)}px`;
|
|
191
|
+
this.#table.innerHTML = data && data.rows.length ? tableHTML(o.heading, data) : '';
|
|
192
|
+
if (this.#at >= 0 && keys && this.shadowRoot.activeElement === box) this.#show(this.#at);
|
|
193
|
+
else this.#at = -1;
|
|
150
194
|
this.#rendered = true;
|
|
151
195
|
this.dispatchEvent(new CustomEvent('bezel-render', { bubbles: true }));
|
|
152
196
|
}
|
|
153
197
|
|
|
154
198
|
#tip(e) {
|
|
155
|
-
|
|
156
|
-
if (!tip || this.hasAttribute('no-tooltips')) return;
|
|
199
|
+
if (this.hasAttribute('no-tooltips')) return;
|
|
157
200
|
const target = e.target?.closest?.('[data-tip]');
|
|
158
201
|
if (!target) {
|
|
159
|
-
|
|
202
|
+
this.#tipEl.classList.remove('on');
|
|
160
203
|
return;
|
|
161
204
|
}
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
this.#place(target, e.clientX, e.clientY);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Shows the tooltip for a mark at a point on screen. */
|
|
209
|
+
#place(mark, x, y) {
|
|
210
|
+
const tip = this.#tipEl;
|
|
211
|
+
tip.textContent = mark.getAttribute('data-tip');
|
|
212
|
+
const box = this.getBoundingClientRect?.() || { left: 0, top: 0, width: 0 };
|
|
164
213
|
const half = tip.offsetWidth / 2;
|
|
165
|
-
tip.style.left = `${clamp(
|
|
166
|
-
tip.style.top = `${
|
|
214
|
+
tip.style.left = `${clamp(x - box.left, half, Math.max(half, box.width - half))}px`;
|
|
215
|
+
tip.style.top = `${y - box.top}px`;
|
|
167
216
|
tip.classList.add('on');
|
|
168
217
|
}
|
|
218
|
+
|
|
219
|
+
/** Every value the arrow keys visit, in order. Donuts mark their slices data-skip, as the legend rows repeat them. */
|
|
220
|
+
#marks() {
|
|
221
|
+
return [...this.#box.querySelectorAll('[data-tip]:not([data-skip])')];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#key(e) {
|
|
225
|
+
const marks = this.#marks(), n = marks.length;
|
|
226
|
+
if (!n) return;
|
|
227
|
+
let i = this.#at, step = STEPS[e.key];
|
|
228
|
+
if (step) {
|
|
229
|
+
if (this.chartType === 'heatmap' && /Left|Right/.test(e.key)) step *= 7;
|
|
230
|
+
i = i < 0 ? (step > 0 ? 0 : n - 1) : clamp(i + step, 0, n - 1);
|
|
231
|
+
} else if (e.key === 'Home' || e.key === 'End') {
|
|
232
|
+
i = e.key === 'Home' ? 0 : n - 1;
|
|
233
|
+
} else {
|
|
234
|
+
if (e.key === 'Escape') this.#clear();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
this.#show(i, true);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Outlines value i, shows its tooltip and, when say is set, reads it out. */
|
|
242
|
+
#show(i, say) {
|
|
243
|
+
const marks = this.#marks();
|
|
244
|
+
if (!marks.length) return;
|
|
245
|
+
i = clamp(i, 0, marks.length - 1);
|
|
246
|
+
this.#box.querySelector('.k')?.classList.remove('k');
|
|
247
|
+
const mark = marks[i], r = mark.getBoundingClientRect?.() || { left: 0, top: 0, width: 0 };
|
|
248
|
+
mark.classList.add('k');
|
|
249
|
+
this.#at = i;
|
|
250
|
+
this.#place(mark, r.left + r.width / 2, r.top);
|
|
251
|
+
if (say) this.#live.textContent = `${mark.getAttribute('data-tip').replace(/\n/g, ', ')}. ${i + 1} of ${marks.length}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#clear() {
|
|
255
|
+
this.#box.querySelector('.k')?.classList.remove('k');
|
|
256
|
+
this.#tipEl.classList.remove('on');
|
|
257
|
+
this.#live.textContent = '';
|
|
258
|
+
this.#at = -1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The visually hidden table screen readers can browse: the chart's numbers in rows and columns. */
|
|
263
|
+
function tableHTML(caption, { head, rows }) {
|
|
264
|
+
const cells = (row) => row.map((c, i) => (i ? `<td>${esc(c)}</td>` : `<th scope="row">${esc(c)}</th>`)).join('');
|
|
265
|
+
return `<table><caption>${esc(caption || 'Chart data')}</caption><thead><tr>${head.map((h) => `<th scope="col">${esc(h)}</th>`).join('')}</tr></thead>` +
|
|
266
|
+
`<tbody>${rows.map((r) => `<tr>${cells(r)}</tr>`).join('')}</tbody></table>`;
|
|
169
267
|
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { TYPES, render, toSVG } from './render.js';
|
|
|
7
7
|
import { THEMES } from './core/themes.js';
|
|
8
8
|
import { BezelChart } from './element.js';
|
|
9
9
|
|
|
10
|
-
export const version = '1.
|
|
10
|
+
export const version = '1.2.0';
|
|
11
11
|
export const types = Object.keys(TYPES);
|
|
12
12
|
export { render, toSVG, BezelChart, THEMES as themes };
|
|
13
13
|
|
package/src/render.js
CHANGED
|
@@ -20,18 +20,31 @@ const camel = (k) => k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
|
20
20
|
* @param {string} type one of TYPES
|
|
21
21
|
* @param {object} [options] the same names as the HTML attributes, in kebab-case or camelCase
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
function prepare(type, options) {
|
|
24
24
|
if (!TYPES[type]) {
|
|
25
25
|
throw new Error(`Bezel Charts: unknown chart type "${type}". Choose one of: ${Object.keys(TYPES).join(', ')}.`);
|
|
26
26
|
}
|
|
27
27
|
const o = {};
|
|
28
28
|
for (const [k, v] of Object.entries(options)) o[camel(k)] = v;
|
|
29
|
+
return o;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function render(type, options = {}) {
|
|
33
|
+
const o = prepare(type, options);
|
|
29
34
|
o.animate = bool(o.animate);
|
|
30
35
|
o.frame = o.frame === 'none' ? 'none' : 'bezel';
|
|
31
36
|
const mode = o.theme === 'light' || o.theme === 'dark' ? o.theme : o.autoTheme || 'dark';
|
|
32
37
|
return TYPES[type](o, THEMES[mode]);
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The chart's data as a table, for screen readers and anyone who wants the exact numbers:
|
|
42
|
+
* { head: [column names], rows: [[cells], ...] }, with numbers formatted the same way as the chart.
|
|
43
|
+
*/
|
|
44
|
+
export function table(type, options = {}) {
|
|
45
|
+
return TYPES[type].table(prepare(type, options));
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/** Strips hover targets and tooltip data so the SVG is a clean static image. */
|
|
36
49
|
export function toStatic(svg) {
|
|
37
50
|
return svg
|