@tangle-network/charts 0.12.0 → 0.12.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.
@@ -77,7 +77,7 @@ interface StackedBarChartProps {
77
77
  label: string;
78
78
  series: readonly StackedBarSeries[];
79
79
  buckets: readonly StackedBarBucket[];
80
- /** Explicit [0, maxValue] domain. Zero is valid only for zero-valued data. */
80
+ /** Values must lie in [0, maxValue]; the axis extends to the next round tick. Zero is valid only for zero-valued data. */
81
81
  maxValue: number;
82
82
  formatValue: (value: number) => string;
83
83
  selectedBucketId: string | null;
@@ -217,6 +217,41 @@ function validate({ series, buckets, maxValue }) {
217
217
  }
218
218
  }
219
219
  }
220
+ /**
221
+ * Round axis ticks covering [0, max]: three to six steps of 1, 2, 2.5 or 5
222
+ * times a power of ten, so labels read $0.003 rather than $0.0028. The
223
+ * tightest top wins, then the fewest steps, so bars keep their height.
224
+ */
225
+ function niceTicks(max) {
226
+ if (!(max > 0)) return [0];
227
+ const magnitude = 10 ** Math.floor(Math.log10(max));
228
+ let best = {
229
+ top: Infinity,
230
+ step: 0,
231
+ count: 0
232
+ };
233
+ for (const scale of [
234
+ 1,
235
+ .1,
236
+ .01
237
+ ]) for (const multiple of [
238
+ 1,
239
+ 2,
240
+ 2.5,
241
+ 5
242
+ ]) {
243
+ const step = multiple * magnitude * scale;
244
+ const count = Math.ceil(max / step - 1e-9);
245
+ const top = count * step;
246
+ if (count < 3 || count > 6) continue;
247
+ if (top < best.top - 1e-12 * top || Math.abs(top - best.top) <= 1e-12 * top && count < best.count) best = {
248
+ top,
249
+ step,
250
+ count
251
+ };
252
+ }
253
+ return Array.from({ length: best.count + 1 }, (_, i) => Number((i * best.step).toPrecision(12)));
254
+ }
220
255
  function StackedBarChart(props) {
221
256
  const { label, series, buckets, maxValue, formatValue, selectedBucketId, onSelectionChange, emptyLabel = "No data yet", unavailableLabel = "Not available", className } = props;
222
257
  const readoutId = useId();
@@ -249,13 +284,16 @@ function StackedBarChart(props) {
249
284
  const formatted = (value) => isValue(value) ? formatValue(value) : unavailableLabel;
250
285
  const summary = (bucket) => `${bucket.label}: total ${formatted(bucket.total)}` + (hasGap(bucket) ? ` — ${unavailableLabel}` : "") + (bucket.segments.length ? ` — ${bucket.segments.map((segment) => `${bySeries.get(segment.seriesId).label} ${formatted(segment.value)}`).join(", ")}` : "") + (bucket.description ? ` — ${bucket.description}` : "");
251
286
  const activeIndex = selectedBucketId === null ? -1 : buckets.findIndex(({ id }) => id === selectedBucketId);
252
- const activeBucket = activeIndex >= 0 ? buckets[activeIndex] : null, marginTop = 12, marginRight = 16, marginLeft = 56;
287
+ const activeBucket = activeIndex >= 0 ? buckets[activeIndex] : null, marginTop = 12, marginRight = 24, marginLeft = 56;
253
288
  const chartHeight = 220;
254
289
  const innerHeight = 176;
255
290
  const barGap = 6;
256
- const barWidth = Math.max(8, Math.min(36, 648 / Math.max(1, buckets.length) - barGap));
291
+ const barWidth = Math.max(8, Math.min(36, 640 / Math.max(1, buckets.length) - barGap));
257
292
  const viewWidth = Math.max(buckets.length * (barWidth + barGap) + marginLeft + marginRight, 400);
258
- const denseLabels = buckets.length > 14;
293
+ const ticks = niceTicks(maxValue);
294
+ const scaleMax = ticks[ticks.length - 1];
295
+ const labelWidth = Math.max(0, ...buckets.map((bucket) => bucket.label.length)) * 6 + 8;
296
+ const labelStride = Math.max(1, Math.ceil(labelWidth / (barWidth + barGap)));
259
297
  if (buckets.length === 0) return /* @__PURE__ */ jsxs("p", {
260
298
  className,
261
299
  children: [
@@ -284,16 +322,12 @@ function StackedBarChart(props) {
284
322
  maxHeight: 300
285
323
  },
286
324
  preserveAspectRatio: "xMinYMin meet",
287
- children: [[
288
- 0,
289
- .25,
290
- .5,
291
- .75,
292
- 1
293
- ].map((fraction) => {
325
+ children: [ticks.map((tick) => {
326
+ const fraction = scaleMax > 0 ? tick / scaleMax : 0;
294
327
  const y = 188 - fraction * innerHeight;
295
328
  return /* @__PURE__ */ jsxs("g", {
296
329
  "aria-hidden": "true",
330
+ "data-tick": "",
297
331
  children: [/* @__PURE__ */ jsx("line", {
298
332
  x1: marginLeft,
299
333
  y1: y,
@@ -310,14 +344,14 @@ function StackedBarChart(props) {
310
344
  fontSize: "11",
311
345
  fill: "currentColor",
312
346
  fontFamily: "inherit",
313
- children: formatValue(maxValue * fraction)
347
+ children: formatValue(tick)
314
348
  })]
315
- }, fraction);
349
+ }, tick);
316
350
  }), buckets.map((bucket, index) => {
317
351
  const x = marginLeft + index * (barWidth + barGap) + barGap / 2;
318
352
  const isActive = index === activeIndex;
319
353
  const gap = hasGap(bucket);
320
- const stackHeight = !gap && maxValue > 0 ? bucket.total / maxValue * innerHeight : 0;
354
+ const stackHeight = !gap && scaleMax > 0 ? bucket.total / scaleMax * innerHeight : 0;
321
355
  let yOffset = 0;
322
356
  const select = () => onSelectionChange(bucket.id);
323
357
  const clear = () => {
@@ -398,7 +432,7 @@ function StackedBarChart(props) {
398
432
  rx: 3
399
433
  }),
400
434
  !gap && bucket.segments.map((segment, segmentIndex) => {
401
- const height = maxValue > 0 ? segment.value / maxValue * innerHeight : 0;
435
+ const height = scaleMax > 0 ? segment.value / scaleMax * innerHeight : 0;
402
436
  const y = 188 - yOffset - height;
403
437
  yOffset += height;
404
438
  return /* @__PURE__ */ jsx("rect", {
@@ -434,16 +468,15 @@ function StackedBarChart(props) {
434
468
  rx: 3,
435
469
  pointerEvents: "none"
436
470
  }),
437
- /* @__PURE__ */ jsx("text", {
471
+ index % labelStride === 0 && /* @__PURE__ */ jsx("text", {
438
472
  x: x + barWidth / 2,
439
473
  y: 204,
440
- textAnchor: denseLabels ? "end" : "middle",
474
+ textAnchor: "middle",
441
475
  fontSize: "10",
442
476
  fill: "currentColor",
443
477
  fontFamily: "inherit",
444
478
  pointerEvents: "none",
445
479
  "aria-hidden": "true",
446
- transform: denseLabels ? `rotate(-40, ${x + barWidth / 2}, 204)` : void 0,
447
480
  children: bucket.label
448
481
  })
449
482
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
5
5
  "type": "module",
6
6
  "license": "MIT",