@niekes/charts-3d 0.5.0 → 0.6.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import * as React from "react";
3
- import { extent, nice, ticks } from "d3-array";
4
- import { axes3D, box, camera3D, cubes3D, gridPlanes3D, normal, points3D, sort } from "d3-3d";
3
+ import { extent, nice, range, ticks } from "d3-array";
4
+ import { axes3D, box, camera3D, cubes3D, gridPlanes3D, normal, points3D, polygons3D, sort } from "d3-3d";
5
5
  import { KeyboardIcon } from "lucide-react";
6
6
  import { clsx } from "clsx";
7
7
  import { twMerge } from "tailwind-merge";
@@ -239,10 +239,9 @@ function keymapOf(keymap, panel) {
239
239
  function ChartContainer3D({ id, config, defaultView = {
240
240
  yaw: Math.PI / 6,
241
241
  tilt: Math.PI / 6
242
- }, width = 640, height = 400, animationDuration, legend = false, renderer: requested = "svg", shortcuts = true, keymap = {}, gizmo = "bottom-left", className, children, "aria-label": ariaLabel, ...props }) {
243
- const uniqueId = React.useId();
244
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
245
- const { scene, renderer, canvas, repaint, hover, hide, leave, tooltip, focus } = useScene(requested, width, height);
242
+ }, width = 640, height = 400, animationDuration, legend = false, renderer: requested = "svg", shortcuts = true, keymap = {}, gizmo = "bottom-left", tooltip = "top-right", className, children, "aria-label": ariaLabel, ...props }) {
243
+ const chartId = chartIdOf(id, React.useId());
244
+ const { scene, renderer, canvas, repaint, hover, hide, leave, tip, focus } = useScene(requested, width, height, tooltip);
246
245
  const keys = keymapOf(keymap, shortcuts);
247
246
  const { view, handlers } = useOrbit(defaultView, width, keys, hide, hover);
248
247
  const camera = useCamera(view, width, height);
@@ -296,11 +295,7 @@ function ChartContainer3D({ id, config, defaultView = {
296
295
  camera,
297
296
  corner: gizmo
298
297
  }),
299
- /* @__PURE__ */ jsx(ChartTooltip3D, {
300
- tooltip,
301
- width,
302
- height
303
- })
298
+ /* @__PURE__ */ jsx(ChartTooltip3D, { tip })
304
299
  ]
305
300
  }),
306
301
  /* @__PURE__ */ jsx(ChartShortcuts3D, { ...keys }),
@@ -308,6 +303,10 @@ function ChartContainer3D({ id, config, defaultView = {
308
303
  ]
309
304
  });
310
305
  }
306
+ /** The id the colors of a chart are scoped to: its own `id`, or the one React made up. */
307
+ function chartIdOf(id, uniqueId) {
308
+ return `chart-${id || uniqueId.replace(/:/g, "")}`;
309
+ }
311
310
  /**
312
311
  * What the svg says about its keymap: a label that names the drag and the
313
312
  * help key if they are on, the keys, the focus only if a key does something
@@ -332,13 +331,13 @@ function keymapProps(keys, label) {
332
331
  * The marks of the chart and what reads them: the renderer that draws them,
333
332
  * the canvas it paints them on and the hover that shows their tooltips.
334
333
  */
