-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
feat(chart) : Add Scatter and Bubble Chart Components #9151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
4cd3559
5664571
b0aa8b4
a7b416d
e51edea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,44 @@ export function ChartCopyButton({ | |
| }, 2000) | ||
| }, [hasCopied]) | ||
|
|
||
| const copyToClipboard = async (text: string) => { | ||
|
||
| try { | ||
|
||
| // Try using the modern clipboard API first | ||
| if (navigator.clipboard && window.isSecureContext) { | ||
| await navigator.clipboard.writeText(text) | ||
| } else { | ||
| // Fallback for older browsers or non-secure contexts | ||
| const textArea = document.createElement("textarea") | ||
| textArea.value = text | ||
| textArea.style.position = "fixed" | ||
| textArea.style.left = "-999999px" | ||
| textArea.style.top = "-999999px" | ||
| document.body.appendChild(textArea) | ||
| textArea.focus() | ||
| textArea.select() | ||
|
|
||
| try { | ||
| document.execCommand("copy") | ||
|
||
| textArea.remove() | ||
| } catch (err) { | ||
| console.error("Fallback: Could not copy text", err) | ||
| textArea.remove() | ||
| throw err | ||
| } | ||
| } | ||
|
|
||
| trackEvent({ | ||
| name: event, | ||
| properties: { | ||
| name, | ||
| }, | ||
| }) | ||
| setHasCopied(true) | ||
| } catch (err) { | ||
| console.error("Failed to copy text: ", err) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
|
|
@@ -41,16 +79,7 @@ export function ChartCopyButton({ | |
| "[&_svg]-h-3.5 h-7 w-7 rounded-[6px] [&_svg]:w-3.5", | ||
| className | ||
| )} | ||
| onClick={() => { | ||
| navigator.clipboard.writeText(code) | ||
| trackEvent({ | ||
| name: event, | ||
| properties: { | ||
| name, | ||
| }, | ||
| }) | ||
| setHasCopied(true) | ||
| }} | ||
| onClick={() => copyToClipboard(code)} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for this refactoring, please could you add more details about the older-browsers and the non-secure contexts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the feedback! I've added detailed documentation explaining both paths: Modern path (
Fallback path (
I've added inline comments explaining the use cases for each path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, However execCommand is deprecated. |
||
| {...props} | ||
| > | ||
| <span className="sr-only">Copy</span> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,10 @@ const links = [ | |
| name: "Radial Charts", | ||
| href: "/charts/radial#charts", | ||
| }, | ||
| { | ||
| name: "Scatter Charts", | ||
| href: "/charts/scatter#charts", | ||
| }, | ||
| { | ||
| name: "Tooltips", | ||
| href: "/charts/tooltip#charts", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MEDIUM - Potentially fragile active link detection logic Description: Suggestion: Confidence: 60% |
||
|
|
||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "$schema": "https://ui.shadcn.com/schema/registry-item.json", | ||
| "name": "chart-scatter-bubble", | ||
| "registryDependencies": [ | ||
| "card", | ||
| "chart" | ||
| ], | ||
| "files": [ | ||
| { | ||
| "path": "registry/new-york-v4/charts/chart-scatter-bubble.tsx", | ||
| "content": "\"use client\"\n\nimport { TrendingUp } from \"lucide-react\"\nimport {\n CartesianGrid,\n Scatter,\n ScatterChart,\n XAxis,\n YAxis,\n ZAxis,\n} from \"recharts\"\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n type ChartConfig,\n} from \"@/registry/new-york-v4/ui/chart\"\n\nexport const description = \"A bubble chart with three dimensions\"\n\nconst chartData = [\n { x: 10, y: 20, z: 200 },\n { x: 30, y: 45, z: 300 },\n { x: 50, y: 28, z: 150 },\n { x: 70, y: 60, z: 400 },\n { x: 90, y: 82, z: 500 },\n { x: 110, y: 55, z: 250 },\n { x: 130, y: 72, z: 350 },\n { x: 150, y: 95, z: 600 },\n]\n\nconst chartConfig = {\n x: {\n label: \"X Value\",\n color: \"var(--chart-1)\",\n },\n y: {\n label: \"Y Value\",\n },\n z: {\n label: \"Size\",\n },\n} satisfies ChartConfig\n\nexport function ChartScatterBubble() {\n return (\n <Card>\n <CardHeader>\n <CardTitle>Bubble Chart</CardTitle>\n <CardDescription>\n Showing three-dimensional data relationships\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ChartContainer config={chartConfig}>\n <ScatterChart\n accessibilityLayer\n margin={{\n left: 12,\n right: 12,\n top: 12,\n bottom: 12,\n }}\n >\n <CartesianGrid strokeDasharray=\"3 3\" />\n <XAxis\n type=\"number\"\n dataKey=\"x\"\n name=\"X Value\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <YAxis\n type=\"number\"\n dataKey=\"y\"\n name=\"Y Value\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <ZAxis type=\"number\" dataKey=\"z\" range={[50, 250]} name=\"Size\" />\n <ChartTooltip\n cursor={{ strokeDasharray: \"3 3\" }}\n content={<ChartTooltipContent />}\n />\n <Scatter data={chartData} fill=\"var(--color-x)\" shape=\"circle\" />\n </ScatterChart>\n </ChartContainer>\n </CardContent>\n <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n <div className=\"flex gap-2 leading-none font-medium\">\n Bubble size represents the third dimension{\" \"}\n <TrendingUp className=\"h-4 w-4\" />\n </div>\n <div className=\"text-muted-foreground leading-none\">\n Displaying three-dimensional data in a two-dimensional chart\n </div>\n </CardFooter>\n </Card>\n )\n}\n", | ||
| "type": "registry:block" | ||
| } | ||
| ], | ||
| "categories": [ | ||
| "charts", | ||
| "charts-scatter" | ||
| ], | ||
| "type": "registry:block" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "$schema": "https://ui.shadcn.com/schema/registry-item.json", | ||
| "name": "chart-scatter-default", | ||
| "registryDependencies": [ | ||
| "card", | ||
| "chart" | ||
| ], | ||
| "files": [ | ||
| { | ||
| "path": "registry/new-york-v4/charts/chart-scatter-default.tsx", | ||
| "content": "\"use client\"\n\nimport { TrendingUp } from \"lucide-react\"\nimport { CartesianGrid, Scatter, ScatterChart, XAxis, YAxis } from \"recharts\"\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n type ChartConfig,\n} from \"@/registry/new-york-v4/ui/chart\"\n\nexport const description = \"A scatter chart\"\n\nconst chartData = [\n { x: 10, y: 20 },\n { x: 30, y: 45 },\n { x: 50, y: 28 },\n { x: 70, y: 60 },\n { x: 90, y: 82 },\n { x: 110, y: 55 },\n { x: 130, y: 72 },\n { x: 150, y: 95 },\n]\n\nconst chartConfig = {\n x: {\n label: \"X Value\",\n color: \"var(--chart-1)\",\n },\n y: {\n label: \"Y Value\",\n },\n} satisfies ChartConfig\n\nexport function ChartScatterDefault() {\n return (\n <Card>\n <CardHeader>\n <CardTitle>Scatter Chart</CardTitle>\n <CardDescription>\n Showing relationship between two variables\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ChartContainer config={chartConfig}>\n <ScatterChart\n accessibilityLayer\n margin={{\n left: 12,\n right: 12,\n }}\n >\n <CartesianGrid strokeDasharray=\"3 3\" />\n <XAxis\n type=\"number\"\n dataKey=\"x\"\n name=\"X Value\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <YAxis\n type=\"number\"\n dataKey=\"y\"\n name=\"Y Value\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <ChartTooltip\n cursor={{ strokeDasharray: \"3 3\" }}\n content={<ChartTooltipContent />}\n />\n <Scatter data={chartData} fill=\"var(--color-x)\" shape=\"circle\" />\n </ScatterChart>\n </ChartContainer>\n </CardContent>\n <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n <div className=\"flex gap-2 leading-none font-medium\">\n Showing positive correlation <TrendingUp className=\"h-4 w-4\" />\n </div>\n <div className=\"text-muted-foreground leading-none\">\n Displaying the relationship between X and Y values\n </div>\n </CardFooter>\n </Card>\n )\n}\n", | ||
| "type": "registry:block" | ||
| } | ||
| ], | ||
| "categories": [ | ||
| "charts", | ||
| "charts-scatter" | ||
| ], | ||
| "type": "registry:block" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "$schema": "https://ui.shadcn.com/schema/registry-item.json", | ||
| "name": "chart-scatter-label", | ||
| "registryDependencies": [ | ||
| "card", | ||
| "chart" | ||
| ], | ||
| "files": [ | ||
| { | ||
| "path": "registry/new-york-v4/charts/chart-scatter-label.tsx", | ||
| "content": "\"use client\"\n\nimport { TrendingUp } from \"lucide-react\"\nimport {\n CartesianGrid,\n LabelList,\n Scatter,\n ScatterChart,\n XAxis,\n YAxis,\n} from \"recharts\"\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n type ChartConfig,\n} from \"@/registry/new-york-v4/ui/chart\"\n\nexport const description = \"A scatter chart with labels\"\n\nconst chartData = [\n { x: 10, y: 20, label: \"A\" },\n { x: 30, y: 45, label: \"B\" },\n { x: 50, y: 28, label: \"C\" },\n { x: 70, y: 60, label: \"D\" },\n { x: 90, y: 82, label: \"E\" },\n { x: 110, y: 55, label: \"F\" },\n { x: 130, y: 72, label: \"G\" },\n]\n\nconst chartConfig = {\n x: {\n label: \"X Value\",\n color: \"var(--chart-1)\",\n },\n y: {\n label: \"Y Value\",\n },\n} satisfies ChartConfig\n\nexport function ChartScatterLabel() {\n return (\n <Card>\n <CardHeader>\n <CardTitle>Scatter Chart - Label</CardTitle>\n <CardDescription>With data point labels</CardDescription>\n </CardHeader>\n <CardContent>\n <ChartContainer config={chartConfig}>\n <ScatterChart\n accessibilityLayer\n margin={{\n left: 12,\n right: 12,\n top: 12,\n bottom: 12,\n }}\n >\n <CartesianGrid strokeDasharray=\"3 3\" />\n <XAxis\n type=\"number\"\n dataKey=\"x\"\n name=\"X Value\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <YAxis\n type=\"number\"\n dataKey=\"y\"\n name=\"Y Value\"\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <ChartTooltip\n cursor={{ strokeDasharray: \"3 3\" }}\n content={<ChartTooltipContent />}\n />\n <Scatter data={chartData} fill=\"var(--color-x)\" shape=\"circle\">\n <LabelList\n dataKey=\"label\"\n position=\"top\"\n offset={12}\n className=\"fill-foreground\"\n fontSize={12}\n />\n </Scatter>\n </ScatterChart>\n </ChartContainer>\n </CardContent>\n <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n <div className=\"flex gap-2 leading-none font-medium\">\n Labels help identify individual data points{\" \"}\n <TrendingUp className=\"h-4 w-4\" />\n </div>\n <div className=\"text-muted-foreground leading-none\">\n Each point is labeled for easy identification\n </div>\n </CardFooter>\n </Card>\n )\n}\n", | ||
| "type": "registry:block" | ||
| } | ||
| ], | ||
| "categories": [ | ||
| "charts", | ||
| "charts-scatter" | ||
| ], | ||
| "type": "registry:block" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "$schema": "https://ui.shadcn.com/schema/registry-item.json", | ||
| "name": "chart-scatter-legend", | ||
| "registryDependencies": [ | ||
| "card", | ||
| "chart" | ||
| ], | ||
| "files": [ | ||
| { | ||
| "path": "registry/new-york-v4/charts/chart-scatter-legend.tsx", | ||
| "content": "\"use client\"\n\nimport { TrendingUp } from \"lucide-react\"\nimport {\n CartesianGrid,\n Scatter,\n ScatterChart,\n XAxis,\n YAxis,\n ZAxis,\n} from \"recharts\"\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n ChartContainer,\n ChartLegend,\n ChartLegendContent,\n ChartTooltip,\n ChartTooltipContent,\n type ChartConfig,\n} from \"@/registry/new-york-v4/ui/chart\"\n\nexport const description = \"A bubble chart with multiple datasets and legend\"\n\nconst chartDataA = [\n { x: 150, y: 220, z: 180 },\n { x: 200, y: 280, z: 220 },\n { x: 250, y: 320, z: 200 },\n { x: 180, y: 350, z: 240 },\n]\n\nconst chartDataB = [\n { x: 350, y: 180, z: 160 },\n { x: 400, y: 240, z: 190 },\n { x: 450, y: 200, z: 210 },\n { x: 380, y: 280, z: 180 },\n]\n\nconst chartDataC = [\n { x: 280, y: 450, z: 250 },\n { x: 350, y: 520, z: 280 },\n { x: 420, y: 480, z: 260 },\n { x: 320, y: 580, z: 300 },\n]\n\nconst chartConfig = {\n seriesA: {\n label: \"Product A\",\n color: \"var(--chart-1)\",\n icon: () => (\n <svg viewBox=\"0 0 12 12\" className=\"h-3 w-3\">\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill=\"var(--chart-1)\" />\n </svg>\n ),\n },\n seriesB: {\n label: \"Product B\",\n color: \"var(--chart-2)\",\n icon: () => (\n <svg viewBox=\"0 0 12 12\" className=\"h-3 w-3\">\n <path d=\"M6 1 L11 11 L1 11 Z\" fill=\"var(--chart-2)\" />\n </svg>\n ),\n },\n seriesC: {\n label: \"Product C\",\n color: \"var(--chart-3)\",\n icon: () => (\n <svg viewBox=\"0 0 12 12\" className=\"h-3 w-3\">\n <path d=\"M6 1 L10 6 L6 11 L2 6 Z\" fill=\"var(--chart-3)\" />\n </svg>\n ),\n },\n} satisfies ChartConfig\n\nexport function ChartScatterLegend() {\n return (\n <Card>\n <CardHeader>\n <CardTitle>Bubble Chart - Legend</CardTitle>\n <CardDescription>Product performance comparison</CardDescription>\n </CardHeader>\n <CardContent>\n <ChartContainer config={chartConfig}>\n <ScatterChart\n accessibilityLayer\n margin={{\n left: 12,\n right: 12,\n top: 12,\n bottom: 12,\n }}\n >\n <CartesianGrid strokeDasharray=\"3 3\" />\n <XAxis\n type=\"number\"\n dataKey=\"x\"\n name=\"Sales Volume\"\n domain={[100, 500]}\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <YAxis\n type=\"number\"\n dataKey=\"y\"\n name=\"Revenue\"\n domain={[100, 650]}\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n />\n <ZAxis\n type=\"number\"\n dataKey=\"z\"\n range={[30, 120]}\n name=\"Market Share\"\n />\n <ChartTooltip\n cursor={{ strokeDasharray: \"3 3\" }}\n content={<ChartTooltipContent />}\n />\n <ChartLegend content={<ChartLegendContent />} />\n <Scatter\n data={chartDataA}\n fill=\"var(--color-seriesA)\"\n name=\"seriesA\"\n shape=\"circle\"\n />\n <Scatter\n data={chartDataB}\n fill=\"var(--color-seriesB)\"\n name=\"seriesB\"\n shape=\"triangle\"\n />\n <Scatter\n data={chartDataC}\n fill=\"var(--color-seriesC)\"\n name=\"seriesC\"\n shape=\"diamond\"\n />\n </ScatterChart>\n </ChartContainer>\n </CardContent>\n <CardFooter className=\"flex-col items-start gap-2 text-sm\">\n <div className=\"flex gap-2 leading-none font-medium\">\n Three product categories analyzed <TrendingUp className=\"h-4 w-4\" />\n </div>\n <div className=\"text-muted-foreground leading-none\">\n Bubble size represents market share percentage\n </div>\n </CardFooter>\n </Card>\n )\n}\n", | ||
| "type": "registry:block" | ||
| } | ||
| ], | ||
| "categories": [ | ||
| "charts", | ||
| "charts-scatter" | ||
| ], | ||
| "type": "registry:block" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We changed the array’s initial length from
12to9, but there’s no comment explaining the reason for this change.If the value is meant to be dynamic, we could derive it from
chartList.lengthinstead of hard-coding it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! The change from 12 to 9 was intentional to better match our scatter chart collection.
Context:
This PR adds 6 new scatter chart variants
The page previously rendered 12 total slots (actual charts + empty placeholders)
With only 6 scatter charts, this created 6 empty placeholder boxes, which looked sparse
Why 9:
Changing to 9 gives us a cleaner 3×3 grid on larger screens
Shows 6 actual scatter charts + 3 empty placeholders
The empty placeholders indicate room for future chart additions
Your suggestion about deriving from chartList.length:
I considered this, but the current implementation intentionally shows a few empty slots to indicate that more charts can be added. However, I'm open to either approach:
Keep it at 9 (current) - Shows intentional space for future charts
Use chartList.length - Dynamically adjusts, no empty slots
Use Math.max(chartList.length, 9) Shows charts + minimum 9 slots
Which approach would you prefer for consistency across the codebase?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You hit it!
I noticed before these empty zones and slots and I was wondering why they were there.
The core team can decide on that.