bezel-charts 1.1.1 → 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.
@@ -1,4 +1,4 @@
1
- /*! Bezel Charts v1.1.1 | MIT | github.com/ShazMoghaddam/bezel-charts */
1
+ /*! Bezel Charts v1.2.0 | MIT | github.com/ShazMoghaddam/bezel-charts */
2
2
 
3
3
  // src/core/themes.js
4
4
  var THEMES = {
@@ -180,24 +180,67 @@ function statOf(v, o, fallback) {
180
180
  if (k === "last" && v.length > 1 && o.delta !== "none") delta = v[v.length - 1] - v[0];
181
181
  return { val: map[k], delta };
182
182
  }
183
- function legend(names, pal, th, x0, y0, w) {
184
- var x = x0, y = y0, s = "";
183
+ function legend(names, pal, th, x0, y0, w, key, kw) {
184
+ var x = x0, y = y0, s = "", off = kw || 13;
185
185
  names.forEach(function(nm, k) {
186
- var wd = 13 + tw(nm, 12) + 16;
186
+ var wd = off + tw(nm, 12) + 16;
187
187
  if (x > x0 && x + wd - 16 > x0 + w) {
188
188
  x = x0;
189
189
  y += 20;
190
190
  }
191
- s += '<circle cx="' + r2(x + 4) + '" cy="' + r2(y + 6) + '" r="4" fill="' + pal[k % pal.length] + '"/>' + text(x + 13, y + 10, nm, 'fill="' + th.fg + '" font-family="' + SANS + '" font-size="12"');
191
+ s += (key ? key(k, x, y) : '<circle cx="' + r2(x + 4) + '" cy="' + r2(y + 6) + '" r="4" fill="' + pal[k % pal.length] + '"/>') + text(x + off, y + 10, nm, 'fill="' + th.fg + '" font-family="' + SANS + '" font-size="12"');
192
192
  x += wd;
193
193
  });
194
194
  return { svg: s, height: y - y0 + 24 };
195
195
  }
196
+ var DASHES = { solid: "", dashed: "7 5", dotted: "1 5", dashdot: "9 4 1 4" };
197
+ var SHAPES = ["circle", "square", "triangle", "diamond"];
198
+ var PATTERNS = ["solid", "stripes", "dots", "grid"];
199
+ function styleOf(v, names, off) {
200
+ if (v == null || v === false || v === "none" || v === "false") return null;
201
+ var l = v === true || v === "" || v === "auto" ? names : list(v).map(function(s) {
202
+ return names.indexOf(s) < 0 ? off : s;
203
+ });
204
+ if (!l.length) return null;
205
+ return function(k) {
206
+ return l[k % l.length];
207
+ };
208
+ }
209
+ function marker(kind, x, y, r, attrs) {
210
+ var a = r * 1.15, b = r * 0.85;
211
+ if (kind === "square") return '<rect x="' + r2(x - b) + '" y="' + r2(y - b) + '" width="' + r2(2 * b) + '" height="' + r2(2 * b) + '" rx="1"' + attrs + "/>";
212
+ 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 + "/>";
213
+ 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 + "/>";
214
+ return '<circle cx="' + r2(x) + '" cy="' + r2(y) + '" r="' + r2(r) + '"' + attrs + "/>";
215
+ }
216
+ function pattern(id, kind, c, bg) {
217
+ var ink = ' stroke="' + bg + '" stroke-opacity=".55" stroke-width="1.5"';
218
+ return '<pattern id="' + id + '" width="6" height="6" patternUnits="userSpaceOnUse"' + (kind === "dots" ? "" : ' patternTransform="rotate(45)"') + '><rect width="6" height="6" fill="' + c + '"/>' + (kind === "dots" ? '<circle cx="3" cy="3" r="1.3" fill="' + bg + '" fill-opacity=".55"/>' : '<path d="M3,0V6' + (kind === "grid" ? "M0,3H6" : "") + '"' + ink + "/>") + "</pattern>";
219
+ }
196
220
  function axisWidth(ticks) {
197
221
  return Math.max.apply(null, ticks.map(function(t) {
198
222
  return tw(t, 10, true);
199
223
  })) + 10;
200
224
  }
225
+ function seriesTable(o, total) {
226
+ var S = series(o.values), names = list(o.series), labels = list(o.labels), rows = [];
227
+ var n = Math.max.apply(null, [0].concat(S.map(function(s) {
228
+ return s.length;
229
+ })));
230
+ for (var i = 0; i < n; i++) {
231
+ var row = [labels[i] || "#" + (i + 1)], t = 0;
232
+ S.forEach(function(s) {
233
+ row.push(i < s.length ? fmt(s[i], o) : "");
234
+ t += i < s.length ? s[i] : 0;
235
+ });
236
+ if (total) row.push(fmt(t, o));
237
+ rows.push(row);
238
+ }
239
+ var head = S.length > 1 || names.length ? S.map(function(_, k) {
240
+ return names[k] || "Series " + (k + 1);
241
+ }) : ["Value"];
242
+ return { head: ["Label"].concat(head, total ? ["Total"] : []), rows };
243
+ }
201
244
 
202
245
  // src/core/tile.js
203
246
  function dims(o) {
@@ -295,6 +338,12 @@ function spark(o, th) {
295
338
  return s;
296
339
  }, { stat: st, summary: v.length ? "latest " + fmt(v[v.length - 1], o) : "no data" });
297
340
  }
341
+ spark.table = function(o) {
342
+ var labels = list(o.labels);
343
+ return { head: ["Label", "Value"], rows: nums(o.values).map(function(n, i) {
344
+ return [labels[i] || "#" + (i + 1), fmt(n, o)];
345
+ }) };
346
+ };
298
347
 
299
348
  // src/charts/line.js
300
349
  function line(o, th) {
@@ -304,13 +353,19 @@ function line(o, th) {
304
353
  })));