335
- function useScene(requested, width, height) {
334
+ function useScene(requested, width, height, placement) {
336
335
  const scene = React.useRef([]);
337
- const [tooltip, setTooltip] = React.useState(null);
336
+ const tip = useTip(placement);
338
337
  const focus = useFocus();
339
338
  const renderer = useRenderer(requested);
340
339
  const { canvas, repaint } = useCanvas(scene, width, height, renderer);
341
- const { hover, hide, leave } = useHover(scene, width, height, setTooltip, focus);
340
+ const { hover, hide, leave } = useHover(scene, width, height, tip, focus);
342
341
  return {
343
342
  scene,
344
343
  renderer,
@@ -347,10 +346,65 @@ function useScene(requested, width, height) {
347
346
  hover,
348
347
  hide,
349
348
  leave,
350
- tooltip,
349
+ tip,
351
350
  focus
352
351
  };
353
352
  }
353
+ const TIP_GAP = 12;
354
+ const shift = (isBefore) => isBefore ? `calc(-100% - ${TIP_GAP}px)` : `${TIP_GAP}px`;
355
+ /**
356
+ * Whether a tooltip of `size` goes before the pointer at `at`, on an axis of
357
+ * `length`: as `preferred`, unless it would leave the chart there and the
358
+ * other side has more room.
359
+ */
360
+ function before(at, size, length, preferred) {
361
+ const room = preferred ? at : length - at;
362
+ return room < size + TIP_GAP && length - room > room ? !preferred : preferred;
363
+ }
364
+ /**
365
+ * The tooltip of the hovered mark and where it sits: beside the pointer, in
366
+ * the pixels of the chart, at `placement`. `follow` moves it with the pointer
367
+ * straight in the DOM, so a move within a mark renders nothing; `place` puts
368
+ * it there again once it shows, or its content changes its size.
369
+ */
370
+ function useTip(placement) {
371
+ const [tooltip, set] = React.useState(null);
372
+ const ref = React.useRef(null);
373
+ const pointer = React.useRef({
374
+ x: 0,
375
+ y: 0,
376
+ width: 0,
377
+ height: 0
378
+ });
379
+ const place = React.useCallback(() => {
380
+ const el = ref.current;
381
+ if (!el) return;
382
+ const shows = !el.dataset.placement;
383
+ if (shows) el.style.transition = "none";
384
+ const { x, y, width, height } = pointer.current;
385
+ const top = before(y, el.offsetHeight, height, placement.startsWith("top"));
386
+ const left = before(x, el.offsetWidth, width, placement.endsWith("left"));
387
+ el.style.left = `${x}px`;
388
+ el.style.top = `${y}px`;
389
+ el.style.translate = `${shift(left)} ${shift(top)}`;
390
+ el.dataset.placement = `${top ? "top" : "bottom"}-${left ? "left" : "right"}`;
391
+ if (shows) {
392
+ el.getBoundingClientRect();
393
+ el.style.transition = "";
394
+ }
395
+ }, [placement]);
396
+ const follow = (at) => {
397
+ pointer.current = at;
398
+ place();
399
+ };
400
+ return {
401
+ tooltip,
402
+ set,
403
+ ref,
404
+ place,
405
+ follow
406
+ };
407
+ }
354
408
  /** The camera of a view. The shapes of the chart hang on it, so a new view re-projects them all. */
355
409
  function useCamera(view, width, height) {
356
410
  return React.useMemo(() => camera3D().origin({
@@ -740,26 +794,33 @@ function useWheelZoom(jump, on) {
740
794
  * drops the tooltip and keeps the focus, so a flow does not flash away and
741
795
  * can be followed from every side.
742
796
  */
743
- function useHover(scene, width, height, setTooltip, focus) {
797
+ function useHover(scene, width, height, tip, focus) {
744
798
  const hovered = React.useRef(void 0);
745
799
  const show = (mark) => {
746
800
  focus.set(mark?.groups);
747
801
  if (mark?.key === hovered.current?.key) return;
748
802
  hovered.current = mark;
749
- setTooltip(mark?.tooltip ?? null);
803
+ tip.set(mark?.tooltip ?? null);
750
804
  };
751
805
  return {
752
806
  hover: (e) => {
753
807
  const rect = e.currentTarget.getBoundingClientRect();
754
- const at = {
755
- x: (e.clientX - rect.left) / rect.width * width,
756
- y: (e.clientY - rect.top) / rect.height * height
757
- };
758
- show(hit(scene.current, at));
808
+ const x = e.clientX - rect.left;
809
+ const y = e.clientY - rect.top;
810
+ tip.follow({
811
+ x,
812
+ y,
813
+ width: rect.width,
814
+ height: rect.height
815
+ });
816
+ show(hit(scene.current, {
817
+ x: x / rect.width * width,
818
+ y: y / rect.height * height
819
+ }));
759
820
  },
760
821
  hide: () => {
761
822
  hovered.current = void 0;
762
- setTooltip(null);
823
+ tip.set(null);
763
824
  },
764
825
  leave: () => show(void 0)
765
826
  };
@@ -919,7 +980,7 @@ function colorBytes(color, pixel, cache) {
919
980
  }
920
981
  /**
921
982
  * Paints `marks` on a webgl `canvas`, from the view box of `width`×`height` to
922
- * the pixels of the canvas: a fan per polygon, a fan of 12 segments per
983
+ * the pixels of the canvas: a polygon ear by ear, a fan of 12 segments per
923
984
  * circle, in paint order and without a depth test, the color of each as bytes.
924
985
  */
925
986
  function paintGL(canvas, marks, width, height, color) {
@@ -961,23 +1022,47 @@ const ringOf = (c, r) => Array.from({ length: 12 }, (_, i) => ({
961
1022
  x: c.x + r * Math.cos(i / 12 * 2 * Math.PI),
962
1023
  y: c.y + r * Math.sin(i / 12 * 2 * Math.PI)
963
1024
  }));
964
- /** The triangles of `mark` as view-box points: a fan per polygon, a fan of 12 segments per circle. */
1025
+ /** The triangles of `mark` as view-box points: its polygon ear by ear, a fan of 12 segments per circle. */
965
1026
  function trianglesOf(mark) {
966
1027
  const { points, r } = mark;
967
- if (r !== void 0) {
968
- const ring = ringOf(points[0], r);
969
- return ring.flatMap((p, i) => [
970
- points[0],
971
- p,
972
- ring[(i + 1) % 12]
973
- ]);
974
- }
975
- return points.slice(1, -1).flatMap((p, i) => [
1028
+ if (r === void 0) return earsOf(points);
1029
+ const ring = ringOf(points[0], r);
1030
+ return ring.flatMap((p, i) => [
976
1031
  points[0],
977
1032
  p,
978
- points[i + 2]
1033
+ ring[(i + 1) % 12]
979
1034
  ]);
980
1035
  }
1036
+ /** Twice the signed area of the triangle `a`, `b`, `c`: its sign tells which way it turns, 0 on a line. */
1037
+ const turn = (a, b, c) => (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
1038
+ /**
1039
+ * The triangles of a polygon, cut off ear by ear, so a concave one - the
1040
+ * front of a ribbon - fills as on the canvas, where a fan would spill over
1041
+ * its dents. An ear is a corner that turns the way of the polygon, or not at
1042
+ * all, with no other corner inside it; a simple polygon always has one. One
1043
+ * that crosses itself may have none, and then loses its first corner.
1044
+ */
1045
+ function earsOf(points) {
1046
+ const left = [...points];
1047
+ const way = Math.sign(left.reduce((sum, p, i) => sum + turn(left[0], p, left[i + 1] ?? p), 0));
1048
+ const triangles = [];
1049
+ while (left.length > 3) {
1050
+ const corner = (i) => [
1051
+ left.at(i - 1),
1052
+ left[i],
1053
+ left[(i + 1) % left.length]
1054
+ ];
1055
+ const ear = left.findIndex((_, i) => {
1056
+ const [a, b, c] = corner(i);
1057
+ if (way * turn(a, b, c) < 0) return false;
1058
+ return !left.some((p) => way * turn(a, b, p) > 0 && way * turn(b, c, p) > 0 && way * turn(c, a, p) > 0);
1059
+ });
1060
+ const i = Math.max(0, ear);
1061
+ triangles.push(...corner(i));
1062
+ left.splice(i, 1);
1063
+ }
1064
+ return [...triangles, ...left];
1065
+ }
981
1066
  /** The outline of `mark` as view-box points: its polygon, or the ring of its circle. */
982
1067
  const outlineOf = (mark) => mark.r === void 0 ? mark.points : ringOf(mark.points[0], mark.r);
983
1068
  const programs = /* @__PURE__ */ new WeakMap();
@@ -1170,15 +1255,14 @@ ${colorConfig.map(([key, item]) => {
1170
1255
  }
1171
1256
  `)].join("\n") } });
1172
1257
  }
1173
- function ChartTooltip3D({ tooltip, width, height }) {
1258
+ function ChartTooltip3D({ tip }) {
1259
+ const { tooltip, ref, place } = tip;
1260
+ React.useLayoutEffect(place, [tooltip, place]);
1174
1261
  if (!tooltip) return null;
1175
1262
  return /* @__PURE__ */ jsxs("div", {
1263
+ ref,
1176
1264
  "data-slot": "chart-3d-tooltip",
1177
- className: "pointer-events-none absolute z-10 grid min-w-32 -translate-x-1/2 -translate-y-[calc(100%+0.5rem)] gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
1178
- style: {
1179
- left: `${tooltip.at.x / width * 100}%`,
1180
- top: `${tooltip.at.y / height * 100}%`
1181
- },
1265
+ className: "pointer-events-none absolute z-10 grid w-max min-w-32 gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl motion-safe:transition-[translate]",
1182
1266
  children: [tooltip.title && /* @__PURE__ */ jsx("div", {
1183
1267
  className: "font-medium",
1184
1268
  children: tooltip.title
@@ -1706,18 +1790,36 @@ function faded(color, opacity) {
1706
1790
  const percent = Math.round(opacity * 100);
1707
1791
  return percent >= 100 ? color : `color-mix(in oklab, ${color} ${percent}%, transparent)`;
1708
1792
  }
1709
- /** The mean of the projected points, where a tooltip points at. */
1710
- function center(points) {
1711
- const sum = points.reduce((s, p) => ({
1712
- x: s.x + p.projected.x,
1713
- y: s.y + p.projected.y
1714
- }), {
1715
- x: 0,
1716
- y: 0
1717
- });
1793
+ /**
1794
+ * The entries of `data` and their series, for a chart that lays the entries
1795
+ * out along x and the series along z, as the bar and the area chart do. The
1796
+ * name of every entry comes from its `index` field, the series are `keys` or
1797
+ * the keys of the config, and the `target` of every cell is keyed by the name
1798
+ * of its entry and its series, so it follows its entry when the data
1799
+ * reorders; a value that is not a number stands as 0. Each glides by name:
1800
+ * `values` from 0, `rows` - the place of every entry - to where it stands, and
1801
+ * `presence` from 0 to 1 as an entry comes and back as it goes, the gone ones
1802
+ * still among them. `labels` name the ticks: the entry at a tick of x, through
1803
+ * `format.x` - a tick of a gone entry fades out unnamed - and the series at a
1804
+ * tick of z.
1805
+ */
1806
+ function useSeries({ data, index, keys, format = {} }) {
1807
+ const { config } = useChart3D();
1808
+ const series = keys ?? Object.keys(config);
1809
+ const names = data.map((d) => String(d[index] ?? ""));
1810
+ const target = Object.fromEntries(data.flatMap((d, i) => series.map((key) => [`${names[i]}-${key}`, Number(d[key]) || 0])));
1811
+ const labels = {
1812
+ x: (i) => (format.x ?? String)(names[i] ?? ""),
1813
+ y: format.y,
1814
+ z: (j) => config[series[j]]?.label ?? series[j]
1815
+ };
1718
1816
  return {
1719
- x: sum.x / points.length,
1720
- y: sum.y / points.length
1817
+ series,
1818
+ target,
1819
+ values: useTween(target, 0),
1820
+ rows: useTween(Object.fromEntries(names.map((name, i) => [name, i]))),
1821
+ presence: useTween(Object.fromEntries(names.map((name) => [name, 1])), 0),
1822
+ labels
1721
1823
  };
1722
1824
  }
1723
1825
  /** The marks of a cube: its faces that look at the camera, shaded in `color`, each with `tooltip` on hover. */
@@ -1729,20 +1831,214 @@ function cubeMarks(id, faces, color, tooltip) {
1729
1831
  tooltip
1730
1832
  }));
1731
1833
  }
1834
+ // registry/new-york/ui/area-chart-3d.tsx
1835
+ const RIBBON = 2 / 3;
1836
+ /**
1837
+ * Areas pushed back into ribbons: the entries of `data` along x, one ribbon
1838
+ * per series along z, the area under its line reaching down to the floor of
1839
+ * the box. The series are the keys of the chart config unless `keys` names
1840
+ * them.
1841
+ */
1842
+ function AreaChart3D({ data, index, keys, format = {}, titles }) {
1843
+ const { config, camera } = useChart3D();
1844
+ const { series, target, values, rows, presence, labels } = useSeries({
1845
+ data,
1846
+ index,
1847
+ keys,
1848
+ format
1849
+ });
1850
+ const spec = useTweenAxes(areaAxes(data.length, series.length, Object.values(target)));
1851
+ const depth = useTween({ depth: Math.min(1, Math.max(.25, series.length / 6)) }).depth;
1852
+ const world = worldBox(spec, depth);
1853
+ const polygons = intoWorld(polygons3D().camera(camera), world);
1854
+ const ends = [spec.x.domain[0] + .5, spec.x.domain[1] - .5];
1855
+ const toward = Math.sign(camera.viewDirection().x);
1856
+ const marks = series.flatMap((key, j) => {
1857
+ const line = linePoints(rows, values, presence, key, ends);
1858
+ if (!line.length) return [];
1859
+ const z = [j - RIBBON / 2, j + RIBBON / 2];
1860
+ return [{
1861
+ key,
1862
+ line,
1863
+ faces: ribbonFaces(polygons, line, spec.y.domain[0], z)
1864
+ }];
1865
+ }).sort((a, b) => sort(a.faces[4], b.faces[4])).flatMap(({ key, line, faces }) => ribbonMarks(key, painted(faces, toward), line, config[key]?.label ?? key, format.x ?? String));
1866
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Axes3D, {
1867
+ spec,
1868
+ world,
1869
+ titles,
1870
+ format: labels
1871
+ }), /* @__PURE__ */ jsx(Marks3D, { marks })] });
1872
+ }
1873
+ /**
1874
+ * The points of the line of the series `key`, in order along x: every entry,
1875
+ * the ones still gliding away among them, at its gliding row and value. An
1876
+ * entry that comes or goes unfolds from the point before it - the one after
1877
+ * it, at the start of the line - and folds back into it, as far as its
1878
+ * `presence`, so the ribbon opens and closes without a step. Its value comes
1879
+ * from nothing as its presence grows - the two glide in step - so the part of
1880
+ * its neighbour it has not yet left is added: the point heads straight from
1881
+ * the neighbour to its own, never down towards the floor. Every
1882
+ * point stays within the gliding `ends` of the x axis, so a line with no
1883
+ * neighbour to unfold from - one that was empty - grows inside the box
1884
+ * instead of standing beyond its walls. A point only one of the tweens still
1885
+ * knows is on its way out and stays unseen.
1886
+ */
1887
+ function linePoints(rows, values, presence, key, [lo, hi]) {
1888
+ const own = Object.entries(rows).sort(([, a], [, b]) => a - b).flatMap(([name, x]) => {
1889
+ const value = values[`${name}-${key}`];
1890
+ return value === void 0 ? [] : [{
1891
+ name,
1892
+ x,
1893
+ value,
1894
+ seen: presence[name]
1895
+ }];
1896
+ });
1897
+ let anchor = own.find((p) => p.seen === 1);
1898
+ return own.map((p) => {
1899
+ if (p.seen === 1) anchor = p;
1900
+ const { x } = anchor ?? p;
1901
+ return {
1902
+ name: p.name,
1903
+ x: Math.min(hi, Math.max(lo, x + p.seen * (p.x - x))),
1904
+ value: p.value + (1 - p.seen) * (anchor?.value ?? 0)
1905
+ };
1906
+ }).sort((a, b) => a.x - b.x);
1907
+ }
1908
+ /**
1909
+ * The faces of the ribbon of `line`, projected: the area under the line,
1910
+ * down to `floor`, pushed back from `z[0]` to `z[1]`. Each is wound
1911
+ * counter-clockwise as seen from outside, so `ccw` tells whether it faces the
1912
+ * camera. The first five lie on its bounding box - the front and the back,
1913
+ * one polygon each however many points the line has, the two ends and the
1914
+ * bottom - the rest are its top, one quad per segment of the line.
1915
+ */
1916
+ function ribbonFaces(polygons, line, floor, [back, front]) {
1917
+ const on = (p, z) => ({
1918
+ x: p.x,
1919
+ y: p.value,
1920
+ z
1921
+ });
1922
+ const under = (p, z) => ({
1923
+ x: p.x,
1924
+ y: floor,
1925
+ z
1926
+ });
1927
+ const face = (name, points, segment = -1) => Object.assign(points, {
1928
+ face: name,
1929
+ segment
1930
+ });
1931
+ const [a, o] = [line[0], line[line.length - 1]];
1932
+ return polygons.data([
1933
+ face("front", [
1934
+ on(a, front),
1935
+ under(a, front),
1936
+ under(o, front),
1937
+ ...line.slice(1).reverse().map((p) => on(p, front))
1938
+ ]),
1939
+ face("back", [
1940
+ ...line.map((p) => on(p, back)),
1941
+ under(o, back),
1942
+ under(a, back)
1943
+ ]),
1944
+ face("left", [
1945
+ on(a, back),
1946
+ under(a, back),
1947
+ under(a, front),
1948
+ on(a, front)
1949
+ ]),
1950
+ face("right", [
1951
+ on(o, front),
1952
+ under(o, front),
1953
+ under(o, back),
1954
+ on(o, back)
1955
+ ]),
1956
+ face("bottom", [
1957
+ under(a, front),
1958
+ under(a, back),
1959
+ under(o, back),
1960
+ under(o, front)
1961
+ ]),
1962
+ ...line.slice(1).map((p, i) => face("top", [
1963
+ on(line[i], front),
1964
+ on(p, front),
1965
+ on(p, back),
1966
+ on(line[i], back)
1967
+ ], i))
1968
+ ]);
1969
+ }
1970
+ /**
1971
+ * The faces of a ribbon that face the camera, in paint order. Every segment
1972
+ * of the top is a slab, and the planes x = const separate them: the top is
1973
+ * painted from the end of the ribbon away from the camera, which `toward`,
1974
+ * the sign of x in the view direction, names. The faces of the bounding box
1975
+ * come last: the ribbon lies wholly behind each of them.
1976
+ */
1977
+ function painted(faces, toward) {
1978
+ const shown = faces.filter((f) => f.ccw);
1979
+ return [...shown.filter((f) => f.face === "top").sort((a, b) => toward * (a.segment - b.segment)), ...shown.filter((f) => f.face !== "top")];
1980
+ }
1981
+ /**
1982
+ * The marks of one ribbon, shaded in the color of its series. Each face is a
1983
+ * group of its own, so hovering highlights the face under the pointer alone.
1984
+ * A segment of the top is keyed by the entry it starts at, so it keeps its
1985
+ * mark when entries come or go before it. Every face names the series; a
1986
+ * segment of the top tells the values at its two ends.
1987
+ */
1988
+ function ribbonMarks(key, faces, line, label, name) {
1989
+ const color = `var(--color-${key})`;
1990
+ const spans = (f) => f.face === "top" ? [line[f.segment], line[f.segment + 1]] : [];
1991
+ return faces.map((f) => {
1992
+ const id = f.face === "top" ? `${key}-top-${line[f.segment].name}` : `${key}-${f.face}`;
1993
+ return {
1994
+ key: id,
1995
+ points: f.map((p) => p.projected),
1996
+ fill: shade(f, color),
1997
+ groups: [id],
1998
+ tooltip: {
1999
+ title: label,
2000
+ items: spans(f).map((p) => ({
2001
+ label: name(p.name),
2002
+ value: p.value,
2003
+ color
2004
+ }))
2005
+ }
2006
+ };
2007
+ });
2008
+ }
2009
+ /**
2010
+ * The axes of an area chart: the entries along x, with half a row of room
2011
+ * between the first and the last and the walls, a row per series along z.
2012
+ * A line has many entries, so x ticks about ten of them, not every one.
2013
+ */
2014
+ function areaAxes(entries, series, values) {
2015
+ const every = entries ? ticks(0, entries - 1, 10).filter(Number.isInteger) : [];
2016
+ return {
2017
+ x: pad({
2018
+ domain: [0, Math.max(0, entries - 1)],
2019
+ ticks: every
2020
+ }, 1),
2021
+ y: axis(Math.min(0, ...values), Math.max(0, ...values)),
2022
+ z: pad({
2023
+ domain: [0, Math.max(0, series - 1)],
2024
+ ticks: range(series)
2025
+ }, 1)
2026
+ };
2027
+ }
1732
2028
  // registry/new-york/ui/bar-chart-3d.tsx
1733
2029
  /**
1734
2030
  * Bars on a floor: one row per entry of `data` along x, one per series
1735
2031
  * along z, the value as the height. The series are the keys of the chart
1736
2032
  * config unless `keys` names them.
1737
2033
  */
1738
- function BarChart3D({ data, index, keys, barSize = .6, format = {}, titles }) {
2034
+ function BarChart3D({ data, index, keys, barSize = .6, format, titles }) {
1739
2035
  const { config, camera } = useChart3D();
1740
- const series = keys ?? Object.keys(config);
1741
- const names = data.map((d) => String(d[index] ?? ""));
1742
- const target = barValues(names, data, series);
1743
- const values = useTween(target, 0);
1744
- const rows = useTween(Object.fromEntries(names.map((name, i) => [name, i])));
1745
- const opacity = useTween(Object.fromEntries(names.map((name) => [name, 1])), 0);
2036
+ const { series, target, values, rows, presence, labels } = useSeries({
2037
+ data,
2038
+ index,
2039
+ keys,
2040
+ format
2041
+ });
1746
2042
  const cells = barCells(rows, series, values);
1747
2043
  const box = barBox(data.length, series.length, Object.values(target));
1748
2044
  const spec = useTweenAxes(box.spec);
@@ -1753,11 +2049,7 @@ function BarChart3D({ data, index, keys, barSize = .6, format = {}, titles }) {
1753
2049
  spec,
1754
2050
  world,
1755
2051
  titles,
1756
- format: {
1757
- x: (i) => (format.x ?? String)(String(data[i]?.[index] ?? "")),
1758
- y: format.y,
1759
- z: (j) => config[series[j]]?.label ?? series[j]
1760
- }
2052
+ format: labels
1761
2053
  }), /* @__PURE__ */ jsx(Marks3D, { marks: bars.flatMap((bar) => barMarks({
1762
2054
  id: `${bar.name}-${bar.key}`,
1763
2055
  faces: bar.faces,
@@ -1765,7 +2057,7 @@ function BarChart3D({ data, index, keys, barSize = .6, format = {}, titles }) {
1765
2057
  title: bar.name,
1766
2058
  label: config[bar.key]?.label ?? bar.key,
1767
2059
  value: bar.value,
1768
- opacity: opacity[bar.name]
2060
+ opacity: presence[bar.name]
1769
2061
  })) })] });
1770
2062
  }
1771
2063
  /** The cubes of the cells, projected by `camera` and painted back to front by their footprint, as the bars stand on one floor. */
@@ -1794,15 +2086,6 @@ function barCells(rows, series, values) {
1794
2086
  }));
1795
2087
  }
1796
2088
  /**
1797
- * The value of every cell of the chart, keyed as its mark: the name of its
1798
- * entry along x and the series `key` along z, standing as 0 where the entry
1799
- * holds none. Keyed by name, not place, so a bar follows its entry when the
1800
- * data reorders.
1801
- */
1802
- function barValues(names, data, series) {
1803
- return Object.fromEntries(data.flatMap((d, i) => series.map((key) => [`${names[i]}-${key}`, Number(d[key]) || 0])));
1804
- }
1805
- /**
1806
2089
  * The box of a bar chart: a row per entry along x, a row per series along z,
1807
2090
  * with half a cell of room between the outer rows and the walls.
1808
2091
  */
@@ -1851,7 +2134,6 @@ function barCorners(world, x, z, value, size) {
1851
2134
  /** The marks of one bar: the faces that look at the camera, shaded and faded to `opacity`, each with its entry and value on hover. Its group is the bar alone, so hovering highlights one bar, not its series. */
1852
2135
  function barMarks({ id, faces, color, title, label, value, opacity }) {
1853
2136
  return cubeMarks(id, faces, color, {
1854
- at: center(faces.find((face) => face.face === "top")),
1855
2137
  title,
1856
2138
  items: [{
1857
2139
  label,
@@ -1889,7 +2171,6 @@ function ScatterChart3D({ data, x, y, z, id, series, radius = 4, format, titles
1889
2171
  groups: [p.name],
1890
2172
  stroke: faded("var(--background)", opacity[p.name]),
1891
2173
  tooltip: {
1892
- at: p.projected,
1893
2174
  title: series ? config[key(p.name)]?.label ?? key(p.name) : void 0,
1894
2175
  items: [
1895
2176
  "x",
@@ -2055,13 +2336,10 @@ function quadMark(quad, fade, ramp, title) {
2055
2336
  points: quad.map((p) => p.projected),
2056
2337
  fill: seen < 1 ? `color-mix(in srgb, ${shaded} ${Math.round(seen * 100)}%, transparent)` : shaded,
2057
2338
  groups: [quad.plane],
2058
- tooltip: {
2059
- at: center(quad),
2060
- items: [{
2061
- label: title ?? "value",
2062
- value: height
2063
- }]
2064
- }
2339
+ tooltip: { items: [{
2340
+ label: title ?? "value",
2341
+ value: height
2342
+ }] }
2065
2343
  };
2066
2344
  }
2067
2345
  /** The height of every cell, keyed by its row and column, gliding into new data. */
@@ -2083,6 +2361,6 @@ function gridHeights(tweened, floor) {
2083
2361
  }
2084
2362
  return Array.from({ length: rows }, (_, r) => Array.from({ length: columns }, (_, c) => tweened[`${r}-${c}`] ?? floor));
2085
2363
  }
2086
- export { Axes3D, BarChart3D, ChartContainer3D, ChartLegend3D, Labels3D, Marks3D, ScatterChart3D, SurfaceChart3D, axis, center, cubeMarks, faded, intoWorld, linear, pad, shade, useChart3D, useTween, useTweenAxes, worldBox };
2364
+ export { AreaChart3D, Axes3D, BarChart3D, ChartContainer3D, ChartLegend3D, Labels3D, Marks3D, ScatterChart3D, SurfaceChart3D, axis, cubeMarks, faded, intoWorld, linear, pad, shade, useChart3D, useSeries, useTween, useTweenAxes, worldBox };
2087
2365
 
2088
2366
  //# sourceMappingURL=index.js.map