{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "line-chart", "title": "Line Chart", "description": "A customizable line chart component with toggle options and Y-axis formatting.", "dependencies": [ "recharts", "lucide-react" ], "registryDependencies": [ "card", "chart", "button" ], "files": [ { "path": "registry/new-york/line-chart/line-chart.tsx", "content": "\"use client\"\n\nimport React, { useMemo, useState } from \"react\"\nimport { CartesianGrid, Line, LineChart, XAxis, YAxis } from \"recharts\"\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport {\n ChartConfig,\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n} from \"@/components/ui/chart\"\nimport { Button } from \"@/components/ui/button\"\nimport { Calendar, CalendarDays, GitCommitVertical } from \"lucide-react\"\n\ninterface ToggleOption {\n value: string\n label: string\n icon?: React.ReactNode\n}\n\ninterface LineChartComponentProps {\n title?: string\n description?: string\n data: Array>\n chartConfig: ChartConfig\n xAxisKey?: string\n yAxisConfig?: {\n domain?: [number, number] | \"auto\"\n padding?: number\n tickCount?: number\n formatType?: 'auto' | 'full' | 'compact' | 'currency' | 'percentage'\n customFormatter?: (value: number) => string\n tickFormatter?: (value: number) => string // Deprecated, use customFormatter instead\n }\n lines?: Array<{\n dataKey: string\n stroke?: string\n strokeWidth?: number\n type?: \"monotone\" | \"linear\" | \"step\" | \"stepBefore\" | \"stepAfter\"\n dot?: boolean\n }>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n dot?: boolean | ((props: any) => React.ReactElement)\n margin?: {\n top?: number\n right?: number\n bottom?: number\n left?: number\n }\n footerContent?: {\n mainText?: string\n subText?: string\n showTrending?: boolean\n trendingIcon?: React.ReactNode\n trendingColor?: string\n }\n toggleOptions?: {\n options: ToggleOption[]\n currentValue: string\n onChange: (value: string) => void\n position?: \"header-right\" | \"header-left\"\n }\n className?: string\n}\n\n//todo: ==== Format the y-axis value based on the format type ====\nconst formatYAxisValue = (value: number, formatType = 'auto') => {\n switch (formatType) {\n case 'percentage':\n return `${value}%`\n\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value)\n\n case 'full':\n return value.toLocaleString()\n\n case 'compact':\n case 'auto':\n default:\n if (value >= 1000000000) {\n return `${(value / 1000000000).toFixed(value % 1000000000 === 0 ? 0 : 1)}B`\n }\n if (value >= 1000000) {\n return `${(value / 1000000).toFixed(value % 1000000 === 0 ? 0 : 1)}M`\n }\n if (value >= 1000) {\n return `${(value / 1000).toFixed(value % 1000 === 0 ? 0 : 1)}K`\n }\n return value.toString()\n }\n}\n\n//todo: ==== Divide y-axĂ­ ticks based-on nice step size ====\nfunction getNiceStepSize(range: number, targetSteps: number): number {\n const rawStep = range / targetSteps\n const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)))\n const normalizedStep = rawStep / magnitude\n\n // Choose nice step sizes: 1, 2, 5, 10\n const niceStep = normalizedStep <= 1 ? 1\n : normalizedStep <= 2 ? 2\n : normalizedStep <= 5 ? 5\n : 10\n\n return niceStep * magnitude\n}\n\nconst calculateYAxisTicks = (domain: [number, number], tickCount: number, dataMax: number) => {\n const [min, max] = domain\n\n if (tickCount <= 1) return [min]\n\n const step = getNiceStepSize(max - min, tickCount - 1)\n const niceMin = Math.floor(min / step) * step\n\n // Generate ticks\n const ticks: number[] = []\n let currentTick = niceMin\n\n while (currentTick <= max && ticks.length < tickCount * 2) {\n if (currentTick >= min) ticks.push(currentTick)\n currentTick += step\n }\n\n // Auto-add extra tick if data max is too close to last tick\n const lastTick = ticks[ticks.length - 1]\n if (dataMax - lastTick > 0 && dataMax - lastTick < step * 0.1) {\n ticks.push(lastTick + step)\n }\n\n return ticks\n}\n\n//todo: ==== Calculate the y-axis domain with padding ====\nconst calculateNiceYDomain = (data: Array>, dataKeys: string[], padding = 0.1) => {\n let min = Infinity, max = -Infinity\n\n data.forEach(item => {\n dataKeys.forEach(key => {\n const value = typeof item[key] === 'number' ? item[key] as number : 0\n min = Math.min(min, value)\n max = Math.max(max, value)\n })\n })\n\n if (min === Infinity || max === -Infinity) return [0, 100]\n\n const range = max - min\n const paddedMin = Math.max(0, min - (range * padding))\n const paddedMax = max + (range * padding)\n\n // Smart rounding based on magnitude\n const getRoundingFactor = (value: number) =>\n value <= 100 ? 10\n : value <= 1000 ? 100\n : value <= 10000 ? 500\n : value <= 50000 ? 1000\n : 5000\n\n const niceMin = Math.floor(paddedMin / getRoundingFactor(paddedMin)) * getRoundingFactor(paddedMin)\n const niceMax = Math.ceil(paddedMax / getRoundingFactor(paddedMax)) * getRoundingFactor(paddedMax)\n\n return [niceMin, niceMax]\n}\n\nexport function LineChartComponent({\n title = \"Line Chart - Multiple\",\n description = \"January - June 2024\",\n data,\n chartConfig,\n xAxisKey = \"month\",\n yAxisConfig,\n lines,\n dot = false,\n margin = {\n left: 12,\n right: 12,\n },\n footerContent,\n toggleOptions,\n className\n}: LineChartComponentProps) {\n // Auto-generate lines from chartConfig if not provided\n const chartLines = lines || Object.keys(chartConfig).map(key => ({\n dataKey: key,\n stroke: chartConfig[key].color,\n strokeWidth: 2,\n type: \"monotone\" as const,\n dot\n }))\n\n // Calculate Y-axis domain\n const yDomain = useMemo(() => {\n if (yAxisConfig?.domain && yAxisConfig.domain !== \"auto\") {\n return yAxisConfig.domain\n }\n\n // Auto-calculate domain with padding\n const dataKeys = chartLines.map(line => line.dataKey)\n const padding = yAxisConfig?.padding || 0.15\n return calculateNiceYDomain(data, dataKeys, padding)\n }, [data, chartLines, yAxisConfig])\n\n // Calculate Y-axis ticks for even spacing\n const yAxisTicks = useMemo(() => {\n const tickCount = yAxisConfig?.tickCount || 6\n const dataKeys = chartLines.map(line => line.dataKey)\n\n const dataMax = Math.max(...data.flatMap(item =>\n dataKeys.map(key => typeof item[key] === 'number' ? item[key] as number : 0)\n ))\n\n return calculateYAxisTicks(yDomain as [number, number], tickCount, dataMax)\n }, [yDomain, yAxisConfig, data, chartLines])\n\n // Create Y-axis tick formatter\n const yAxisTickFormatter = useMemo(() => {\n return yAxisConfig?.customFormatter\n || yAxisConfig?.tickFormatter\n || ((value: number) => formatYAxisValue(value, yAxisConfig?.formatType))\n }, [yAxisConfig])\n\n return (\n \n \n
\n
\n {title}\n {description}\n
\n\n {toggleOptions && (\n
\n {toggleOptions.options.map((option) => (\n toggleOptions.onChange(option.value)}\n className=\"flex items-center gap-2\"\n >\n {option.icon}\n {option.label}\n \n ))}\n
\n )}\n
\n
\n\n \n
\n \n \n \n value.slice(0, 3)}\n />\n \n } />\n {chartLines.map((line) => (\n \n ))}\n \n \n
\n
\n\n {footerContent && (\n \n
\n
\n
\n {footerContent.mainText} {footerContent.showTrending && footerContent.trendingIcon}\n
\n
\n {footerContent.subText}\n
\n
\n
\n
\n )}\n
\n )\n}\n\n\n// ========================================================================================\n// Line Chart Example\n// ========================================================================================\ninterface UserActivityChartProps {\n className?: string\n}\n\ntype TimePeriod = \"week\" | \"month\"\n\nexport const LineChartExample = ({ className }: UserActivityChartProps) => {\n const [timePeriod, setTimePeriod] = useState(\"week\")\n\n const weeklyData = [\n { period: \"Mon\", activeUsers: 1200, totalPrompts: 2400 },\n { period: \"Tue\", activeUsers: 1350, totalPrompts: 2650 },\n { period: \"Wed\", activeUsers: 1450, totalPrompts: 2800 },\n { period: \"Thu\", activeUsers: 1600, totalPrompts: 3100 },\n { period: \"Fri\", activeUsers: 1800, totalPrompts: 3400 },\n { period: \"Sat\", activeUsers: 1650, totalPrompts: 3200 },\n { period: \"Sun\", activeUsers: 1400, totalPrompts: 2900 },\n ]\n\n const monthlyData = [\n { period: \"Jan\", activeUsers: 8500, totalPrompts: 18200 },\n { period: \"Feb\", activeUsers: 9200, totalPrompts: 19800 },\n { period: \"Mar\", activeUsers: 10100, totalPrompts: 21500 },\n { period: \"Apr\", activeUsers: 11300, totalPrompts: 23800 },\n { period: \"May\", activeUsers: 12800, totalPrompts: 26400 },\n { period: \"Jun\", activeUsers: 14200, totalPrompts: 29100 },\n { period: \"Jul\", activeUsers: 15600, totalPrompts: 31800 },\n { period: \"Aug\", activeUsers: 16800, totalPrompts: 34200 },\n { period: \"Sep\", activeUsers: 17900, totalPrompts: 36500 },\n { period: \"Oct\", activeUsers: 19200, totalPrompts: 38900 },\n { period: \"Nov\", activeUsers: 20500, totalPrompts: 41200 },\n { period: \"Dec\", activeUsers: 21800, totalPrompts: 43600 },\n ]\n\n const getCurrentData = () => {\n return timePeriod === \"week\" ? weeklyData : monthlyData\n }\n\n const getTitle = () => {\n return timePeriod === \"week\"\n ? \"Weekly Engagement Growth\"\n : \"Monthly Engagement Growth\"\n }\n\n const getDescription = () => {\n return timePeriod === \"week\"\n ? \"Tracks daily active users and prompts throughout the week\"\n : \"Tracks monthly active users and prompts throughout the year\"\n }\n\n const chartConfig = {\n activeUsers: {\n label: \"Active Users\",\n color: \"var(--chart-1)\",\n },\n totalPrompts: {\n label: \"Total Prompts\",\n color: \"var(--chart-2)\",\n },\n } satisfies ChartConfig\n\n return (\n {\n const r = 24\n return (\n \n )\n }}\n toggleOptions={{\n options: [\n {\n value: \"week\",\n label: \"Week\",\n icon: \n },\n {\n value: \"month\",\n label: \"Month\",\n icon: \n }\n ],\n currentValue: timePeriod,\n onChange: (value) => setTimePeriod(value as TimePeriod),\n position: \"header-right\"\n }}\n className={className}\n />\n )\n}\n", "type": "registry:component" } ], "type": "registry:block" }