305
354
  var notes = parseNotes(o, labels), multi = S.length > 1 || names.length > 0;
306
355
  var st = statOf(S[0] || [], o, "");
356
+ var dashOf = styleOf(o.dash, Object.keys(DASHES), "solid"), markOf = styleOf(o.markers, SHAPES, "none");
357
+ var bg = o.background || th.tile;
358
+ var key = (dashOf || markOf) && function(k, x, y) {
359
+ var c = pal[k % pal.length], dk = dashOf ? DASHES[dashOf(k)] : "", mk = markOf ? markOf(k) : "none";
360
+ return '<line x1="' + r2(x) + '" x2="' + r2(x + 18) + '" y1="' + r2(y + 6) + '" y2="' + r2(y + 6) + '" stroke="' + c + '" stroke-width="2.5" stroke-linecap="round"' + (dk ? ' stroke-dasharray="' + dk + '"' : "") + "/>" + (mk !== "none" ? marker(mk, x + 9, y + 6, 3.5, ' fill="' + c + '" stroke="' + bg + '" stroke-width="1.5"') : "");
361
+ };
307
362
  return tile(o, th, h, function(x0, y0, w, h2) {
308
363
  if (!n) return emptyState(th, x0, y0, w, h2);
309
364
  var s = "", top = y0;
310
365
  if (multi) {
311
366
  var lg = legend(S.map(function(_, k) {
312
367
  return names[k] || "Series " + (k + 1);
313
- }), pal, th, x0, top, w);
368
+ }), pal, th, x0, top, w, key, key && 24);
314
369
  s += lg.svg;
315
370
  top += lg.height;
316
371
  }
@@ -369,9 +424,17 @@ function line(o, th) {
369
424
  var g = newId("g"), last = P[P.length - 1];
370
425
  s += '<defs><linearGradient id="' + g + '" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="' + c + '" stop-opacity=".26"/><stop offset="1" stop-color="' + c + '" stop-opacity="0"/></linearGradient></defs><path d="' + d + "L" + r2(last[0]) + "," + r2(top + ph) + "L" + r2(P[0][0]) + "," + r2(top + ph) + 'Z" fill="url(#' + g + ')"' + anim(o, "bzc-a") + "/>";
371
426
  }
372
- s += '<path d="' + d + '" fill="none" stroke="' + c + '" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" pathLength="1" style="stroke-dasharray:1' + (o.animate ? ";animation-delay:" + k * 0.15 + "s" : "") + '"' + (o.animate ? ' class="bzc-d"' : "") + "/>";
373
- var e = P[P.length - 1];
374
- s += '<circle cx="' + r2(e[0]) + '" cy="' + r2(e[1]) + '" r="4" fill="' + c + '" stroke="' + (o.background || th.tile) + '" stroke-width="2"/>';
427
+ var dk = dashOf ? DASHES[dashOf(k)] : "", mk = markOf ? markOf(k) : "none";
428
+ s += dk ? '<path d="' + d + '" fill="none" stroke="' + c + '" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="' + dk + '"' + anim(o, "bzc-a", k * 0.15) + "/>" : '<path d="' + d + '" fill="none" stroke="' + c + '" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" pathLength="1" style="stroke-dasharray:1' + (o.animate ? ";animation-delay:" + k * 0.15 + "s" : "") + '"' + (o.animate ? ' class="bzc-d"' : "") + "/>";
429
+ if (mk !== "none") {
430
+ var every2 = Math.ceil(v.length / 40);
431
+ v.forEach(function(y, i2) {
432
+ if (i2 % every2 === 0 || i2 === v.length - 1) s += marker(mk, X(i2), Y(y), 3.5, ' fill="' + c + '" stroke="' + bg + '" stroke-width="1.5"' + anim(o, "bzc-a", 0.6 + k * 0.15));
433
+ });
434
+ } else {
435
+ var e = P[P.length - 1];
436
+ s += '<circle cx="' + r2(e[0]) + '" cy="' + r2(e[1]) + '" r="4" fill="' + c + '" stroke="' + bg + '" stroke-width="2"/>';
437
+ }
375
438
  if (bool(o.showValues)) v.forEach(function(y, i2) {
376
439
  s += text(X(i2), Y(y) - 8, fmt(y, o, true), small(th, 9, 'text-anchor="middle"'));
377
440
  });
