@awesomephant/reactcake 1.1.2 → 1.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # [1.3.0](https://github.com/awesomephant/reactcake/compare/v1.2.0...v1.3.0) (2026-11-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * iterate LayoutProps ([e6ea26a](https://github.com/awesomephant/reactcake/commit/e6ea26ad9f583846f20925ada21b5b7504f83d1d))
package/Readme.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A headless chart framework for React inspired by [Layercake](https://layercake.graphics/). See [documentation](https://reactcake.maxkohler.com/) for usage and motivation.
4
4
 
5
- ![NPM Version](https://img.shields.io/npm/v/%40awesomephant%2Freactcake) [![release](https://github.com/awesomephant/reactcake/actions/workflows/release.yml/badge.svg)](https://github.com/awesomephant/reactcake/actions/workflows/release.yml)
5
+ [![NPM Version](https://img.shields.io/npm/v/%40awesomephant%2Freactcake)](https://www.npmjs.com/package/@awesomephant/reactcake) [![release](https://github.com/awesomephant/reactcake/actions/workflows/release.yml/badge.svg)](https://github.com/awesomephant/reactcake/actions/workflows/release.yml)
6
6
 
7
7
  ## Contributing
8
8
 
@@ -3,18 +3,31 @@ import { useReactCake } from "../ReactCake";
3
3
  import { LayoutProps } from "../types";
4
4
 
5
5
  interface HtmlProps extends LayoutProps {
6
+ /**
7
+ * Sets the HTML `role` attribute
8
+ */
9
+ role?: string;
6
10
  children?: ReactNode;
7
11
  }
8
12
 
9
- const Html = ({ overflow = "visible", label, labelledBy, describedBy, children }: HtmlProps) => {
13
+ const Html = ({
14
+ overflow = "visible",
15
+ zIndex,
16
+ label,
17
+ labelledBy,
18
+ describedBy,
19
+ children,
20
+ role = "figure",
21
+ }: HtmlProps) => {
10
22
  const { padding } = useReactCake();
11
23
 
12
24
  return (
13
25
  <div
14
26
  className="reactcake-html"
15
- aria-label={labelledBy}
27
+ aria-label={label}
28
+ aria-labelledby={labelledBy}
16
29
  aria-describedby={describedBy}
17
- role={label ? "figure" : "none"}
30
+ role={role}
18
31
  style={{
19
32
  position: "absolute",
20
33
  top: `${padding.top}px`,
@@ -22,6 +35,7 @@ const Html = ({ overflow = "visible", label, labelledBy, describedBy, children }
22
35
  right: `${padding.right}px`,
23
36
  bottom: `${padding.bottom}px`,
24
37
  overflow,
38
+ zIndex,
25
39
  }}
26
40
  >
27
41
  {children}
@@ -29,4 +43,4 @@ const Html = ({ overflow = "visible", label, labelledBy, describedBy, children }
29
43
  );
30
44
  };
31
45
 
32
- export default Html;
46
+ export { Html, type HtmlProps };
File without changes
@@ -2,26 +2,57 @@ import { createContext, useContext, type CSSProperties, type ReactNode } from "r
2
2
  import { extent } from "d3-array";
3
3
 
4
4
  import { useDimensions } from "../hooks/useDimensions";
5
- import type { DatumValue, Padding, ScaleLike, ContextValue } from "../types";
5
+ import type { ScaleLike, ContextValue, DomainLike, Accessor } from "../types";
6
6
 
7
7
  const ReactCakeContext = createContext<ContextValue>(null);
8
8
 
9
+ type RangeLike = [number, number];
10
+
9
11
  interface ReactCakeProps {
10
- x?: string | ((d: unknown, i: number) => Datum);
11
- x2?: string | ((d: unknown, i: number) => Datum);
12
- y?: string | ((d: unknown, i: number) => Datum);
13
- data?: unknown[];
14
- flatData?: unknown[];
15
- padding?: Padding;
12
+ data: object[];
13
+ /**
14
+ * The key in each row of data that corresponds to the x-field
15
+ */
16
+ x?: Accessor;
17
+ x2?: Accessor;
18
+ y?: Accessor;
19
+ z?: Accessor;
20
+ flatData?: object[];
21
+ /**
22
+ * Sets the chart area padding in pixels following the {@link https://gist.github.com/mbostock/3019563 | D3 margin convention}. All values are optional.
23
+ */
24
+ padding?: {
25
+ top?: number;
26
+ left?: number;
27
+ right?: number;
28
+ bottom?: number;
29
+ };
16
30
  xScale?: ScaleLike;
17
- yDomain?: [DatumValue | null, DatumValue | null];
31
+ xRange?: RangeLike;
32
+ x2Range?: RangeLike;
33
+ xDomain?: DomainLike;
34
+ x2Domain?: DomainLike;
35
+ x2Scale?: ScaleLike;
18
36
  yScale?: ScaleLike;
37
+ yRange?: RangeLike;
38
+ yDomain?: DomainLike;
19
39
  zScale?: ScaleLike;
40
+ zRange?: RangeLike;
41
+ zDomain?: DomainLike;
42
+ /**
43
+ * CSS property/value pairs applied to the container element.
44
+ */
20
45
  style?: CSSProperties;
46
+ /**
47
+ * Sets ranges to `[0, 100]` for SSR
48
+ */
21
49
  percentRange?: boolean;
22
50
  children?: ReactNode;
23
51
  }
24
52
 
53
+ /**
54
+ * Top-level container element for your chart.
55
+ */
25
56
  const ReactCake = ({
26
57
  data = [],
27
58
  flatData,
@@ -50,7 +81,7 @@ const ReactCake = ({
50
81
  const _x = typeof x === "string" ? (d) => d[x] : x;
51
82
  const _x2 = typeof x2 === "string" ? (d) => d[x2] : x2;
52
83
  const _y = typeof y === "string" ? (d) => d[y] : y;
53
- const _z = typeof y === "string" ? (d) => d[z] : z;
84
+ const _z = typeof z === "string" ? (d) => d[z] : z;
54
85
 
55
86
  const xIsBandscale = xScale && typeof xScale.bandwidth === "function";
56
87
  const yIsBandscale = yScale && typeof yScale.bandwidth === "function";
@@ -71,10 +102,10 @@ const ReactCake = ({
71
102
  ? Array.from(new Set(data.map((d) => _z(d))))
72
103
  : extent(flatData || data, _z);
73
104
 
74
- const _xScale = xScale?.range([0, width]).domain(xDomain || extentX);
105
+ const _xScale = xScale?.range(xRange || [0, width]).domain(xDomain || extentX);
75
106
 
76
107
  const _yScale = yScale
77
- ?.range([0, height])
108
+ ?.range(yRange || [0, height])
78
109
  .domain([
79
110
  yDomain[0] !== null ? yDomain[0] : extentY[0],
80
111
  yDomain[1] !== null ? yDomain[1] : extentY[1],
@@ -123,4 +154,4 @@ function useReactCake() {
123
154
  return context;
124
155
  }
125
156
 
126
- export { ReactCake, useReactCake };
157
+ export { ReactCake, useReactCake, type ReactCakeProps };
@@ -2,7 +2,10 @@ import { type ReactNode } from "react";
2
2
  import { useReactCake } from "../ReactCake";
3
3
  import { LayoutProps } from "../types";
4
4
 
5
- interface SvgProps extends LayoutProps {
5
+ interface ScaledSvgProps extends LayoutProps {
6
+ /**
7
+ * Sets the contents of the <title> tag
8
+ */
6
9
  title?: string;
7
10
  children?: ReactNode;
8
11
  }
@@ -14,7 +17,8 @@ const ScaledSvg = ({
14
17
  describedBy,
15
18
  title,
16
19
  overflow = "visible",
17
- }: SvgProps) => {
20
+ zIndex,
21
+ }: ScaledSvgProps) => {
18
22
  const { padding } = useReactCake();
19
23
  return (
20
24
  <>
@@ -33,6 +37,7 @@ const ScaledSvg = ({
33
37
  top: `${padding.top}px`,
34
38
  left: `${padding.left}px`,
35
39
  overflow: overflow,
40
+ zIndex,
36
41
  }}
37
42
  >
38
43
  {title && <title>{title}</title>}
@@ -42,4 +47,4 @@ const ScaledSvg = ({
42
47
  );
43
48
  };
44
49
 
45
- export default ScaledSvg;
50
+ export { ScaledSvg, type ScaledSvgProps };
package/lib/Svg/index.tsx CHANGED
@@ -3,11 +3,14 @@ import { useReactCake } from "../ReactCake";
3
3
  import { LayoutProps } from "../types";
4
4
 
5
5
  interface SvgProps extends LayoutProps {
6
+ /**
7
+ * Sets the contents of the `<title>` tag
8
+ */
6
9
  title?: string;
7
10
  children?: ReactNode;
8
11
  }
9
12
 
10
- const Svg = ({ children, title, overflow = "hidden" }: SvgProps) => {
13
+ const Svg = ({ children, title, overflow = "hidden", zIndex }: SvgProps) => {
11
14
  const { padding } = useReactCake();
12
15
  return (
13
16
  <svg
@@ -23,6 +26,7 @@ const Svg = ({ children, title, overflow = "hidden" }: SvgProps) => {
23
26
  bottom: 0,
24
27
  right: 0,
25
28
  overflow: overflow,
29
+ zIndex,
26
30
  }}
27
31
  >
28
32
  {title && <title>{title}</title>}
@@ -31,4 +35,4 @@ const Svg = ({ children, title, overflow = "hidden" }: SvgProps) => {
31
35
  );
32
36
  };
33
37
 
34
- export default Svg;
38
+ export { Svg, type SvgProps };
package/lib/index.ts CHANGED
@@ -1,6 +1,18 @@
1
- import Html from "./Html/index.tsx";
2
- import Svg from "./Svg";
3
- import ScaledSvg from "./ScaledSvg"
4
- import { ReactCake , useReactCake} from "./ReactCake";
1
+ import { Html, type HtmlProps } from "./Html/index.tsx";
2
+ import { Svg, type SvgProps } from "./Svg";
3
+ import { ScaledSvg, type ScaledSvgProps } from "./ScaledSvg";
4
+ import { ReactCake, useReactCake, type ReactCakeProps } from "./ReactCake/index.tsx";
5
+ import { type Accessor } from "./types.ts";
5
6
 
6
- export {ReactCake, useReactCake, Html, Svg, ScaledSvg};
7
+ export {
8
+ Html,
9
+ ReactCake,
10
+ useReactCake,
11
+ Svg,
12
+ ScaledSvg,
13
+ type HtmlProps,
14
+ type ScaledSvgProps,
15
+ type SvgProps,
16
+ type ReactCakeProps,
17
+ type Accessor,
18
+ };
package/lib/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactcake",
3
3
  "dependencies": {
4
- "d3-array": "^3.2.4"
4
+ "d3-array": "^3.2.5"
5
5
  },
6
6
  "release": {
7
7
  "branches": [
package/lib/types.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  import { type ReactNode } from "react";
2
2
 
3
- export type DatumValue = number | Date | string;
3
+ export type DatumValue = number | string | Date;
4
4
  export type TickFormatter<TValue = DatumValue> = (value: TValue) => ReactNode;
5
5
 
6
- export type Padding = {
7
- top?: number;
8
- left?: number;
9
- right?: number;
10
- bottom?: number;
11
- };
6
+ export type DomainLike =
7
+ | [min: number | null, max: number | null]
8
+ | Array<string | number | null>
9
+ | ((domain: Array<any>) => Array<any>)
10
+ | undefined;
12
11
 
13
12
  export interface ScaleLike<T = DatumValue> {
14
13
  (value: unknown): number;
15
- domain: () => T[];
14
+ domain: (domain: DomainLike) => ScaleLike<T>;
16
15
  range: (range: [number, number]) => ScaleLike<T>;
17
16
  ticks?: (count?: number) => T[];
18
17
  tickFormat?: (count?: number) => TickFormatter<T>;
@@ -20,7 +19,7 @@ export interface ScaleLike<T = DatumValue> {
20
19
  invert?: (d: number) => ScaleLike;
21
20
  }
22
21
 
23
- type Accessor = (x: DatumValue) => number
22
+ export type Accessor = string | ((d: unknown, i?: number) => unknown);
24
23
 
25
24
  export interface ContextValue {
26
25
  width: number;
@@ -35,12 +34,39 @@ export interface ContextValue {
35
34
  xGet?: (d: unknown) => number;
36
35
  yGet?: (d: unknown) => number;
37
36
  zGet?: (d: unknown) => number | string;
38
- padding: Padding;
37
+ padding: {
38
+ top?: number;
39
+ left?: number;
40
+ right?: number;
41
+ bottom?: number;
42
+ };
39
43
  }
40
44
 
41
45
  export interface LayoutProps {
42
- overflow?: "hidden" | "visible",
46
+ /**
47
+ * Sets the CSS `overflow` property.
48
+ * @defaultValue "visible"
49
+ */
50
+ overflow?: "hidden" | "visible";
51
+ /**
52
+ * Sets the `aria-label` attribute
53
+ */
43
54
  label?: string;
55
+ /**
56
+ * Sets the `aria-labelledby` attribute
57
+ */
44
58
  labelledBy?: string;
59
+ /**
60
+ * Sets the `aria-describedby` attribute
61
+ */
45
62
  describedBy?: string;
63
+ /**
64
+ * Sets the CSS `z-index` property
65
+ */
66
+ zIndex?: number;
67
+ /**
68
+ * Sets the CSS `pointer-events` property.
69
+ * @defaultValue true
70
+ */
71
+ pointerEvents?: boolean;
46
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomephant/reactcake",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "A headless chart framework for React inspired by Layercake",
5
5
  "keywords": [
6
6
  "react",
@@ -27,21 +27,30 @@
27
27
  "build:ts": "esbuild lib/index.ts --outdir=./dist --bundle",
28
28
  "build:min": "esbuild lib/index.ts --outdir=./dist --entry-names=[dir]/[name].min --bundle --minify",
29
29
  "build:types": "tsc",
30
- "build:lib": "run-p 'build:ts' 'build:min'",
31
- "build:demo": "esbuild src/index.ts src/index.html --loader:.html=copy --outdir=./_site --minify"
30
+ "build:lib": "run-p 'build:ts' 'build:min'"
32
31
  },
33
32
  "devDependencies": {
34
- "esbuild": "^0.28.0",
33
+ "esbuild": "^0.28.2",
35
34
  "npm-run-all": "^4.1.5",
36
- "tsx": "^4.21.0",
37
- "typescript": "^7.0.2"
35
+ "tsx": "^4.23.15",
36
+ "typescript": "^6.0.0"
38
37
  },
39
38
  "workspaces": [
40
- "site",
41
- "lib"
39
+ "lib",
40
+ "site"
42
41
  ],
43
42
  "allowScripts": {
44
43
  "esbuild@0.28.2": true,
45
- "fsevents@2.3.3": true
44
+ "fsevents@2.3.3": true,
45
+ "@parcel/watcher@2.6.0": true
46
+ },
47
+ "release": {
48
+ "plugins": [
49
+ "@semantic-release/commit-analyzer",
50
+ "@semantic-release/release-notes-generator",
51
+ "@semantic-release/changelog",
52
+ "@semantic-release/npm",
53
+ "@semantic-release/github"
54
+ ]
46
55
  }
47
56
  }
@@ -1,22 +1,54 @@
1
1
  // @ts-check
2
- import { defineConfig } from 'astro/config';
3
- import {transformerRenderIndentGuides, transformerRenderLineNumber} from "@shikijs/transformers"
2
+ import { defineConfig, fontProviders } from "astro/config";
3
+ import {
4
+ transformerRenderIndentGuides,
5
+ transformerRenderLineNumber,
6
+ } from "@shikijs/transformers";
4
7
 
5
- import mdx from '@astrojs/mdx';
8
+ import mdx from "@astrojs/mdx";
6
9
 
7
- import react from '@astrojs/react';
10
+ import react from "@astrojs/react";
8
11
 
9
12
  export default defineConfig({
13
+ fonts: [
14
+ {
15
+ provider: fontProviders.local(),
16
+ name: "Areal",
17
+ cssVariable: "--font-areal",
18
+ options: {
19
+ variants: [
20
+ {
21
+ weight: "100 900",
22
+ style: "normal",
23
+ src: ["./src/assets/ABCArealVariable.woff2"],
24
+ },
25
+ ],
26
+ },
27
+ },
28
+ {
29
+ provider: fontProviders.local(),
30
+ name: "Areal Mono",
31
+ cssVariable: "--font-areal-mono",
32
+ options: {
33
+ variants: [
34
+ {
35
+ weight: "100 900",
36
+ style: "normal",
37
+ src: ["./src/assets/ABCArealMonoVariable.woff2"],
38
+ },
39
+ ],
40
+ },
41
+ },
42
+ ],
10
43
  markdown: {
11
44
  shikiConfig: {
12
- theme: 'css-variables',
45
+ theme: "css-variables",
13
46
  defaultColor: false,
14
- transformers:[transformerRenderLineNumber(), transformerRenderIndentGuides()]
15
47
  },
16
48
  syntaxHighlight: {
17
- type: 'shiki',
18
- },
49
+ type: "shiki",
50
+ },
19
51
  },
20
52
 
21
- integrations: [mdx(), react()]
53
+ integrations: [mdx(), react()],
22
54
  });
package/site/package.json CHANGED
@@ -7,23 +7,31 @@
7
7
  },
8
8
  "scripts": {
9
9
  "start": "astro dev",
10
- "build": "astro build",
10
+ "build:typedoc": "typedoc ../lib/index.ts --tsconfig ../tsconfig.json --json ./src/content/typedoc.json",
11
+ "build:astro": "astro build",
11
12
  "preview": "astro preview",
12
- "astro": "astro"
13
+ "astro": "astro",
14
+ "build": "run-s 'build:*'"
13
15
  },
14
16
  "dependencies": {
15
- "@astrojs/markdown-satteri": "^0.4.6",
16
- "@astrojs/mdx": "^8.1.2",
17
- "@astrojs/react": "^7.1.2",
18
- "astro": "^7.4.4",
19
- "d3-scale": "^4.0.2",
17
+ "@astrojs/markdown-satteri": "^0.4.7",
18
+ "@astrojs/mdx": "^8.1.3",
19
+ "@astrojs/react": "^7.1.4",
20
+ "astro": "^7.4.7",
21
+ "d3-scale": "^4.0.3",
20
22
  "react": "^19.3.1",
21
23
  "react-dom": "^19.3.1"
22
24
  },
23
25
  "devDependencies": {
26
+ "@semantic-release/changelog": "^7.0.0",
24
27
  "@shikijs/transformers": "^4.6.0",
25
28
  "@types/d3-scale": "^4.0.9",
26
29
  "@types/react": "^19.3.0",
27
- "@types/react-dom": "^19.3.0"
30
+ "@types/react-dom": "^19.3.0",
31
+ "npm-run-all": "^4.1.5",
32
+ "prettier": "^3.10.1",
33
+ "prettier-plugin-astro": "^1.1.2",
34
+ "sass-embedded": "^1.108.2",
35
+ "typedoc": "^0.28.20"
28
36
  }
29
37
  }
@@ -10,8 +10,7 @@ const {height, width = "100%"} = Astro.props
10
10
 
11
11
  <style>
12
12
  .container {
13
- margin: 1rem 0;
14
13
  border: 1px solid var(--blue-50);
15
- font-family: sans-serif;
14
+ font-size: 13px;
16
15
  }
17
16
  </style>
@@ -0,0 +1,97 @@
1
+ ---
2
+ import { getCollection } from "astro:content";
3
+ import TsdComment from "./TSDComment.astro";
4
+
5
+ const { data } = Astro.props;
6
+ const typedoc = await getCollection("typedoc");
7
+
8
+ const expandReferenceType = (name: string, data) => {
9
+ const ref = data.find((d) => d.data.name === name);
10
+ return ref ? ref.data.children : null;
11
+ };
12
+
13
+ const expanded = data[0].parameters
14
+ ? data[0].parameters
15
+ .map((d) => {
16
+ return d.type.type === "reference"
17
+ ? expandReferenceType(d.type.name, typedoc)
18
+ : d.type;
19
+ })
20
+ .flat()
21
+ : [];
22
+ ---
23
+
24
+ <ul>
25
+ {expanded.map((d) => (
26
+ <>
27
+ <li>
28
+ <span>
29
+ {d?.name}
30
+ {!d?.flags.isOptional && <strong class="required">required</strong>}
31
+ </span>
32
+ <span>
33
+ <code class="value">
34
+ {d?.type.type === "union" ? (
35
+ `${d.type.types.map((t) => `"${t.value}"`).join(" | ")}`
36
+ ) : d?.type.type === "array" ? (
37
+ <>{d.type.elementType.name}[]</>
38
+ ) : d?.type.type === "reflection" ? (
39
+ <pre>{JSON.stringify(
40
+ Object.fromEntries(
41
+ d.type.declaration.children.map((c) => [
42
+ c.name,
43
+ c.type.name,
44
+ ]),
45
+ ),
46
+ null,
47
+ " ",
48
+ ).replaceAll('"', "")}</pre>
49
+ ) : (
50
+ <>{d?.type?.name}</>
51
+ )}
52
+ </code>
53
+ </span>
54
+ {d.comment && <TsdComment comment={d.comment} />}
55
+ </li>
56
+ </>
57
+ ))}
58
+ </ul>
59
+
60
+ <style lang="scss">
61
+ ul {
62
+ display: grid;
63
+ grid-template-columns: repeat(2, max-content) 1fr;
64
+ margin: 0;
65
+ column-gap: 2rem;
66
+ margin-bottom: 1rem;
67
+ width: 100%;
68
+ }
69
+
70
+ li {
71
+ display: grid;
72
+ grid-template-columns: subgrid;
73
+ border-bottom: 1px dotted currentColor;
74
+ grid-column: 1 / 4;
75
+ align-items: baseline;
76
+ &:last-child {
77
+ border-bottom: 0;
78
+ }
79
+ }
80
+ .required {
81
+ margin-left: 0.5ch;
82
+ background: #f2d9d9;
83
+ font-family: monospace;
84
+ padding: 0.1em 0.3em;
85
+ padding-bottom: 0;
86
+ font-size: 0.9em;
87
+ font-weight: normal;
88
+ text-transform: uppercase;
89
+ }
90
+
91
+ .value {
92
+ display: block;
93
+ hyphens: none;
94
+ max-width: 32ch;
95
+ overflow-wrap: break-word;
96
+ }
97
+ </style>
@@ -0,0 +1,32 @@
1
+ ---
2
+ const { comment } = Astro.props;
3
+
4
+ const defaultValue = comment.blockTags?.find((c) => c.tag === "@defaultValue");
5
+ ---
6
+
7
+ <span>
8
+ {comment.summary.map((part) =>
9
+ part.kind === "text" ? (
10
+ <>{part.text}</>
11
+ ) : part.kind === "code" ? (
12
+ <code>{part.text.replaceAll("`", "")}</code>
13
+ ) : part.tag === "@link" ? (
14
+ <a href={part.target}>{part.text}</a>
15
+ ) : (
16
+ <></>
17
+ ),
18
+ )}
19
+ {defaultValue && (
20
+ <>
21
+ {" "}
22
+ Defaults to{" "}
23
+ <code>
24
+ {defaultValue.content[0].text
25
+ .replaceAll("```", "")
26
+ .replace("ts", "")
27
+ .trim()}
28
+ </code>
29
+ .
30
+ </>
31
+ )}
32
+ </span>
@@ -0,0 +1,18 @@
1
+ ## Background
2
+
3
+ ReactCake provides useful context for drawing data-driven charts, like a shared coordinate system, data extents and margins. Components that draw actual chart elements, like bars and lines, live in your project or your own component library; ReactCake is completely agnostic to what you render with it.
4
+
5
+ It provides sensible defaults and infers as much information as possible from your data, but also exposes fine-grained props to configure things yourself. Its primary use case are custom news graphics.
6
+
7
+ ReactCake is built on [D3](https://d3js.org/what-is-d3).
8
+
9
+ ### Relationship to Layercake
10
+
11
+ This project is an attempt to carefully translate the overall approach and development ergonomics of the Layercake framework from Svelte to React. It does not aim for feature parity or identical API surfaces.
12
+
13
+
14
+ ### Prior work
15
+
16
+ - Rich Harris (2020): *[A new technique for making responsive, JavaScript-free charts](https://dev.to/richharris/a-new-technique-for-making-responsive-javascript-free-charts-gmp)*
17
+ - Michael Keller + contributors: *[Layercake](https://layercake.graphics/)*
18
+ - Rich Harris (2020): *[pancake](https://github.com/Rich-Harris/pancake)*
@@ -0,0 +1,38 @@
1
+ ### Installation
2
+
3
+ ```bash
4
+ npm install --save @awesomephant/reactcake
5
+ ```
6
+
7
+ ### Usage
8
+
9
+ ```tsx
10
+ import { ReactCake, Html, Svg } from "@awesomephant/reactcake";
11
+ import { scaleLinear, scaleBand } from "d3-scale";
12
+
13
+ // Chart components live in your project
14
+ import { xScale, yScale, Bars } from "./lib";
15
+
16
+ const data = [
17
+ { year: 2024, value: 32 },
18
+ { year: 2025, value: 15 },
19
+ { year: 2026, value: 52 },
20
+ ];
21
+
22
+ // Use any d3 scale
23
+ const xScale = scaleBand();
24
+ const yScale = scaleLinear();
25
+
26
+ // Build charts declaratively
27
+ return (
28
+ <ReactCake data={data} x="year" y="value" xScale={xScale} yScale={yScale}>
29
+ <Html>
30
+ <AxisX />
31
+ <AxisY />
32
+ </Html>
33
+ <Svg>
34
+ <Bars />
35
+ </Svg>
36
+ </ReactCake>
37
+ );
38
+ ```
@@ -0,0 +1,8 @@
1
+ import { defineCollection } from "astro:content";
2
+ import { file } from "astro/loaders";
3
+
4
+ const typedoc = defineCollection({
5
+ loader: file("./src/content/typedoc.json", { parser: (s) => JSON.parse(s).children }),
6
+ });
7
+
8
+ export const collections = { typedoc };
@@ -9,7 +9,7 @@
9
9
  }
10
10
 
11
11
  .tick__mark {
12
- border-left: 1px solid var(--blue-50);
12
+ border-left: 1px solid currentColor;
13
13
  position: absolute;
14
14
  transform: translateX(-50%);
15
15
  }
@@ -17,20 +17,19 @@
17
17
  .gridline {
18
18
  bottom: 0;
19
19
  position: absolute;
20
- border-left: 1px dotted var(--blue-50);
20
+ border-left: 1px dotted currentColor;
21
21
  }
22
22
 
23
23
 
24
24
  .baseline {
25
25
  position: absolute;
26
- border-bottom: 1px solid var(--blue-50);
26
+ border-bottom: 1px solid currentColor;
27
27
  width: 100%;
28
28
  }
29
29
 
30
30
  .tick__text {
31
31
  display: block;
32
32
  font-size: var(--ms--1);
33
- color: var(--blue-50);
34
33
  position: relative;
35
34
  white-space: nowrap;
36
35
  }
@@ -11,6 +11,7 @@ interface AxisYProps {
11
11
  grid?: boolean;
12
12
  showTicks?: boolean;
13
13
  snapLabels?: boolean;
14
+ tickGutter: number;
14
15
  tickLength?: number;
15
16
  }
16
17
 
@@ -23,7 +24,7 @@ const AxisY = ({
23
24
  tickGutter = 3,
24
25
  format = (d) => String(d),
25
26
  }: AxisYProps) => {
26
- const { yScale, padding } = useReactCake();
27
+ const { yScale } = useReactCake();
27
28
  const isBandwidth = typeof yScale.bandwidth === "function";
28
29
 
29
30
  const tickVals = Array.isArray(ticks)
@@ -6,7 +6,7 @@
6
6
 
7
7
  .tick {
8
8
  position: absolute;
9
- border-top: 1px solid var(--blue-50);
9
+ border-top: 1px solid currentColor;
10
10
  }
11
11
 
12
12
 
@@ -14,20 +14,18 @@
14
14
  left: 0;
15
15
  right: 0;
16
16
  position: absolute;
17
- border-top: 1px dotted var(--blue-50);
17
+ border-top: 1px dotted currentColor;
18
18
  }
19
19
 
20
-
21
20
  .baseline {
22
21
  position: absolute;
23
- border-left: 1px solid var(--blue-50);
22
+ border-left: 1px solid currentColor;
24
23
  height: 100%;
25
24
  }
26
25
 
27
26
  .text {
28
27
  display: block;
29
28
  font-size: var(--ms--1);
30
- color: var(--blue-50);
31
29
  white-space: nowrap;
32
30
  transform: translateY(-50%);
33
31
  }
@@ -7,8 +7,6 @@ import { max } from "d3-array";
7
7
  import DebugPadding from "./DebugPadding";
8
8
 
9
9
  const data = [
10
- { year: 2021, value: 18 },
11
- { year: 2022, value: 12 },
12
10
  { year: 2023, value: 22 },
13
11
  { year: 2024, value: 32 },
14
12
  { year: 2025, value: 15 },
@@ -18,7 +18,7 @@ export default function Columns() {
18
18
  y={100 - valueY}
19
19
  width={colWidth}
20
20
  height={valueY}
21
- stroke="var(--blue-50)"
21
+ stroke="currentColor"
22
22
  fill="none"
23
23
  />
24
24
  );
@@ -2,6 +2,7 @@ import { useReactCake } from "../../../lib";
2
2
 
3
3
  export default function DebugPadding() {
4
4
  const { padding } = useReactCake();
5
+ const fill = "rgba(125,125,105,.1)";
5
6
  return (
6
7
  <div
7
8
  style={{
@@ -10,7 +11,7 @@ export default function DebugPadding() {
10
11
  right: 0,
11
12
  top: 0,
12
13
  bottom: 0,
13
- boxShadow: `${padding.right}px 0 var(--blue-20), 0 ${padding.bottom}px var(--blue-20), 0 ${-padding.top}px var(--blue-20), -${padding.left}px 0 var(--blue-20)`,
14
+ boxShadow: `${padding.right}px 0 ${fill}, 0 ${padding.bottom}px ${fill}, 0 ${-padding.top}px ${fill}, -${padding.left}px 0 ${fill}`,
14
15
  }}
15
16
  ></div>
16
17
  );
@@ -1,82 +1,109 @@
1
1
  ---
2
- import "./code.css"
2
+ import { Font } from "astro:assets";
3
+ import "./code.scss";
3
4
  ---
4
5
 
5
6
  <!doctype html>
6
7
  <html lang="en">
7
- <head>
8
- <meta charset="utf-8" />
9
- <meta name="viewport" content="width=device-width" />
10
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
11
- <meta name="generator" content={Astro.generator} />
12
- <title>ReactCake</title>
13
- </head>
14
- <body>
15
- <main class="content">
16
- <slot />
17
- </main>
18
- </body>
8
+ <head>
9
+ <meta charset="utf-8" />
10
+ <meta name="viewport" content="width=device-width" />
11
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
12
+ <meta name="generator" content={Astro.generator} />
13
+ <title>ReactCake</title>
14
+ <Font cssVariable="--font-areal" />
15
+ <Font cssVariable="--font-areal-mono" />
16
+ </head>
17
+ <body>
18
+ <slot />
19
+ </body>
19
20
  </html>
20
21
 
21
22
  <style is:global>
22
23
  * {
23
24
  box-sizing: border-box;
25
+ font-size: inherit;
24
26
  margin: 0;
25
27
  padding: 0;
26
28
  }
27
29
 
28
- body {
29
- margin: 0;
30
- width: 100%;
31
- padding: .5rem;
32
- font-family: Georgia, 'Times New Roman', Times, serif;
33
- --blue-70: hsl(220, 0%, 20%);
34
- --blue-50: hsl(220, 0%, 36%);
35
- --blue-30: hsl(220, 0%, 76%);
36
- --blue-20: hsl(220, 0%, 92%);
37
- --blue-10: hsl(220, 0%, 97%);
38
- --ms--1: .8rem
39
- }
40
-
41
- a, a:visited {
42
- color: var(--blue-70);
43
- }
30
+ body {
31
+ margin: 0;
32
+ width: 100%;
33
+ padding: 0.5rem;
34
+ font-family: var(--font-areal), sans-serif;
35
+ --blue-70: hsl(20, 80%, 20%);
36
+ --blue-50: hsl(20, 80%, 26%);
37
+ --blue-30: hsl(20, 80%, 76%);
38
+ --blue-20: hsl(20, 80%, 92%);
39
+ --blue-10: hsl(20, 80%, 97%);
40
+ --ms--1: 0.8rem padding: 1rem;
41
+ background: #d8d2f6;
42
+ line-height: 1.25;
43
+ color: var(--blue-50);
44
+ }
44
45
 
45
- .content {
46
- border: 1px solid var(--blue-70);
47
- max-width: 40rem;
48
- line-height: 1.4;
49
- padding: 1rem;
50
- border-right: 1px solid var(--blue-70);
51
- }
46
+ a,
47
+ a:visited {
48
+ color: var(--blue-50);
49
+ background: #f0f0f0;
50
+ text-decoration: none;
51
+ border-bottom: 1px dotted currentColor;
52
+ }
52
53
 
53
- p, li {
54
- letter-spacing: .002em;
55
- }
54
+ code {
55
+ font-family: var(--font-areal-mono);
56
+ font-size: 0.825rem;
57
+ }
56
58
 
57
- h1 {
58
- font-weight: 900;
59
- }
59
+ p,
60
+ li {
61
+ letter-spacing: 0.002em;
62
+ }
60
63
 
61
- h2 {
62
- margin-top: 1rem;
63
- margin-bottom: .25rem;
64
- }
65
- h3 {
66
- margin-top: 1.25rem;
67
- margin-bottom: 0.1rem;
68
- font-weight: 600;
69
- }
64
+ h1 {
65
+ font-weight: 900;
66
+ font-size: 2.75em;
67
+ transform: scaleX(0.8);
68
+ transform-origin: left;
69
+ }
70
70
 
71
- p, ul, ol {
72
- margin-bottom: 1rem;
73
- &:last-child {
74
- margin-bottom: 0;
75
- }
76
- }
71
+ h2 {
72
+ transform: scaleX(0.8) skew(-10deg);
73
+ transform-origin: left;
74
+ font-size: 1.85rem;
75
+ margin-top: 1rem;
76
+ margin-bottom: 0.25rem;
77
+ &:first-child {
78
+ margin-top: 0;
79
+ }
80
+ }
81
+ h3 {
82
+ margin-bottom: 0.1rem;
83
+ font-weight: 600;
84
+ font-size: 1.2em;
85
+ }
86
+ h4 {
87
+ font-weight: 400;
88
+ font-size: 1.1rem;
89
+ margin-bottom: 0.25rem;
90
+ code {
91
+ font-size: 0.85em;
92
+ font-weight: 800;
93
+ }
94
+ }
77
95
 
78
- ul, ol {
79
- margin-left: 1em;
80
- }
96
+ p,
97
+ ul,
98
+ ol {
99
+ margin-bottom: 0.75em;
100
+ &:last-child {
101
+ margin-bottom: 0;
102
+ }
103
+ }
81
104
 
105
+ ul,
106
+ ol {
107
+ margin-left: 1em;
108
+ }
82
109
  </style>
@@ -0,0 +1,32 @@
1
+ .astro-code {
2
+ --astro-code-background: white;
3
+ --astro-code-token-comment: #666;
4
+ font-family: var(--font-areal-mono);
5
+ border: 1px solid currentColor;
6
+ margin: 1em 0;
7
+ margin-top: 0.5em;
8
+ padding: 0.5em;
9
+ line-height: 0.95;
10
+ &:last-child {
11
+ margin-bottom: 0;
12
+ }
13
+ }
14
+
15
+ pre.astro-code .indent {
16
+ display: inline-block;
17
+ position: relative;
18
+ left: var(--indent-offset);
19
+ }
20
+
21
+ pre.astro-code .indent:empty {
22
+ height: 1lh;
23
+ vertical-align: bottom;
24
+ }
25
+
26
+ pre.astro-code .indent::before {
27
+ content: "";
28
+ position: absolute;
29
+ opacity: 0.5;
30
+ width: 1px;
31
+ height: 100%;
32
+ }
@@ -0,0 +1,137 @@
1
+ ---
2
+ import { getCollection } from "astro:content";
3
+
4
+ import Base from "../layouts/Base.astro";
5
+ import { Content as Installation } from "../content/installation.md";
6
+ import { Content as Background } from "../content/background.md";
7
+ import Example from "../components/Example.astro";
8
+ import PropList from "../components/PropList.astro";
9
+ import ColumnChart from "../examples/ColumnChart.tsx";
10
+
11
+ const typedoc = await getCollection("typedoc");
12
+
13
+ const components = ["Reactcake", "Html", "Svg", "ScaledSvg"];
14
+ ---
15
+
16
+ <Base>
17
+ <article class="home">
18
+ <header>
19
+ <div>
20
+ <h1>ReactCake</h1>
21
+ <p>
22
+ A headless chart framework for React inspired by{" "}
23
+ <a href="https://layercake.graphics/">Layercake</a>
24
+ </p>
25
+ <ul>
26
+ <li>
27
+ <a href="https://github.com/awesomephant/reactcake">Github</a>
28
+ </li>
29
+ <li>
30
+ <a href="https://www.npmjs.com/package/@awesomephant/reactcake">
31
+ npm
32
+ </a>
33
+ </li>
34
+ </ul>
35
+ </div>
36
+ <Example height="12rem">
37
+ <ColumnChart />
38
+ </Example>
39
+ </header>
40
+
41
+ <section class="installation">
42
+ <h2>Quickstart</h2>
43
+ <Installation />
44
+ </section>
45
+
46
+ <section class="docs">
47
+ <h2>Docs</h2>
48
+ <h3>Components</h3>
49
+ {typedoc
50
+ .filter((d) => d.data.kind === 64 && !d.data.name.startsWith("use"))
51
+ .sort(
52
+ (a, b) =>
53
+ components.indexOf(a.data.name) - components.indexOf(b.data.name),
54
+ )
55
+ .map((d) => {
56
+ return (
57
+ <>
58
+ <h4>
59
+ <code>
60
+ {`<`}
61
+ {d.data.name}
62
+ {`/>`}
63
+ </code>
64
+ </h4>
65
+ {d.data.signatures[0].comment && (
66
+ <p>{d.data.signatures[0].comment.summary[0].text}</p>
67
+ )}
68
+ <PropList data={d.data.signatures} />
69
+ </>
70
+ );
71
+ })}
72
+ <h3>Hooks</h3>
73
+ {typedoc
74
+ .filter((d) => d.data.kind === 64 && d.data.name.startsWith("use"))
75
+ .map((d) => {
76
+ return (
77
+ <>
78
+ <h3>
79
+ <code>
80
+ {d.data.name}
81
+ {`()`}
82
+ </code>
83
+ </h3>
84
+ {d.data.signatures[0].comment && (
85
+ <p>{d.data.signatures[0].comment.summary[0].text}</p>
86
+ )}
87
+ <PropList data={d.data.signatures} />
88
+ </>
89
+ );
90
+ })}
91
+ <h3>Utils</h3>
92
+ </section>
93
+
94
+ <section class="background">
95
+ <Background />
96
+ </section>
97
+ </article>
98
+ </Base>
99
+
100
+ <style lang="scss">
101
+ .home {
102
+ display: grid;
103
+ grid-template-columns: repeat(12, 1fr);
104
+ }
105
+
106
+ header,
107
+ section {
108
+ padding: 1rem;
109
+ border: 1px solid var(--blue-50);
110
+ background-color: white;
111
+ margin-bottom: -1px;
112
+ margin-right: -1px;
113
+ }
114
+ header {
115
+ border-top: 1px solid var(--blue-50);
116
+ border-left: 1px solid var(--blue-50);
117
+ grid-column: span 2;
118
+ display: flex;
119
+ flex-flow: column;
120
+ justify-content: space-between;
121
+ }
122
+
123
+ .installation {
124
+ border-top: 1px solid var(--blue-50);
125
+ grid-column: 3 / 8;
126
+ }
127
+ .docs {
128
+ border-top: 1px solid var(--blue-50);
129
+ grid-column: 8 / 13;
130
+ grid-row: span 3;
131
+ }
132
+
133
+ .background {
134
+ grid-column: 3 / 8;
135
+ border-left: 1px solid var(--blue-50);
136
+ }
137
+ </style>
package/tsconfig.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "noUncheckedIndexedAccess": true,
8
8
  "noImplicitOverride": true,
9
9
  "outDir": "dist",
10
- "rootDir": "./lib",
10
+ "rootDir": ".",
11
11
  "declaration": true,
12
12
  "emitDeclarationOnly": true,
13
13
  "allowImportingTsExtensions": true,
@@ -1,37 +0,0 @@
1
- .astro-code {
2
- --astro-code-background: hsl(51, 62.5%, 93.7%);
3
- --astro-code-token-comment: #666;
4
- margin: 1em 0;
5
- margin-top: .5em;
6
- padding: .5em;
7
- font-size: 12px;
8
- line-height: 1.25;
9
- font-family: monospace;
10
- }
11
-
12
-
13
- pre.astro-code .indent {
14
- display: inline-block;
15
- position: relative;
16
- left: var(--indent-offset);
17
- }
18
-
19
- pre.astro-code .indent:empty {
20
- height: 1lh;
21
- vertical-align: bottom;
22
- }
23
-
24
- pre.astro-code .indent::before {
25
- content: '';
26
- position: absolute;
27
- opacity: 0.5;
28
- width: 1px;
29
- height: 100%;
30
- background-color: red;
31
- }
32
-
33
- pre.astro-code .line-number {
34
- user-select: none;
35
- color: #666;
36
- margin-right: 2ch;
37
- }
@@ -1,73 +0,0 @@
1
- ---
2
- layout: "../layouts/Base.astro"
3
-
4
- ---
5
-
6
- import Example from "../components/Example.astro"
7
- import ColumnChart from "../examples/ColumnChart.tsx"
8
-
9
- # ReactCake
10
-
11
- A headless chart framework for React inspired by [Layercake](https://layercake.graphics/).
12
-
13
- <Example height="12rem" width="20rem">
14
- <ColumnChart></ColumnChart>
15
- </Example>
16
-
17
- ## Installation
18
-
19
- ```bash
20
- npm install --save @awesomephant/reactcake
21
- ```
22
-
23
- ## Usage
24
-
25
- ```tsx
26
- import { ReactCake, Html, Svg } from "@awesomephant/reactcake";
27
- import { scaleLinear, scaleBand } from "d3-scale"
28
-
29
- // Chart components live in your project
30
- import { xScale, yScale, Bars } from "./lib";
31
-
32
- const data = [
33
- {year: 2024, value: 32},
34
- {year: 2025, value: 15},
35
- {year: 2026, value: 52}
36
- ];
37
-
38
- // Use any d3 scale
39
- const xScale = scaleTime();
40
- const xScale = scaleBand();
41
-
42
- // Build charts declaratively
43
- return (
44
- <ReactCake data={data} x="year" y="value" xScale={xScale} yScale={yScale}>
45
- <Html>
46
- <AxisX />
47
- <AxisY />
48
- </Html>
49
- <Svg>
50
- <Bars/>
51
- </Svg>
52
- </ReactCake>
53
- );
54
- ```
55
-
56
- ## Concepts
57
-
58
- ReactCake provides useful context for drawing data-driven charts, like a coordinate system, data extents and shared margins. Components that draw actual chart elements, like bars and lines, live in your project (or maybe a local component library), ReactCake is completely agnostic to what you render with it.
59
-
60
- It provides sensible defaults and infers as much information as possible from your data, but also exposes fine-grained props to configure things yourself.
61
-
62
- ReactCake is built on [D3](https://d3js.org/what-is-d3).
63
-
64
- ### Relationship to Layercake
65
-
66
- This project is an attempt to carefully translate the overall approach and development ergonomics of the Layercake framework from Svelte to React. It does not aim for feature parity or identical API surfaces.
67
-
68
-
69
- ### Prior work
70
-
71
- - [A new technique for making responsive, JavaScript-free charts](https://dev.to/richharris/a-new-technique-for-making-responsive-javascript-free-charts-gmp)
72
- - [Layercake](https://layercake.graphics/)
73
- - [pancake](https://github.com/Rich-Harris/pancake)