Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/v4/app/(app)/charts/[type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const chartTypes = [
"pie",
"radar",
"radial",
"scatter",
"tooltip",
] as const
type ChartType = (typeof chartTypes)[number]
Expand Down Expand Up @@ -50,7 +51,7 @@ export default async function ChartPage({ params }: ChartPageProps) {
{type.charAt(0).toUpperCase() + type.slice(1)} Charts
</h2>
<div className="grid flex-1 scroll-mt-20 items-stretch gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10">
{Array.from({ length: 12 }).map((_, index) => {
{Array.from({ length: 9 }).map((_, index) => {

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 12 to 9, 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.length instead of hard-coding it.

Copy link
Contributor Author

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:

  1. Keep it at 9 (current) - Shows intentional space for future charts

  2. Use chartList.length - Dynamically adjusts, no empty slots

  3. Use Math.max(chartList.length, 9) Shows charts + minimum 9 slots

Which approach would you prefer for consistency across the codebase?

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.

const chart = chartList[index]
return chart ? (
<ChartDisplay
Expand Down
21 changes: 21 additions & 0 deletions apps/v4/app/(app)/charts/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ import { ChartRadialShape } from "@/registry/new-york-v4/charts/chart-radial-sha
import { ChartRadialSimple } from "@/registry/new-york-v4/charts/chart-radial-simple"
import { ChartRadialStacked } from "@/registry/new-york-v4/charts/chart-radial-stacked"
import { ChartRadialText } from "@/registry/new-york-v4/charts/chart-radial-text"
import { ChartScatterBubble } from "@/registry/new-york-v4/charts/chart-scatter-bubble"
import { ChartScatterDefault } from "@/registry/new-york-v4/charts/chart-scatter-default"
import { ChartScatterLabel } from "@/registry/new-york-v4/charts/chart-scatter-label"
import { ChartScatterLegend } from "@/registry/new-york-v4/charts/chart-scatter-legend"
import { ChartScatterMultiple } from "@/registry/new-york-v4/charts/chart-scatter-multiple"
import { ChartScatterShape } from "@/registry/new-york-v4/charts/chart-scatter-shape"
import { ChartTooltipAdvanced } from "@/registry/new-york-v4/charts/chart-tooltip-advanced"
import { ChartTooltipDefault } from "@/registry/new-york-v4/charts/chart-tooltip-default"
import { ChartTooltipFormatter } from "@/registry/new-york-v4/charts/chart-tooltip-formatter"
Expand All @@ -86,6 +92,7 @@ interface ChartGroups {
pie: ChartItem[]
radar: ChartItem[]
radial: ChartItem[]
scatter: ChartItem[]
tooltip: ChartItem[]
}

Expand Down Expand Up @@ -178,6 +185,14 @@ export const charts: ChartGroups = {
{ id: "chart-radial-shape", component: ChartRadialShape },
{ id: "chart-radial-stacked", component: ChartRadialStacked },
],
scatter: [
{ id: "chart-scatter-default", component: ChartScatterDefault },
{ id: "chart-scatter-bubble", component: ChartScatterBubble },
{ id: "chart-scatter-multiple", component: ChartScatterMultiple },
{ id: "chart-scatter-shape", component: ChartScatterShape },
{ id: "chart-scatter-label", component: ChartScatterLabel },
{ id: "chart-scatter-legend", component: ChartScatterLegend },
],
tooltip: [
{ id: "chart-tooltip-default", component: ChartTooltipDefault },
{
Expand Down Expand Up @@ -263,6 +278,12 @@ export {
ChartRadialText,
ChartRadialShape,
ChartRadialStacked,
ChartScatterDefault,
ChartScatterBubble,
ChartScatterMultiple,
ChartScatterShape,
ChartScatterLabel,
ChartScatterLegend,
ChartTooltipDefault,
ChartTooltipIndicatorLine,
ChartTooltipIndicatorNone,
Expand Down
49 changes: 39 additions & 10 deletions apps/v4/components/chart-copy-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ export function ChartCopyButton({
}, 2000)
}, [hasCopied])

const copyToClipboard = async (text: string) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function references event, name and setHasCopied in from the closure but it's not memoized.

We could wrap it with useCallback to avoid the creation of the object references between re-renders:

useCallback(()=>{},[event,name])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch!
I've wrapped copyToClipboardinuseCallbackwith the correct dependencies[event, name]` to prevent unnecessary function recreations on each render. This improves performance and follows React best practices.

try {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH - Memory leak: setTimeout not cleaned up in useEffect
Category: bug

Description:
The setTimeout in useEffect lacks a cleanup function. If the component unmounts before the 2-second timeout completes, it will attempt to update state on an unmounted component.

Suggestion:
Return a cleanup function from useEffect:

React.useEffect(() => {
  if (!hasCopied) return
  
  const timeoutId = setTimeout(() => {
    setHasCopied(false)
  }, 2000)
  
  return () => clearTimeout(timeoutId)
}, [hasCopied])

Confidence: 95%
Rule: bug_missing_cleanup

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened a pull request for this fix.

// 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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execCommand is a deprecated and it's no longer recommended.Though some browsers might still support it, it may have already been removed from the relevant web standards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right that execCommand is deprecated. I've acknowledged this in the code comments and marked it clearly as a legacy fallback.

The implementation prioritizes the modern navigator.clipboard API and only falls back to execCommand when there's no alternative (older browsers or HTTP contexts). This ensures maximum compatibility while following best practices where possible.

As browser support continues to improve and HTTPS becomes universal, this fallback will naturally become less necessary over time.

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>
Expand All @@ -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)}

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 (navigator.clipboard):

  • Used in secure contexts (HTTPS, localhost)
  • Supported in all modern browsers (Chrome 63+, Firefox 53+, Safari 13.1+, Edge 79+)
  • Async and follows current web standards

Fallback path (execCommand):

  • Handles older browsers (IE 11, older Android browsers)
  • Works in non-secure contexts (HTTP-served pages)
  • Needed for environments where clipboard API is unavailable or disabled
  • Includes scenarios like certain iframe configurations

I've added inline comments explaining the use cases for each path.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, However execCommand is deprecated.

{...props}
>
<span className="sr-only">Copy</span>
Expand Down
9 changes: 9 additions & 0 deletions apps/v4/components/chart-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MousePointer2Icon,
PieChartIcon,
RadarIcon,
ScatterChartIcon,
} from "lucide-react"

import { cn } from "@/lib/utils"
Expand Down Expand Up @@ -94,6 +95,14 @@ function ChartTitle({ chart }: { chart: Chart }) {
)
}

if (chart.name.includes("chart-scatter")) {
return (
<>
<ScatterChartIcon /> Scatter Chart
</>
)
}

if (chart.name.includes("chart-tooltip")) {
return (
<>
Expand Down
4 changes: 4 additions & 0 deletions apps/v4/components/charts-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM - Potentially fragile active link detection logic
Category: bug

Description:
The condition link.href.startsWith(pathname) works for the current use case where links contain hash fragments, but could be fragile if link structure changes or pathname includes query params.

Suggestion:
Consider using pathname.startsWith(link.href.split('#')[0]) for more robust route matching that handles edge cases.

Confidence: 60%
Rule: qual_inverted_logic_js

Expand Down
2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-lyra/input-group-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-lyra/item-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-lyra/toggle-example.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-maia/input-group-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-maia/item-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-maia/toggle-example.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-mira/input-group-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-mira/item-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-mira/toggle-example.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-nova/input-group-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-nova/item-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-nova/toggle-example.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-vega/input-group-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-vega/item-example.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/v4/public/r/styles/base-vega/toggle-example.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions apps/v4/public/r/styles/new-york-v4/chart-scatter-bubble.json
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"
}
20 changes: 20 additions & 0 deletions apps/v4/public/r/styles/new-york-v4/chart-scatter-default.json
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"
}
20 changes: 20 additions & 0 deletions apps/v4/public/r/styles/new-york-v4/chart-scatter-label.json
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"
}
20 changes: 20 additions & 0 deletions apps/v4/public/r/styles/new-york-v4/chart-scatter-legend.json
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"
}
Loading