@@ -388,6 +451,9 @@ function line(o, th) {
388
451
  return s;
389
452
  }, { stat: st, summary: S.length + " series over " + n + " points" });
390
453
  }
454
+ line.table = function(o) {
455
+ return seriesTable(o);
456
+ };
391
457
 
392
458
  // src/charts/bars.js
393
459
  var ANGLE = 40;
@@ -416,8 +482,18 @@ function bars(o, th) {
416
482
  var nameOf = function(j) {
417
483
  return names[j] || "Series " + (j + 1);
418
484
  };
485
+ var patOf = grouped ? styleOf(o.patterns, PATTERNS, "solid") : null, defs = "", fills = [];
486
+ for (var p = 0; patOf && p < k; p++) {
487
+ if (patOf(p) === "solid") continue;
488
+ var pid = newId("g");
489
+ defs += pattern(pid, patOf(p), pal[p % pal.length], o.background || th.tile);
490
+ fills[p] = "url(#" + pid + ")";
491
+ }
419
492
  var colourOf = function(j, i) {
420
- return grouped ? pal[j % pal.length] : many ? pal[i % pal.length] : pal[0];
493
+ return grouped ? fills[j] || pal[j % pal.length] : many ? pal[i % pal.length] : pal[0];
494
+ };
495
+ var key = patOf && function(j, x, y) {
496
+ return '<rect x="' + r2(x) + '" y="' + r2(y + 1) + '" width="10" height="10" rx="2" fill="' + colourOf(j) + '"/>';
421
497
  };
422
498
  var opac = function(val) {
423
499
  return grouped || many || o.highlight === "none" || val === top ? 1 : 0.5;
@@ -434,7 +510,7 @@ function bars(o, th) {
434
510
  var d0 = dims(o);
435
511
  var legendH = grouped || names.length ? legend(S.map(function(_, j) {
436
512
  return nameOf(j);
437
- }), pal, th, 0, 0, d0.w).height : 0;
513
+ }), pal, th, 0, 0, d0.w, key, key && 15).height : 0;
438
514
  var rows = stacked ? 1 : k, thin = grouped && !stacked;
439
515
  var subH = thin ? 10 : 14, subGap = 3;
440
516
  var rowH = horiz ? rows * subH + (rows - 1) * subGap + (thin ? 14 : 16) : 0;
@@ -442,11 +518,11 @@ function bars(o, th) {
442
518
  var best = v0.indexOf(Math.max.apply(null, v0.length ? v0 : [0]));
443
519
  return tile(o, th, h, function(x0, y0, w, h2) {
444
520
  if (!n) return emptyState(th, x0, y0, w, h2);
445
- var s = "", top0 = y0;
521
+ var s = defs ? "<defs>" + defs + "</defs>" : "", top0 = y0;
446
522
  if (legendH) {
447
523
  s += legend(S.map(function(_, j) {
448
524
  return nameOf(j);
449
- }), pal, th, x0, y0, w).svg;
525
+ }), pal, th, x0, y0, w, key, key && 15).svg;
450
526
  top0 += legendH;
451
527
  }
452
528
  var lo = Math.min(0, Math.min.apply(null, scale)), hi = Math.max(0, top);
@@ -597,6 +673,9 @@ function bars(o, th) {
597
673
  summary: !n ? "no data" : stacked ? k + " series stacked across " + n + " groups" : grouped ? k + " series across " + n + " groups" : "highest " + (labels[best] || "") + " " + fmt(v0[best], o)
598
674
  });
599
675
  }
676
+ bars.table = function(o) {
677
+ return seriesTable(o, series(o.values).length > 1 && bool(o.stacked));
678
+ };
600
679
 
601
680
  // src/charts/ring.js
602
681
  function ring(o, th) {
@@ -636,6 +715,12 @@ function ring(o, th) {
636
715
  return (labels[i] ? labels[i] + " " : "") + show(n);
637
716
  }).join(", ") });
638
717
  }
718
+ ring.table = function(o) {
719
+ var labels = list(o.labels), max = num(o.max, 100) || 100, auto = !has(o.prefix) && !has(o.suffix) && max === 100;
720
+ return { head: ["Label", "Value"], rows: nums(o.values).map(function(n, i) {
721
+ return [labels[i] || "#" + (i + 1), fmt(n, o) + (auto ? "%" : " of " + fmt(max, o))];
722
+ }) };
723
+ };
639
724
 
640
725
  // src/charts/donut.js
