Skip to content

Commit d64f41c

Browse files
authored
feat(blocks) add new chart area step block (#226)
1 parent eef01b6 commit d64f41c

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

app/blocks/charts/charts.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Users } from "lucide-react";
22

33
import { ChartExample } from "@/components/ui/8bit/blocks/chart";
4+
import ChartAreaStep from "@/components/ui/8bit/blocks/chart-area-step";
45
import ChartBarMultiple from "@/components/ui/8bit/blocks/chart-bar";
56
import {
67
Card,
@@ -70,6 +71,34 @@ export default function ChartsBlocks() {
7071
</CardContent>
7172
</Card>
7273
</div>
74+
75+
<div className="flex flex-col gap-4 border rounded-lg p-4 min-h-[450px]">
76+
<div className="flex flex-col md:flex-row gap-2 items-center justify-between">
77+
<h2 className="text-sm text-muted-foreground sm:pl-3">
78+
A step area chart
79+
</h2>
80+
81+
<div className="flex flex-col md:flex-row items-center gap-2">
82+
<CopyCommandButton
83+
command="npx shadcn@latest add 8bit-chart-area-step"
84+
copyCommand={`pnpm dlx shadcn@canary add ${process.env.NEXT_PUBLIC_BASE_URL}/r/8bit-chart-area-step.json`}
85+
/>
86+
<OpenInV0Button name="8bit-chart-area-step" className="w-fit" />
87+
</div>
88+
</div>
89+
90+
<Card className="w-full md:w-[600px] mx-auto">
91+
<CardHeader className="flex flex-row items-center justify-between">
92+
<CardTitle className="text-sm font-medium">
93+
Total visitors in the last 6 months
94+
</CardTitle>
95+
<Users className="size-4 text-muted-foreground" />
96+
</CardHeader>
97+
<CardContent>
98+
<ChartAreaStep />
99+
</CardContent>
100+
</Card>
101+
</div>
73102
</div>
74103
);
75104
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client";
2+
3+
import { Activity } from "lucide-react";
4+
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
5+
6+
import {
7+
ChartConfig,
8+
ChartContainer,
9+
ChartTooltip,
10+
ChartTooltipContent,
11+
} from "@/components/ui/8bit/chart";
12+
13+
export const description = "A step area chart";
14+
15+
const chartData = [
16+
{ month: "January", desktop: 99 },
17+
{ month: "February", desktop: 204 },
18+
{ month: "March", desktop: 180 },
19+
{ month: "April", desktop: 120 },
20+
{ month: "May", desktop: 180 },
21+
{ month: "June", desktop: 42 },
22+
];
23+
24+
const chartConfig = {
25+
desktop: {
26+
label: "Desktop",
27+
color: "var(--chart-1)",
28+
icon: Activity,
29+
},
30+
} satisfies ChartConfig;
31+
32+
export default function ChartAreaStep() {
33+
return (
34+
<ChartContainer config={chartConfig}>
35+
<AreaChart
36+
accessibilityLayer
37+
data={chartData}
38+
margin={{
39+
left: 12,
40+
right: 12,
41+
}}
42+
>
43+
<CartesianGrid vertical={false} />
44+
<XAxis
45+
dataKey="month"
46+
tickLine={false}
47+
axisLine={false}
48+
tickMargin={8}
49+
tickFormatter={(value) => value.slice(0, 3)}
50+
/>
51+
<ChartTooltip
52+
cursor={false}
53+
content={<ChartTooltipContent hideLabel />}
54+
/>
55+
<Area
56+
dataKey="desktop"
57+
type="step"
58+
fill="var(--color-desktop)"
59+
stroke="var(--color-desktop)"
60+
activeDot={{
61+
fill: "var(--chart-active-dot)",
62+
}}
63+
/>
64+
</AreaChart>
65+
</ChartContainer>
66+
);
67+
}

public/r/8bit-chart-area-step.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "8bit-chart-area-step",
4+
"type": "registry:component",
5+
"title": "8-bit Chart Area Step",
6+
"description": "A simple 8-bit chart area step component",
7+
"registryDependencies": [],
8+
"files": [
9+
{
10+
"path": "components/ui/8bit/blocks/chart-area-step.tsx",
11+
"content": "\"use client\";\n\nimport { Activity } from \"lucide-react\";\nimport { Area, AreaChart, CartesianGrid, XAxis } from \"recharts\";\n\nimport {\n ChartConfig,\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n} from \"@/components/ui/8bit/chart\";\n\nexport const description = \"A step area chart\";\n\nconst chartData = [\n { month: \"January\", desktop: 99 },\n { month: \"February\", desktop: 204 },\n { month: \"March\", desktop: 180 },\n { month: \"April\", desktop: 120 },\n { month: \"May\", desktop: 180 },\n { month: \"June\", desktop: 42 },\n];\n\nconst chartConfig = {\n desktop: {\n label: \"Desktop\",\n color: \"var(--chart-1)\",\n icon: Activity,\n },\n} satisfies ChartConfig;\n\nexport default function ChartAreaStep() {\n return (\n <ChartContainer config={chartConfig}>\n <AreaChart\n accessibilityLayer\n data={chartData}\n margin={{\n left: 12,\n right: 12,\n }}\n >\n <CartesianGrid vertical={false} />\n <XAxis\n dataKey=\"month\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n tickFormatter={(value) => value.slice(0, 3)}\n />\n <ChartTooltip\n cursor={false}\n content={<ChartTooltipContent hideLabel />}\n />\n <Area\n dataKey=\"desktop\"\n type=\"step\"\n fill=\"var(--color-desktop)\"\n stroke=\"var(--color-desktop)\"\n activeDot={{\n fill: \"var(--chart-active-dot)\",\n }}\n />\n </AreaChart>\n </ChartContainer>\n );\n}\n",
12+
"type": "registry:component",
13+
"target": "components/ui/8bit/blocks/chart-area-step.tsx"
14+
},
15+
{
16+
"path": "components/ui/8bit/chart.tsx",
17+
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport * as RechartsPrimitive from \"recharts\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\nexport type ChartConfig = {\n [k in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & (\n | { color?: string; theme?: never }\n | { color?: never; theme: Record<keyof typeof THEMES, string> }\n );\n};\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error(\"useChart must be used within a <ChartContainer />\");\n }\n\n return context;\n}\n\nfunction ChartContainer({\n id,\n className,\n children,\n config,\n ...props\n}: React.ComponentProps<\"div\"> & {\n config: ChartConfig;\n children: React.ComponentProps<\n typeof RechartsPrimitive.ResponsiveContainer\n >[\"children\"];\n}) {\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`;\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n className\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <RechartsPrimitive.ResponsiveContainer>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(\n ([, config]) => config.theme || config.color\n );\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n <style\n dangerouslySetInnerHTML={{\n __html: Object.entries(THEMES)\n .map(\n ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color =\n itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join(\"\\n\")}\n}\n`\n )\n .join(\"\\n\"),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n active,\n payload,\n className,\n indicator = \"dot\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n React.ComponentProps<\"div\"> & {\n hideLabel?: boolean;\n hideIndicator?: boolean;\n indicator?: \"line\" | \"dot\" | \"dashed\";\n nameKey?: string;\n labelKey?: string;\n }) {\n const { config } = useChart();\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const value =\n !labelKey && typeof label === \"string\"\n ? config[label as keyof typeof config]?.label || label\n : itemConfig?.label;\n\n if (labelFormatter) {\n return (\n <div className={cn(\"font-medium\", labelClassName)}>\n {labelFormatter(value, payload)}\n </div>\n );\n }\n\n if (!value) {\n return null;\n }\n\n return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n return (\n <div\n className={cn(\n \"relative border-y-6 border-foreground dark:border-ring bg-background grid min-w-[8rem] items-start gap-1.5 px-2.5 py-1.5 text-xs shadow-xl\",\n className\n )}\n >\n <div\n className=\"absolute inset-0 border-x-6 -mx-1.5 border-foreground dark:border-ring pointer-events-none\"\n aria-hidden=\"true\"\n />\n {!nestLabel ? tooltipLabel : null}\n <div className=\"grid gap-1.5\">\n {payload.map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor = color || item.payload.fill || item.color;\n\n return (\n <div\n key={item.dataKey}\n className={cn(\n \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n indicator === \"dot\" && \"items-center\"\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n <div\n className={cn(\n \"shrink-0 border-(--color-border) bg-(--color-bg)\",\n {\n \"h-2.5 w-2.5\": indicator === \"dot\",\n \"w-1\": indicator === \"line\",\n \"w-0 border-[1.5px] border-dashed bg-transparent\":\n indicator === \"dashed\",\n \"my-0.5\": nestLabel && indicator === \"dashed\",\n }\n )}\n style={\n {\n \"--color-bg\": indicatorColor,\n \"--color-border\": indicatorColor,\n } as React.CSSProperties\n }\n />\n )\n )}\n <div\n className={cn(\n \"flex flex-1 justify-between leading-none\",\n nestLabel ? \"items-end\" : \"items-center\"\n )}\n >\n <div className=\"grid gap-1.5\">\n {nestLabel ? tooltipLabel : null}\n <span className=\"text-muted-foreground\">\n {itemConfig?.label || item.name}\n </span>\n {item.value && (\n <span className=\"text-foreground font-medium tabular-nums\">\n {item.value.toLocaleString()}\n </span>\n )}\n </div>\n </div>\n </>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = \"bottom\",\n nameKey,\n}: React.ComponentProps<\"div\"> &\n Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n hideIcon?: boolean;\n nameKey?: string;\n }) {\n const { config } = useChart();\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n <div\n className={cn(\n \"flex items-center justify-center gap-4\",\n verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n className\n )}\n >\n {payload.map((item) => {\n const key = `${nameKey || item.dataKey || \"value\"}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n return (\n <div\n key={item.value}\n className={cn(\n \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\"\n )}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2 w-2 shrink-0\"\n style={{\n backgroundColor: item.color,\n }}\n />\n )}\n {itemConfig?.label}\n </div>\n );\n })}\n </div>\n );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string\n) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined;\n }\n\n const payloadPayload =\n \"payload\" in payload &&\n typeof payload.payload === \"object\" &&\n payload.payload !== null\n ? payload.payload\n : undefined;\n\n let configLabelKey: string = key;\n\n if (\n key in payload &&\n typeof payload[key as keyof typeof payload] === \"string\"\n ) {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n ) {\n configLabelKey = payloadPayload[\n key as keyof typeof payloadPayload\n ] as string;\n }\n\n return configLabelKey in config\n ? config[configLabelKey]\n : config[key as keyof typeof config];\n}\n\nexport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n};\n",
18+
"type": "registry:component",
19+
"target": "components/ui/8bit/chart.tsx"
20+
},
21+
{
22+
"path": "components/ui/8bit/styles/retro.css",
23+
"content": "@import url(\"https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap\");\n\n.retro {\n font-family:\n \"Press Start 2P\",\n system-ui,\n -apple-system,\n sans-serif;\n line-height: 1.5;\n letter-spacing: 0.5px;\n}\n",
24+
"type": "registry:component",
25+
"target": "components/ui/8bit/styles/retro.css"
26+
}
27+
]
28+
}

registry.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,30 @@
958958
}
959959
]
960960
},
961+
{
962+
"name": "8bit-chart-area-step",
963+
"type": "registry:component",
964+
"title": "8-bit Chart Area Step",
965+
"description": "A simple 8-bit chart area step component",
966+
"registryDependencies": [],
967+
"files": [
968+
{
969+
"path": "components/ui/8bit/blocks/chart-area-step.tsx",
970+
"type": "registry:component",
971+
"target": "components/ui/8bit/blocks/chart-area-step.tsx"
972+
},
973+
{
974+
"path": "components/ui/8bit/chart.tsx",
975+
"type": "registry:component",
976+
"target": "components/ui/8bit/chart.tsx"
977+
},
978+
{
979+
"path": "components/ui/8bit/styles/retro.css",
980+
"type": "registry:component",
981+
"target": "components/ui/8bit/styles/retro.css"
982+
}
983+
]
984+
},
961985
{
962986
"name": "8bit-toggle-group",
963987
"type": "registry:component",

0 commit comments

Comments
 (0)