@awesomephant/reactcake 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md 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
- [![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) [![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
 
@@ -1,21 +1,22 @@
1
1
  import { type ReactNode } from "react";
2
+ import { useReactCake } from "../ReactCake";
2
3
 
3
4
  interface HtmlProps {
4
5
  children?: ReactNode;
5
6
  }
6
7
 
7
8
  const Html = ({ children }: HtmlProps) => {
9
+ const { padding } = useReactCake();
10
+
8
11
  return (
9
12
  <div
10
13
  className="reactcake-html"
11
14
  style={{
12
15
  position: "absolute",
13
- height: "100%",
14
- width: "100%",
15
- top: 0,
16
- left: 0,
17
- right: 0,
18
- bottom: 0,
16
+ top: padding.top,
17
+ left: padding.left,
18
+ right: padding.right,
19
+ bottom: padding.bottom,
19
20
  }}
20
21
  >
21
22
  {children}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomephant/reactcake",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A headless chart framework for React inspired by Layercake",
5
5
  "keywords": [
6
6
  "react",
@@ -11,6 +11,6 @@ const {height, width = "100%"} = Astro.props
11
11
  <style>
12
12
  .container {
13
13
  margin: 1rem 0;
14
- border: 1px solid #2b598a;
14
+ border: 1px solid var(--blue-50);
15
15
  }
16
16
  </style>
@@ -0,0 +1,106 @@
1
+ import { Fragment } from "react";
2
+ import { useReactCake } from "../../../../lib";
3
+ import styles from "./styles.module.css";
4
+
5
+ interface AxisProps {
6
+ position?: "top" | "bottom";
7
+ count?: number;
8
+ format?: (d: unknown) => string;
9
+ label?: string;
10
+ ticksValues?: number | DatumValue[] | ((ticks: unknown[]) => unknown[]);
11
+ showBaseline?: boolean;
12
+ grid?: boolean;
13
+ snapLabels?: boolean;
14
+ tickLength?: number;
15
+ }
16
+
17
+ const AxisX = ({
18
+ ticks,
19
+ showBaseline = true,
20
+ position = "bottom",
21
+ grid = false,
22
+ format = (d) => String(d),
23
+ tickLength = 8,
24
+ }: AxisProps) => {
25
+ const { xScale } = useReactCake();
26
+
27
+ if (!xScale) return <></>;
28
+
29
+ const isBandwidth = typeof xScale.bandwidth === "function";
30
+
31
+ const tickVals = Array.isArray(ticks)
32
+ ? ticks
33
+ : isBandwidth
34
+ ? xScale.domain()
35
+ : typeof ticks === "function"
36
+ ? ticks(xScale.ticks())
37
+ : xScale.ticks(ticks);
38
+
39
+ const halfBand = isBandwidth ? xScale.bandwidth() / 2 : 0;
40
+
41
+ const tickEls = tickVals.map((v) => {
42
+ const x = xScale(v);
43
+
44
+ return (
45
+ <Fragment key={`tick-${v.toString()}`}>
46
+ {grid && (
47
+ <div
48
+ className={styles.gridline}
49
+ style={{
50
+ top: 0,
51
+ bottom: 0,
52
+ left: `${x + halfBand}%`,
53
+ }}
54
+ />
55
+ )}
56
+ <div
57
+ className={styles.tick}
58
+ style={{
59
+ left: `${x + halfBand}%`,
60
+ bottom: position === "bottom" ? -tickLength : "auto",
61
+ top: position === "top" ? -tickLength + 1 : "auto",
62
+ height: `${tickLength}px`,
63
+ }}
64
+ >
65
+ <span className={styles.tick_mark}></span>
66
+ <span
67
+ className={styles.tick__text}
68
+ style={{
69
+ top: position === "bottom" ? `${tickLength}px` : "auto",
70
+ left: 0,
71
+ bottom: position === "top" ? `${tickLength + 2}px` : "auto",
72
+ transform: `translateX(-50%) ${position === "top" ? "translateY(-50%)" : ""}`,
73
+ }}
74
+ >
75
+ {format(v)}
76
+ </span>
77
+ </div>
78
+ </Fragment>
79
+ );
80
+ });
81
+
82
+ return (
83
+ <div
84
+ className={styles.axis}
85
+ style={{
86
+ bottom: position === "bottom" ? 0 : "auto",
87
+ top: position === "top" ? 0 : "auto",
88
+ }}
89
+ >
90
+ {tickEls}
91
+ {showBaseline && (
92
+ <div
93
+ className={styles.baseline}
94
+ style={{
95
+ bottom: position === "bottom" ? 0 : "auto",
96
+ top: position === "top" ? 0 : "auto",
97
+ left: 0,
98
+ right: 0,
99
+ }}
100
+ ></div>
101
+ )}
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default AxisX;
@@ -0,0 +1,31 @@
1
+ .axis {
2
+ width: 100%;
3
+ height: 100%;
4
+ position: absolute;
5
+ }
6
+
7
+ .tick {
8
+ position: absolute;
9
+ border-left: 1px solid var(--blue-50);
10
+ }
11
+
12
+ .gridline {
13
+ bottom: 0;
14
+ position: absolute;
15
+ border-left: 1px dotted var(--blue-50);
16
+ }
17
+
18
+
19
+ .baseline {
20
+ position: absolute;
21
+ border-bottom: 1px solid var(--blue-50);
22
+ width: 100%;
23
+ }
24
+
25
+ .tick__text {
26
+ display: block;
27
+ font-size: var(--ms--1);
28
+ color: var(--blue-50);
29
+ position: relative;
30
+ white-space: nowrap;
31
+ }
@@ -0,0 +1,76 @@
1
+ import { Fragment } from "react";
2
+ import { useReactCake } from "../../../../lib";
3
+ import styles from "./styles.module.css";
4
+
5
+ interface AxisYProps {
6
+ position?: "left" | "right";
7
+ format?: (d: unknown) => string;
8
+ label?: string;
9
+ ticks?: number | DatumValue[] | ((ticks: unknown[]) => unknown[]);
10
+ showBaseline?: boolean;
11
+ grid?: boolean;
12
+ showTicks?: boolean;
13
+ snapLabels?: boolean;
14
+ tickLength?: number;
15
+ }
16
+
17
+ const AxisY = ({
18
+ ticks,
19
+ showBaseline = true,
20
+ grid = true,
21
+ showTicks = true,
22
+ tickLength = 8,
23
+ tickGutter = 3,
24
+ format = (d) => String(d),
25
+ }: AxisYProps) => {
26
+ const { yScale, padding } = useReactCake();
27
+ const isBandwidth = typeof yScale.bandwidth === "function";
28
+
29
+ const tickVals = Array.isArray(ticks)
30
+ ? ticks
31
+ : isBandwidth
32
+ ? yScale.domain()
33
+ : typeof ticks === "function"
34
+ ? ticks(yScale.ticks())
35
+ : yScale.ticks(ticks);
36
+
37
+ const halfBand = isBandwidth ? yScale.bandwidth() / 2 : 0;
38
+
39
+ const tickEls = tickVals.map((v, i) => {
40
+ const y = yScale(v);
41
+ return (
42
+ <Fragment key={`tick-${i}`}>
43
+ {grid && y + halfBand > 0 && (
44
+ <div className={styles.gridline} style={{ bottom: `${y + halfBand}%`, left: 0 }}></div>
45
+ )}
46
+ {format(v) !== "" && (
47
+ <div
48
+ className={styles.tick}
49
+ style={{
50
+ width: `${tickLength}px`,
51
+ bottom: `${y + halfBand}%`,
52
+ left: -tickLength,
53
+ }}
54
+ >
55
+ {showTicks && <span className={styles.mark}></span>}
56
+ <span
57
+ className={styles.text}
58
+ style={{ position: "absolute", right: `calc(100% + ${tickGutter}px)` }}
59
+ >
60
+ {format(v)}
61
+ </span>
62
+ </div>
63
+ )}
64
+ </Fragment>
65
+ );
66
+ });
67
+
68
+ return (
69
+ <div className={styles.axis} style={{ bottom: 0, top: 0 }}>
70
+ {tickEls}
71
+ {showBaseline && <div className={styles.baseline} style={{ left: 0 }}></div>}
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default AxisY;
@@ -0,0 +1,33 @@
1
+ .axis {
2
+ width: 100%;
3
+ height: 100%;
4
+ position: absolute;
5
+ }
6
+
7
+ .tick {
8
+ position: absolute;
9
+ border-top: 1px solid var(--blue-50);
10
+ }
11
+
12
+
13
+ .gridline {
14
+ left: 0;
15
+ right: 0;
16
+ position: absolute;
17
+ border-top: 1px dotted var(--blue-50);
18
+ }
19
+
20
+
21
+ .baseline {
22
+ position: absolute;
23
+ border-left: 1px solid var(--blue-50);
24
+ height: 100%;
25
+ }
26
+
27
+ .text {
28
+ display: block;
29
+ font-size: var(--ms--1);
30
+ color: var(--blue-50);
31
+ white-space: nowrap;
32
+ transform: translateY(-50%);
33
+ }
@@ -1,26 +1,41 @@
1
1
  import { ReactCake, ScaledSvg, Html } from "../../../lib";
2
+ import AxisX from "./AxisX/index";
3
+ import AxisY from "./AxisY";
2
4
  import Columns from "./Columns";
3
- import { scaleLinear, scaleBand } from "d3-scale"
5
+ import { scaleLinear, scaleBand } from "d3-scale";
6
+ import { max } from "d3-array";
4
7
 
5
8
  const data = [
6
- {year: 2022, value: 12},
7
- {year: 2023, value: 22},
8
- {year: 2024, value: 32},
9
- {year: 2025, value: 15},
10
- {year: 2026, value: 52}
11
- ]
9
+ { year: 2021, value: 18 },
10
+ { year: 2022, value: 12 },
11
+ { year: 2023, value: 22 },
12
+ { year: 2024, value: 32 },
13
+ { year: 2025, value: 15 },
14
+ { year: 2026, value: 52 },
15
+ ];
12
16
 
13
- const xScale = scaleBand().paddingInner(.1)
17
+ const xScale = scaleBand().paddingInner(0.1).paddingOuter(0.2);
18
+ const yKey = "value";
14
19
 
15
20
  export default function ColumnChart() {
16
21
  return (
17
- <ReactCake data={data} x="year" y="value" yScale={scaleLinear()} xScale={xScale} percentRange={true} yDomain={[0, null]} padding={{ top: 10, left: 10, right: 10, bottom: 10 }}>
18
- <ScaledSvg>
19
- <Columns/>
20
- </ScaledSvg>
21
- <Html>
22
-
23
- </Html>
24
- </ReactCake>
25
- )
22
+ <ReactCake
23
+ data={data}
24
+ x="year"
25
+ y={yKey}
26
+ yScale={scaleLinear()}
27
+ xScale={xScale}
28
+ percentRange={true}
29
+ yDomain={[0, max(data, (d) => d[yKey]) + 5]}
30
+ padding={{ top: 10, left: 35, right: 10, bottom: 30 }}
31
+ >
32
+ <Html>
33
+ <AxisX grid ticks />
34
+ <AxisY grid ticks={3} />
35
+ </Html>
36
+ <ScaledSvg overflow="hidden">
37
+ <Columns />
38
+ </ScaledSvg>
39
+ </ReactCake>
40
+ );
26
41
  }
@@ -1,30 +1,28 @@
1
- import { useReactCake } from '../../../lib';
1
+ import { useReactCake } from "../../../lib";
2
2
 
3
3
  export default function Columns() {
4
4
  const { data, x, xGet, yGet, xScale } = useReactCake();
5
5
 
6
- console.log(data)
7
-
8
6
  return (
9
7
  <g className="column-group">
10
8
  {data?.map((d) => {
9
+ const valueY = yGet(d);
10
+ const xGot = xGet(d);
11
+ const xPos = Array.isArray(xGot) ? xGot[0] : xGot;
12
+ const colWidth = xScale.bandwidth ? xScale.bandwidth() : 20;
11
13
 
12
- const valueY = yGet(d)
13
- const xGot = xGet(d)
14
- const xPos = Array.isArray(xGot) ? xGot[0] : xGot
15
- const colWidth = xScale.bandwidth ? xScale.bandwidth() : 20
16
-
17
- return <rect
18
- key={x(d)}
19
- x={xPos}
20
- y={100 - valueY}
21
- width={colWidth}
22
- height={valueY}
23
- stroke="#196c9e"
24
- fill="none"
25
- />
14
+ return (
15
+ <rect
16
+ key={x(d)}
17
+ x={xPos}
18
+ y={100 - valueY}
19
+ width={colWidth}
20
+ height={valueY}
21
+ stroke="var(--blue-50)"
22
+ fill="none"
23
+ />
24
+ );
26
25
  })}
27
26
  </g>
28
-
29
- )
27
+ );
30
28
  }
@@ -0,0 +1,39 @@
1
+ import { useContext } from "react";
2
+ import { Context } from "../ReactCake/";
3
+ import { type ContextValue } from "../ReactCake/types";
4
+
5
+ export default function GanttChart({ currentStep = null, highlight, maxDate, maxLegalDate }) {
6
+ const { x2, xGet, yGet, data, xScale, yScale, zGet } = useContext<ContextValue>(Context);
7
+
8
+ const scaledData = data.map((d, i) => {
9
+ const _maxDate =
10
+ d.kind === "construction" && maxLegalDate ? Math.min(maxLegalDate, maxDate) : maxDate;
11
+
12
+ const _x2 = x2(d) > _maxDate ? _maxDate : x2(d);
13
+
14
+ const fromStep = d.from_step || i;
15
+ const isActive = currentStep >= fromStep;
16
+
17
+ return {
18
+ ...d,
19
+ width: isActive ? xScale(_x2) - xGet(d) : 0,
20
+ };
21
+ });
22
+
23
+ return scaledData.map((d, i) => {
24
+ const x1 = xGet(d);
25
+ const y = yGet(d);
26
+
27
+ return (
28
+ <rect
29
+ key={`gantt-${i}`}
30
+ x={x1}
31
+ y={y}
32
+ width={d.width}
33
+ height={yScale.bandwidth()}
34
+ fill={zGet(d) || "lightgray"}
35
+ className={`gantt-block ${currentStep > -1 && highlight[currentStep].includes(i) ? "current" : ""}`}
36
+ ></rect>
37
+ );
38
+ });
39
+ }
@@ -0,0 +1,55 @@
1
+ import { useContext } from "react";
2
+ import { Context } from "../ReactCake/";
3
+ import { type ContextValue } from "../ReactCake/types";
4
+
5
+ interface OrdinalKeyProps {
6
+ shape?: "square" | "circle" | "line";
7
+ capitalise?: boolean;
8
+ format?: ((s: string | number) => string) | Record<string | number, string>;
9
+ align?: "start" | "center" | "end";
10
+ sort?: string[];
11
+ }
12
+
13
+ function cap(s: string) {
14
+ return String(s).replace(/^\w/, (d) => d.toUpperCase());
15
+ }
16
+
17
+ export default function OrdinalKey({
18
+ shape = "square",
19
+ align = "start",
20
+ sort = [],
21
+ format,
22
+ capitalise = true,
23
+ }: OrdinalKeyProps) {
24
+ const { zDomain, zScale } = useContext<ContextValue>(Context);
25
+
26
+ function displayName(val: string | number): string {
27
+ if (format) {
28
+ return typeof format === "function" ? format(val) : format[val] || val;
29
+ }
30
+ return capitalise === true ? cap(String(val)) : val;
31
+ }
32
+
33
+ return (
34
+ <div className="ordinal-key" style={{ justifyContent: align === "end" ? "flex-end" : align }}>
35
+ {zDomain
36
+ .sort((a, b) => sort.indexOf(b.kind) - sort.indexOf(a.kind))
37
+ .map((d) => {
38
+ return (
39
+ <div className="key-item" key={d}>
40
+ <div
41
+ className={`chip chip__${shape}`}
42
+ style={{
43
+ background:
44
+ shape === `line`
45
+ ? `linear-gradient(-45deg, var(--color-pageFill) 40%, ${zScale(d)} 41%, ${zScale(d)} 59%, var(--color-pageFill) 60%)`
46
+ : zScale(d),
47
+ }}
48
+ ></div>
49
+ <p className="name">{displayName(d)}</p>
50
+ </div>
51
+ );
52
+ })}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,47 @@
1
+ .ordinal-key {
2
+ display: flex;
3
+ flex-flow: column;
4
+ color: var(--o3-color-palette-black-80);
5
+ font-family: var(--o3-font-family-metric);
6
+ gap: 0.5em;
7
+ background: var(--o3-color-palette-paper);
8
+ box-shadow: 0 0 10px 10px var(--o3-color-palette-paper);
9
+
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+
15
+ .key-item {
16
+ display: flex;
17
+ gap: 0.1em;
18
+ align-items: center;
19
+ }
20
+
21
+ .chip {
22
+ display: inline-flex;
23
+ position: relative;
24
+ width: 12px;
25
+ height: 12px;
26
+ }
27
+
28
+ .chip__circle {
29
+ border-radius: 50%;
30
+ }
31
+
32
+ .chip__line:after {
33
+ content: "";
34
+ position: absolute;
35
+ border-width: 3px;
36
+ width: 14px;
37
+ transform: rotate(-45deg);
38
+ transform-origin: 14px 5px;
39
+ }
40
+
41
+ .name {
42
+ display: inline-block;
43
+ font-size: var(--o3-font-size-negative-2);
44
+ margin-left: 0.2em;
45
+ line-height: 1;
46
+ }
47
+ }
@@ -26,6 +26,15 @@
26
26
  width: 100%;
27
27
  padding: 2rem 0;
28
28
  font-family: sans-serif;
29
+ --blue-70: hsl(212, 70%, 26%);
30
+ --blue-50: hsl(212, 70%, 36%);
31
+ --blue-30: hsl(212, 50%, 76%);
32
+ --blue-10: hsl(212, 40%, 97%);
33
+ --ms--1: .8rem
34
+ }
35
+
36
+ a, a:visited {
37
+ color: var(--blue-70);
29
38
  }
30
39
 
31
40
  .content {
@@ -35,8 +35,6 @@ const data = [
35
35
  {year: 2026, value: 52}
36
36
  ];
37
37
 
38
- const xKey = "date";
39
- const yKey = "value";
40
38
  const xScale = scaleTime();
41
39
  const xScale = scaleBand();
42
40
 
@@ -11,4 +11,4 @@
11
11
  "jsx": "react-jsx",
12
12
  "jsxImportSource": "react"
13
13
  }
14
- }
14
+ }
@@ -1 +0,0 @@
1
- // TKTK