641
726
  function donut(o, th) {
@@ -688,12 +773,20 @@ function donut(o, th) {
688
773
  v.forEach(function(n, i) {
689
774
  var len = n / total * 100;
690
775
  if (len <= 0) return;
691
- s += '<circle cx="' + r2(cx) + '" cy="' + r2(cy) + '" r="' + r2(r) + '" fill="none" stroke="#000" stroke-opacity="0" stroke-width="' + r2(sw) + '" pathLength="100" stroke-dasharray="' + r2(len) + " " + r2(100 - len) + '" stroke-dashoffset="' + r2(-start) + '" transform="rotate(-90 ' + r2(cx) + " " + r2(cy) + ')" class="bzc-hit" data-tip="' + esc((labels[i] || "#" + (i + 1)) + ": " + fmt(n, o) + " (" + Math.round(len) + "%)") + '"/>';
776
+ s += '<circle cx="' + r2(cx) + '" cy="' + r2(cy) + '" r="' + r2(r) + '" fill="none" stroke="#000" stroke-opacity="0" stroke-width="' + r2(sw) + '" pathLength="100" stroke-dasharray="' + r2(len) + " " + r2(100 - len) + '" stroke-dashoffset="' + r2(-start) + '" transform="rotate(-90 ' + r2(cx) + " " + r2(cy) + ')" class="bzc-hit" data-skip="" data-tip="' + esc((labels[i] || "#" + (i + 1)) + ": " + fmt(n, o) + " (" + Math.round(len) + "%)") + '"/>';
692
777
  start += len;
693
778
  });
694
779
  return s;
695
780
  }, { stat: st, summary: "total " + fmt(total, o) + " across " + v.length + " parts" });
696
781
  }
782
+ donut.table = function(o) {
783
+ var v = nums(o.values).map(function(n) {
784
+ return Math.max(0, n);
785
+ }), labels = list(o.labels), total = sum(v);
786
+ return { head: ["Label", "Value", "Share"], rows: v.map(function(n, i) {
787
+ return [labels[i] || "#" + (i + 1), fmt(n, o), (total ? Math.round(n / total * 100) : 0) + "%"];
788
+ }) };
789
+ };
697
790
 
698
791
  // src/charts/gauge.js
699
792
  function gauge(o, th) {
@@ -740,6 +833,12 @@ function gauge(o, th) {
740
833
  return s;
741
834
  }, { summary: isFinite(val) ? show(val) + " on a scale of " + fmt(mn, o) + " to " + fmt(mx, o) : "no data" });
742
835
  }
836
+ gauge.table = function(o) {
837
+ var v = nums(o.values), val = v.length ? v[0] : num(o.value, NaN), labels = list(o.labels);
838
+ var mn = num(o.min, 0), mx = num(o.max, 100), auto = !has(o.prefix) && !has(o.suffix) && mn === 0 && mx === 100;
839
+ if (!isFinite(val)) return { head: ["Label", "Value"], rows: [] };
840
+ return { head: ["Label", "Value"], rows: [[labels[0] || "Reading", fmt(val, o) + (auto ? "%" : "")], ["Lowest", fmt(mn, o)], ["Highest", fmt(mx, o)]].concat(has(o.goal) ? [["Goal", fmt(+o.goal, o)]] : []) };
841
+ };
743
842
 
744
843
  // src/charts/progress.js
745
844
  function progress(o, th) {
@@ -802,16 +901,36 @@ function progress(o, th) {
802
901
  return s;
803
902
  }, { stat: st, summary: single ? show(v[0]) + (auto ? "" : " of " + fmt(max, o)) : fmt(total, o) + " of " + fmt(max, o) });
804
903
  }
904
+ progress.table = function(o) {
905
+ var v = nums(o.values).map(function(n) {
906
+ return Math.max(0, n);
907
+ }), labels = list(o.labels), single = v.length === 1;
908
+ var max = has(o.max) ? num(o.max, 100) : single ? 100 : sum(v);
909
+ if (!(max > 0)) max = 1;
910
+ var auto = !has(o.prefix) && !has(o.suffix) && max === 100 && single;
911
+ if (single) return { head: ["Label", "Value"], rows: [[labels[0] || "Progress", fmt(v[0], o) + (auto ? "%" : " of " + fmt(max, o))]] };
912
+ return { head: ["Label", "Value", "Share"], rows: v.map(function(n, i) {
913
+ return [labels[i] || "#" + (i + 1), fmt(n, o), Math.round(n / max * 100) + "%"];
914
+ }) };
915
+ };
805
916
 
806
917
  // src/charts/heatmap.js
918
+ var FULL = { weekday: "short", day: "numeric", month: "short", year: "numeric" };
919
+ function startOf(o, len) {
920
+ if (has(o.start) && /^\d{4}-\d{2}-\d{2}$/.test(String(o.start))) return /* @__PURE__ */ new Date(String(o.start) + "T00:00:00Z");
921
+ var t = /* @__PURE__ */ new Date();
922
+ return new Date(Date.UTC(t.getFullYear(), t.getMonth(), t.getDate()) - Math.max(0, len - 1) * 864e5);
923
+ }
924
+ function dates(lang, opt) {
925
+ try {
926
+ return new Intl.DateTimeFormat(lang, Object.assign({ timeZone: "UTC" }, opt));
927
+ } catch (e) {
928
+ return new Intl.DateTimeFormat("en-GB", Object.assign({ timeZone: "UTC" }, opt));
929
+ }
930
+ }
807
931
  function heatmap(o, th) {
808
932
  var v = nums(o.values), c = palette(o, th)[0], lang = o.lang || "en-GB";
809
- var start;
810
- if (has(o.start) && /^\d{4}-\d{2}-\d{2}$/.test(String(o.start))) start = /* @__PURE__ */ new Date(String(o.start) + "T00:00:00Z");
811
- else {
812
- var t = /* @__PURE__ */ new Date();
813
- start = new Date(Date.UTC(t.getFullYear(), t.getMonth(), t.getDate()) - Math.max(0, v.length - 1) * 864e5);
814
- }
933
+ var start = startOf(o, v.length);
815
934
  var offset = (start.getUTCDay() + 6) % 7, cols = Math.max(1, Math.ceil((offset + v.length) / 7));
816
935
  var d0 = dims(o), leftW = 28, maxCols = Math.max(1, Math.floor((d0.w - leftW) / 10));
817
936
  if (cols > maxCols) {
@@ -825,13 +944,9 @@ function heatmap(o, th) {
825
944
  var h = 16 + 7 * pitch + 26;
826
945
  var max = v.length ? Math.max.apply(null, v) : 0;
827
946
  var dfm = function(opt) {
828
- try {
829
- return new Intl.DateTimeFormat(lang, Object.assign({ timeZone: "UTC" }, opt));
830
- } catch (e) {
831
- return new Intl.DateTimeFormat("en-GB", Object.assign({ timeZone: "UTC" }, opt));
832
- }
947
+ return dates(lang, opt);
833
948
  };
834
- var monthF = dfm({ month: "short" }), dayF = dfm({ weekday: "short" }), fullF = dfm({ weekday: "short", day: "numeric", month: "short", year: "numeric" });
949
+ var monthF = dfm({ month: "short" }), dayF = dfm({ weekday: "short" }), fullF = dfm(FULL);
835
950
  var st = statOf(v, o, "");
836
951
  return tile(o, th, h, function(x0, y0, w, h2) {
837
952
  if (!v.length) return emptyState(th, x0, y0, w, h2);
@@ -861,6 +976,12 @@ function heatmap(o, th) {
861
976
  return s;
862
977
  }, { stat: st, summary: fmt(sum(v), o) + " across " + v.length + " days" });
863
978
  }
979
+ heatmap.table = function(o) {
980
+ var v = nums(o.values), start = startOf(o, v.length), f = dates(o.lang || "en-GB", FULL);
981
+ return { head: ["Date", "Value"], rows: v.map(function(n, i) {
982
+ return [f.format(new Date(start.getTime() + i * 864e5)), fmt(n, o)];
983
+ }) };
984
+ };
864
985
 
865
986
  // src/charts/scatter.js
866
987
  function fit(xs, ys) {
@@ -902,13 +1023,21 @@ function scatter(o, th) {
902
1023
  };
903
1024
  var xo = { decimals: o.decimals, compact: o.compact, lang: o.lang };
904
1025
  var notes = parseNotes(o, labels);
1026
+ var markOf = styleOf(o.markers, SHAPES, "circle"), bg = o.background || th.tile;
1027
+ var shapeOf = function(j) {
1028
+ var m2 = markOf ? markOf(j) : "circle";
1029
+ return m2 === "none" ? "circle" : m2;
1030
+ };
1031
+ var key = markOf && function(j, x, y) {
1032
+ return marker(shapeOf(j), x + 4, y + 6, 4, ' fill="' + pal[j % pal.length] + '"');
1033
+ };
905
1034
  return tile(o, th, h, function(x0, y0, w, h2) {
906
1035
  if (!count) return emptyState(th, x0, y0, w, h2);
907
1036
  var s = "", top = y0;
908
1037
  if (multi) {
909
1038
  var lg = legend(groups.map(function(_, j) {
910
1039
  return nameOf(j);
911
- }), pal, th, x0, top, w);
1040
+ }), pal, th, x0, top, w, key);
912
1041
  s += lg.svg;
913
1042
  top += lg.height;
914
1043
  }
@@ -975,7 +1104,7 @@ function scatter(o, th) {
975
1104
  gr.xs.forEach(function(xv, i) {
976
1105
  var cx = PX(xv), cy2 = PY(gr.ys[i]);
977
1106
  var tip = (labels[idx] ? labels[idx] + "\n" : multi ? nameOf(j) + "\n" : "") + (has(o.xLabel) ? o.xLabel : "x") + ": " + fmt(xv, xo) + "\n" + (has(o.yLabel) ? o.yLabel : "y") + ": " + fmt(gr.ys[i], o);
978
- s += '<circle cx="' + r2(cx) + '" cy="' + r2(cy2) + '" r="' + r + '" fill="' + c + '" fill-opacity=".85" stroke="' + (o.background || th.tile) + '" stroke-width="1.5"' + (o.animate ? ' class="bzc-a" style="animation-delay:' + r2(Math.min(0.8, idx * 0.012)) + 's"' : "") + "/>";
1107
+ s += marker(shapeOf(j), cx, cy2, r, ' fill="' + c + '" fill-opacity=".85" stroke="' + bg + '" stroke-width="1.5"' + (o.animate ? ' class="bzc-a" style="animation-delay:' + r2(Math.min(0.8, idx * 0.012)) + 's"' : ""));
979
1108
  s += hit(cx - 8, cy2 - 8, 16, 16, tip);
980
1109
  idx++;
981
1110
  });
@@ -994,21 +1123,39 @@ function scatter(o, th) {
994
1123
  return s;
995
1124
  }, { summary: count + " points" + (multi ? " in " + groups.length + " groups" : "") });
996
1125
  }
1126
+ scatter.table = function(o) {
1127
+ var X = series(o.x), Y = series(o.y), names = list(o.series), labels = list(o.labels), multi = X.length > 1 || names.length > 0;
1128
+ var xo = { decimals: o.decimals, compact: o.compact, lang: o.lang }, rows = [], idx = 0;
1129
+ X.forEach(function(xs, g) {
1130
+ var ys = Y[g] || [];
1131
+ for (var i = 0; i < Math.min(xs.length, ys.length); i++, idx++) {
1132
+ rows.push([labels[idx] || "#" + (idx + 1)].concat(multi ? [names[g] || "Group " + (g + 1)] : [], [fmt(xs[i], xo), fmt(ys[i], o)]));
1133
+ }
1134
+ });
1135
+ return { head: ["Point"].concat(multi ? ["Group"] : [], [has(o.xLabel) ? o.xLabel : "x", has(o.yLabel) ? o.yLabel : "y"]), rows };
1136
+ };
997
1137
 
998
1138
  // src/render.js
999
1139
  var TYPES = { spark, line, bars, scatter, ring, donut, gauge, progress, heatmap };
1000
1140
  var camel = (k) => k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1001
- function render(type, options = {}) {
1141
+ function prepare(type, options) {
1002
1142
  if (!TYPES[type]) {
1003
1143
  throw new Error(`Bezel Charts: unknown chart type "${type}". Choose one of: ${Object.keys(TYPES).join(", ")}.`);
1004
1144
  }
1005
1145
  const o = {};
1006
1146
  for (const [k, v] of Object.entries(options)) o[camel(k)] = v;
1147
+ return o;
1148
+ }
1149
+ function render(type, options = {}) {
1150
+ const o = prepare(type, options);
1007
1151
  o.animate = bool(o.animate);
1008
1152
  o.frame = o.frame === "none" ? "none" : "bezel";
1009
1153
  const mode = o.theme === "light" || o.theme === "dark" ? o.theme : o.autoTheme || "dark";
1010
1154
  return TYPES[type](o, THEMES[mode]);
1011
1155
  }
1156
+ function table(type, options = {}) {
1157
+ return TYPES[type].table(prepare(type, options));
1158
+ }
1012
1159
  function toStatic(svg) {
1013
1160
  return svg.replace(/<rect class="bzc-hit"[^>]*\/>/g, "").replace(/<circle[^>]* class="bzc-hit"[^>]*\/>/g, "").replace(/ data-tip="[^"]*"/g, "");
1014
1161
  }
@@ -1060,9 +1207,13 @@ var ATTRIBUTES = [
1060
1207
  "static",
1061
1208
  "no-tooltips",
1062
1209
  "stacked",
1063
- "angle-labels"
1210
+ "angle-labels",
1211
+ "dash",
1212
+ "markers",
1213
+ "patterns"
1064
1214
  ];
1065
- var HOST_CSS = `:host{display:block;position:relative;max-width:100%}:host([hidden]){display:none}svg{display:block;max-width:100%;height:auto;overflow:visible}.tip{position:absolute;left:0;top:0;pointer-events:none;background:#0b0b10;color:#f2f1f7;font:600 12px/1.5 ${MONO};padding:7px 10px;border-radius:10px;white-space:pre;opacity:0;transform:translate(-50%,calc(-100% - 12px));transition:opacity .12s;z-index:3;border:1px solid #2a2935;box-shadow:0 8px 24px rgba(0,0,0,.35)}.tip.on{opacity:1}`;
1215
+ var HOST_CSS = `:host{display:block;position:relative;max-width:100%}:host([hidden]){display:none}svg{display:block;max-width:100%;height:auto;overflow:visible}.tip{position:absolute;left:0;top:0;pointer-events:none;background:#0b0b10;color:#f2f1f7;font:600 12px/1.5 ${MONO};padding:7px 10px;border-radius:10px;white-space:pre;opacity:0;transform:translate(-50%,calc(-100% - 12px));transition:opacity .12s;z-index:3;border:1px solid #2a2935;box-shadow:0 8px 24px rgba(0,0,0,.35)}.tip.on{opacity:1}.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}.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}@media (forced-colors:active){.c:focus-visible{outline-color:Highlight}}`;
1216
+ var STEPS = { ArrowLeft: -1, ArrowUp: -1, ArrowRight: 1, ArrowDown: 1 };
1066
1217
  var darkQuery = () => typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : null;
1067
1218
  var pageTheme = () => {
1068
1219
  const t = typeof document !== "undefined" && document.documentElement?.getAttribute?.("data-theme");
@@ -1093,10 +1244,13 @@ var BezelChart = class extends Base {
1093
1244
  }
1094
1245
  connectedCallback() {
1095
1246
  if (!this.shadowRoot) {
1096
- this.attachShadow({ mode: "open" });
1247
+ this.attachShadow({ mode: "open" }).innerHTML = `<style>${HOST_CSS}</style><div class="c" role="group" aria-roledescription="chart"></div><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>`;
1248
+ [, this.#box, , this.#tipEl, this.#live, this.#table] = this.shadowRoot.children;
1097
1249
  this.shadowRoot.addEventListener("pointermove", (e) => this.#tip(e));
1098
1250
  this.shadowRoot.addEventListener("pointerdown", (e) => this.#tip(e));
1099
- this.addEventListener("pointerleave", () => this.shadowRoot.querySelector(".tip")?.classList.remove("on"));
1251
+ this.addEventListener("pointerleave", () => this.#tipEl.classList.remove("on"));
1252
+ this.#box.addEventListener("keydown", (e) => this.#key(e));
1253
+ this.#box.addEventListener("blur", () => this.#clear());
1100
1254
  }
1101
1255
  live.add(this);
1102
1256
  this.#dark = darkQuery();
@@ -1153,6 +1307,11 @@ var BezelChart = class extends Base {
1153
1307
  setTimeout(() => URL.revokeObjectURL(url), 1e3);
1154
1308
  }
1155
1309
  #ro = null;
1310
+ #box = null;
1311
+ #tipEl = null;
1312
+ #live = null;
1313
+ #table = null;
1314
+ #at = -1;
1156
1315
  #dark = null;
1157
1316
  #width = 0;
1158
1317
  #rendered = false;
@@ -1167,36 +1326,105 @@ var BezelChart = class extends Base {
1167
1326
  this.#width = w > 0 ? w : 360;
1168
1327
  const o = this.options;
1169
1328
  o.animate = !this.#rendered && !this.hasAttribute("static");
1170
- let svg;
1329
+ let svg, data = null;
1171
1330
  try {
1172
1331
  svg = render(this.chartType, o);
1332
+ data = table(this.chartType, o);
1173
1333
  } catch (err) {
1174
1334
  svg = `<p style="font:13px ${SANS};color:#d63f7c">${esc(err.message)}</p>`;
1175
1335
  }
1176
- svg = svg.replace(/<title>[\s\S]*?<\/title>/, "");
1177
- this.shadowRoot.innerHTML = `<style>${HOST_CSS}</style>${svg}<div class="tip" role="status"></div>`;
1336
+ const box = this.#box;
1337
+ box.innerHTML = svg.replace(/<title>[\s\S]*?<\/title>/, "");
1338
+ const img = box.querySelector("svg");
1339
+ if (img) {
1340
+ box.setAttribute("aria-label", img.getAttribute("aria-label"));
1341
+ img.removeAttribute("role");
1342
+ img.removeAttribute("aria-label");
1343
+ img.setAttribute("aria-hidden", "true");
1344
+ } else {
1345
+ box.removeAttribute("aria-label");
1346
+ }
1347
+ const keys = !!img && !this.hasAttribute("no-tooltips");
1348
+ if (keys) {
1349
+ box.setAttribute("tabindex", "0");
1350
+ box.setAttribute("aria-describedby", "h");
1351
+ } else {
1352
+ box.removeAttribute("tabindex");
1353
+ box.removeAttribute("aria-describedby");
1354
+ }
1355
+ box.style.color = THEMES[o.theme === "light" || o.theme === "dark" ? o.theme : o.autoTheme].fg;
1356
+ box.style.borderRadius = `${o.frame === "none" ? 4 : clamp(+o.corners || 22, 0, 60)}px`;
1357
+ this.#table.innerHTML = data && data.rows.length ? tableHTML(o.heading, data) : "";
1358
+ if (this.#at >= 0 && keys && this.shadowRoot.activeElement === box) this.#show(this.#at);
1359
+ else this.#at = -1;
1178
1360
  this.#rendered = true;
1179
1361
  this.dispatchEvent(new CustomEvent("bezel-render", { bubbles: true }));
1180
1362
  }
1181
1363
  #tip(e) {
1182
- const tip = this.shadowRoot.querySelector(".tip");
1183
- if (!tip || this.hasAttribute("no-tooltips")) return;
1364
+ if (this.hasAttribute("no-tooltips")) return;
1184
1365
  const target = e.target?.closest?.("[data-tip]");
1185
1366
  if (!target) {
1186
- tip.classList.remove("on");
1367
+ this.#tipEl.classList.remove("on");
1187
1368
  return;
1188
1369
  }
1189
- tip.textContent = target.getAttribute("data-tip");
1190
- const box = this.getBoundingClientRect();
1370
+ this.#place(target, e.clientX, e.clientY);
1371
+ }
1372
+ /** Shows the tooltip for a mark at a point on screen. */
1373
+ #place(mark, x, y) {
1374
+ const tip = this.#tipEl;
1375
+ tip.textContent = mark.getAttribute("data-tip");
1376
+ const box = this.getBoundingClientRect?.() || { left: 0, top: 0, width: 0 };
1191
1377
  const half = tip.offsetWidth / 2;
1192
- tip.style.left = `${clamp(e.clientX - box.left, half, Math.max(half, box.width - half))}px`;
1193
- tip.style.top = `${e.clientY - box.top}px`;
1378
+ tip.style.left = `${clamp(x - box.left, half, Math.max(half, box.width - half))}px`;
1379
+ tip.style.top = `${y - box.top}px`;
1194
1380
  tip.classList.add("on");
1195
1381
  }
1382
+ /** Every value the arrow keys visit, in order. Donuts mark their slices data-skip, as the legend rows repeat them. */
1383
+ #marks() {
1384
+ return [...this.#box.querySelectorAll("[data-tip]:not([data-skip])")];
1385
+ }
1386
+ #key(e) {
1387
+ const marks = this.#marks(), n = marks.length;
1388
+ if (!n) return;
1389
+ let i = this.#at, step = STEPS[e.key];
1390
+ if (step) {
1391
+ if (this.chartType === "heatmap" && /Left|Right/.test(e.key)) step *= 7;
1392
+ i = i < 0 ? step > 0 ? 0 : n - 1 : clamp(i + step, 0, n - 1);
1393
+ } else if (e.key === "Home" || e.key === "End") {
1394
+ i = e.key === "Home" ? 0 : n - 1;
1395
+ } else {
1396
+ if (e.key === "Escape") this.#clear();
1397
+ return;
1398
+ }
1399
+ e.preventDefault();
1400
+ this.#show(i, true);
1401
+ }
1402
+ /** Outlines value i, shows its tooltip and, when say is set, reads it out. */
1403
+ #show(i, say) {
1404
+ const marks = this.#marks();
1405
+ if (!marks.length) return;
1406
+ i = clamp(i, 0, marks.length - 1);
1407
+ this.#box.querySelector(".k")?.classList.remove("k");
1408
+ const mark = marks[i], r = mark.getBoundingClientRect?.() || { left: 0, top: 0, width: 0 };
1409
+ mark.classList.add("k");
1410
+ this.#at = i;
1411
+ this.#place(mark, r.left + r.width / 2, r.top);
1412
+ if (say) this.#live.textContent = `${mark.getAttribute("data-tip").replace(/\n/g, ", ")}. ${i + 1} of ${marks.length}`;
1413
+ }
1414
+ #clear() {
1415
+ this.#box.querySelector(".k")?.classList.remove("k");
1416
+ this.#tipEl.classList.remove("on");
1417
+ this.#live.textContent = "";
1418
+ this.#at = -1;
1419
+ }
1196
1420
  };
1421
+ function tableHTML(caption, { head, rows }) {
1422
+ const cells = (row) => row.map((c, i) => i ? `<td>${esc(c)}</td>` : `<th scope="row">${esc(c)}</th>`).join("");
1423
+ return `<table><caption>${esc(caption || "Chart data")}</caption><thead><tr>${head.map((h) => `<th scope="col">${esc(h)}</th>`).join("")}</tr></thead><tbody>${rows.map((r) => `<tr>${cells(r)}</tr>`).join("")}</tbody></table>`;
1424
+ }
1197
1425
 
1198
1426
  // src/index.js
1199
- var version = "1.1.1";
1427
+ var version = "1.2.0";
1200
1428
  var types = Object.keys(TYPES);
1201
1429
  if (typeof customElements !== "undefined" && typeof HTMLElement !== "undefined") {
1202
1430
  for (const type of types) {