{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "streamdown-recharts", "title": "Streamdown Recharts", "description": "A Streamdown custom renderer for recharts-json code fences with chart/table views and exports.", "type": "registry:component", "categories": [ "streamdown", "ai", "charts", "data-visualization", "recharts" ], "dependencies": [ "@e965/xlsx", "html-to-image", "lucide-react", "recharts", "streamdown", "zod" ], "registryDependencies": [ "button", "dropdown-menu" ], "cssVars": { "light": { "chart-1": "oklch(0.646 0.222 41.116)", "chart-2": "oklch(0.6 0.118 184.704)", "chart-3": "oklch(0.398 0.07 227.392)", "chart-4": "oklch(0.828 0.189 84.429)", "chart-5": "oklch(0.769 0.188 70.08)" }, "dark": { "chart-1": "oklch(0.488 0.243 264.376)", "chart-2": "oklch(0.696 0.17 162.48)", "chart-3": "oklch(0.769 0.188 70.08)", "chart-4": "oklch(0.627 0.265 303.9)", "chart-5": "oklch(0.645 0.246 16.439)" } }, "files": [ { "content": "\"use client\";\n\nimport { DownloadIcon, MoreHorizontalIcon, Table2Icon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport type { ChartView, RechartsChartSpec } from \"./schema\";\n\ntype ChartShellProps = {\n spec: RechartsChartSpec;\n children: ReactNode;\n shellRef: React.RefObject;\n view: ChartView;\n onDownloadCsv: () => void;\n onDownloadImage: () => void;\n onDownloadXlsx: () => void;\n onToggleView: () => void;\n};\n\nexport const ChartShell = ({\n spec,\n children,\n shellRef,\n view,\n onDownloadCsv,\n onDownloadImage,\n onDownloadXlsx,\n onToggleView,\n}: ChartShellProps) => (\n
\n
\n
\n
\n {spec.meta?.title ?? \"Visualization\"}\n
\n {spec.meta?.description ? (\n

\n {spec.meta.description}\n

\n ) : null}\n
\n
\n \n \n \n \n \n \n \n \n \n {view === \"chart\" ? \"Show as table\" : \"Show as chart\"}\n \n {view === \"chart\" ? (\n \n \n Download image\n \n ) : (\n <>\n \n \n Download CSV\n \n \n \n Download XLSX\n \n \n )}\n \n \n
\n
\n
{children}
\n
\n);\n", "path": "registry/default/streamdown-recharts/chart-shell.tsx", "target": "components/streamdown-recharts/chart-shell.tsx", "type": "registry:component" }, { "content": "\"use client\";\n\nimport { chartMaxHeightClass } from \"./charts\";\nimport { formatTableValue, getTableColumns } from \"./table\";\nimport type { RechartsChartSpec } from \"./schema\";\n\nexport const ChartDataTable = ({ spec }: { spec: RechartsChartSpec }) => {\n const columns = getTableColumns(spec);\n\n return (\n
\n \n \n \n {columns.map((column) => (\n \n ))}\n \n \n \n {spec.data.map((row, rowIndex) => (\n \n {columns.map((column) => (\n \n ))}\n \n ))}\n \n
\n {column.label}\n
\n {formatTableValue(row[column.key], column.series, spec)}\n
\n
\n );\n};\n", "path": "registry/default/streamdown-recharts/chart-table.tsx", "target": "components/streamdown-recharts/chart-table.tsx", "type": "registry:component" }, { "content": "\"use client\";\n\nimport type { ReactElement } from \"react\";\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Cell,\n Legend,\n Line,\n LineChart,\n Pie,\n PieChart,\n ResponsiveContainer,\n Scatter,\n ScatterChart,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { formatNumber, formatTick } from \"./formatters\";\nimport type { RechartsChartSpec, RechartsSeries } from \"./schema\";\n\nexport const chartColors = [\n \"var(--chart-1)\",\n \"var(--chart-2)\",\n \"var(--chart-3)\",\n \"var(--chart-4)\",\n \"var(--chart-5)\",\n \"color-mix(in oklch, var(--chart-1) 70%, transparent)\",\n \"color-mix(in oklch, var(--chart-2) 70%, transparent)\",\n \"color-mix(in oklch, var(--chart-3) 70%, transparent)\",\n];\n\nexport const chartHeightClass = \"h-[420px]\";\nexport const chartMaxHeightClass = \"max-h-[420px]\";\n\nconst getSeriesColor = (index: number): string =>\n chartColors[index % chartColors.length];\n\nconst getSeriesName = (series: RechartsSeries): string =>\n series.label ?? series.dataKey;\n\nconst renderBars = (spec: RechartsChartSpec) =>\n spec.series.map((series, index) => (\n \n ));\n\nconst renderLines = (spec: RechartsChartSpec) =>\n spec.series.map((series, index) => (\n \n ));\n\nconst renderAreas = (spec: RechartsChartSpec) =>\n spec.series.map((series, index) => (\n \n ));\n\nconst ChartFrame = ({ children }: { children: ReactElement }) => (\n
\n \n {children}\n \n
\n);\n\nexport const CartesianChart = ({ spec }: { spec: RechartsChartSpec }) => {\n const chartMargin = { top: 8, right: 20, bottom: 8, left: 8 };\n const xKey = spec.xKey ?? spec.nameKey ?? \"name\";\n const firstSeries = spec.series[0];\n const vertical = spec.chartType === \"bar\" && spec.layout === \"vertical\";\n\n if (vertical) {\n return (\n \n \n \n formatNumber(value, firstSeries, spec)}\n type=\"number\"\n />\n \n \n \n {renderBars(spec)}\n \n \n );\n }\n\n if (spec.chartType === \"bar\") {\n return (\n \n \n \n \n formatTick(value, spec)} />\n \n \n {renderBars(spec)}\n \n \n );\n }\n\n if (spec.chartType === \"area\") {\n return (\n \n \n \n \n formatTick(value, spec)} />\n \n \n {renderAreas(spec)}\n \n \n );\n }\n\n return (\n \n \n \n \n formatTick(value, spec)} />\n \n \n {renderLines(spec)}\n \n \n );\n};\n\nexport const PieVisualization = ({ spec }: { spec: RechartsChartSpec }) => {\n const nameKey = spec.nameKey ?? spec.xKey ?? \"name\";\n const valueKey = spec.valueKey ?? spec.series[0].dataKey;\n\n return (\n \n \n \n \n \n {spec.data.map((row, index) => (\n \n ))}\n \n \n \n );\n};\n\nexport const ScatterVisualization = ({ spec }: { spec: RechartsChartSpec }) => {\n const series = spec.series[0];\n const inferredXKey = Object.keys(spec.data[0] ?? {}).find(\n (key) => key !== series.dataKey,\n );\n const xKey = spec.xKey ?? inferredXKey ?? \"x\";\n const xIsNumeric = spec.data.every((row) => typeof row[xKey] === \"number\");\n\n return (\n \n \n \n formatTick(value, spec)}\n type={xIsNumeric ? \"number\" : \"category\"}\n />\n formatNumber(value, series, spec)}\n type=\"number\"\n />\n \n \n \n \n \n );\n};\n", "path": "registry/default/streamdown-recharts/charts.tsx", "target": "components/streamdown-recharts/charts.tsx", "type": "registry:component" }, { "content": "import { toPng } from \"html-to-image\";\nimport { getTableColumns, getTableExportRows } from \"./table\";\nimport type { RechartsChartSpec } from \"./schema\";\n\nexport const sanitizeFileName = (fileName: string): string =>\n fileName.trim().replace(/[<>:\"/\\\\|?*]+/g, \"-\") || \"chart\";\n\nconst downloadBlob = (blob: Blob, fileName: string) => {\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n\n link.href = url;\n link.download = fileName;\n link.click();\n URL.revokeObjectURL(url);\n};\n\nexport const downloadTableAsCsv = (spec: RechartsChartSpec) => {\n const columns = getTableColumns(spec);\n const rows = getTableExportRows(spec);\n const header = columns.map((column) => column.label);\n const csvContent = [header, ...rows.map((row) => header.map((key) => row[key]))]\n .map((row) => row.map((value) => `\"${String(value).replace(/\"/g, '\"\"')}\"`).join(\",\"))\n .join(\"\\n\");\n const blob = new Blob([`\\ufeff${csvContent}`], {\n type: \"text/csv;charset=utf-8\",\n });\n\n downloadBlob(blob, `${sanitizeFileName(spec.meta?.title ?? \"chart\")}.csv`);\n};\n\nexport const downloadTableAsXlsx = async (spec: RechartsChartSpec) => {\n const xlsx = await import(\"@e965/xlsx\");\n const fileName = sanitizeFileName(spec.meta?.title ?? \"chart\");\n const worksheet = xlsx.utils.json_to_sheet(getTableExportRows(spec));\n const workbook = xlsx.utils.book_new();\n\n xlsx.utils.book_append_sheet(workbook, worksheet, \"Data\");\n xlsx.writeFile(workbook, `${fileName}.xlsx`);\n};\n\nexport const downloadChartAsPng = async (element: HTMLElement, fileName: string) => {\n const dataUrl = await toPng(element, {\n pixelRatio: 2,\n filter: (node: HTMLElement) =>\n node.getAttribute?.(\"data-export-exclude\") !== \"true\",\n });\n const link = document.createElement(\"a\");\n\n link.href = dataUrl;\n link.download = fileName;\n link.click();\n};\n", "path": "registry/default/streamdown-recharts/downloads.ts", "target": "components/streamdown-recharts/downloads.ts", "type": "registry:component" }, { "content": "import type { RechartsChartSpec, RechartsSeries } from \"./schema\";\n\ntype NumberFormat = NonNullable;\n\nconst numberFormatOptions = {\n compact: { maximumFractionDigits: 1, notation: \"compact\" },\n currency: { maximumFractionDigits: 0, style: \"currency\" },\n integer: { maximumFractionDigits: 0 },\n percent: { maximumFractionDigits: 1, style: \"percent\" },\n raw: { maximumFractionDigits: 2 },\n} satisfies Record;\n\ntype FormatOptions = Pick;\n\nexport const formatNumber = (\n value: number,\n series?: RechartsSeries,\n spec?: FormatOptions,\n): string => {\n const prefix = series?.valuePrefix ?? \"\";\n const suffix = series?.valueSuffix ?? \"\";\n const valueFormat = series?.valueFormat ?? \"raw\";\n const options = {\n ...numberFormatOptions[valueFormat],\n ...(valueFormat === \"currency\" ? { currency: spec?.currency ?? \"USD\" } : {}),\n };\n const formattedValue = new Intl.NumberFormat(\n spec?.locale ?? \"en-US\",\n options,\n ).format(value);\n\n return `${prefix}${formattedValue}${suffix}`;\n};\n\nexport const formatTick = (\n value: number | string,\n spec?: Pick,\n): string => {\n if (typeof value !== \"number\") {\n return value;\n }\n\n return new Intl.NumberFormat(spec?.locale ?? \"en-US\", {\n maximumFractionDigits: 1,\n notation: Math.abs(value) >= 10_000 ? \"compact\" : \"standard\",\n }).format(value);\n};\n", "path": "registry/default/streamdown-recharts/formatters.ts", "target": "components/streamdown-recharts/formatters.ts", "type": "registry:component" }, { "content": "\"use client\";\n\nimport type { CustomRenderer } from \"streamdown\";\nimport { RechartsJsonRenderer } from \"./renderer\";\n\nexport {\n RechartsJsonRenderer,\n RechartsChartSpecSchema,\n ChartDataRowSchema,\n ChartSeriesSchema,\n ChartValueSchema,\n parseChartSpec,\n} from \"./renderer\";\nexport type {\n ChartDataRow,\n ChartView,\n RechartsChartSpec,\n RechartsSeries,\n} from \"./renderer\";\n\nexport const rechartsRenderers = [\n {\n language: [\"recharts-json\", \"rechart-json\"],\n component: RechartsJsonRenderer,\n },\n] satisfies CustomRenderer[];\n", "path": "registry/default/streamdown-recharts/index.tsx", "target": "components/streamdown-recharts/index.tsx", "type": "registry:component" }, { "content": "\"use client\";\n\nimport { chartHeightClass } from \"./charts\";\n\nexport const ChartLoadingPlaceholder = () => (\n
\n
\n
\n
\n
\n
\n
\n
\n {[\"h-2/5\", \"h-3/5\", \"h-1/2\", \"h-4/5\", \"h-2/3\"].map((height) => (\n
\n ))}\n
\n
\n
\n
\n
\n);\n", "path": "registry/default/streamdown-recharts/loading-placeholder.tsx", "target": "components/streamdown-recharts/loading-placeholder.tsx", "type": "registry:component" }, { "content": "import { RechartsChartSpecSchema } from \"./schema\";\nimport type { ParseResult } from \"./schema\";\n\nexport const parseChartSpec = (code: string, isIncomplete?: boolean): ParseResult => {\n if (isIncomplete) {\n return { status: \"loading\" };\n }\n\n let parsedJson: unknown;\n\n try {\n parsedJson = JSON.parse(code);\n } catch {\n // The fence may report complete before the JSON is fully streamed in.\n // Truncated JSON throws here, so keep showing the loading state rather\n // than flashing an error mid-stream.\n return { status: \"loading\" };\n }\n\n const parseResult = RechartsChartSpecSchema.safeParse(parsedJson);\n\n if (!parseResult.success) {\n return {\n status: \"invalid\",\n message:\n parseResult.error.issues[0]?.message ??\n \"Invalid Recharts JSON structure.\",\n };\n }\n\n return { status: \"valid\", spec: parseResult.data };\n};\n", "path": "registry/default/streamdown-recharts/parser.ts", "target": "components/streamdown-recharts/parser.ts", "type": "registry:component" }, { "content": "\"use client\";\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { CustomRendererProps } from \"streamdown\";\nimport { CodeBlock, CodeBlockContainer, CodeBlockHeader } from \"streamdown\";\nimport { CartesianChart, PieVisualization, ScatterVisualization } from \"./charts\";\nimport { ChartShell } from \"./chart-shell\";\nimport { ChartDataTable } from \"./chart-table\";\nimport {\n downloadChartAsPng,\n downloadTableAsCsv,\n downloadTableAsXlsx,\n sanitizeFileName,\n} from \"./downloads\";\nimport { ChartLoadingPlaceholder } from \"./loading-placeholder\";\nimport { parseChartSpec } from \"./parser\";\nimport type { ChartView, RechartsChartSpec } from \"./schema\";\n\nexport {\n ChartDataRowSchema,\n ChartSeriesSchema,\n ChartValueSchema,\n RechartsChartSpecSchema,\n} from \"./schema\";\nexport { parseChartSpec } from \"./parser\";\nexport type {\n ChartDataRow,\n ChartView,\n RechartsChartSpec,\n RechartsSeries,\n} from \"./schema\";\n\nconst ChartBody = ({ spec, view }: { spec: RechartsChartSpec; view: ChartView }) => {\n if (view === \"table\") {\n return ;\n }\n\n if (spec.chartType === \"pie\") {\n return ;\n }\n\n if (spec.chartType === \"scatter\") {\n return ;\n }\n\n return ;\n};\n\nconst ChartVisualization = ({ spec }: { spec: RechartsChartSpec }) => {\n const shellRef = useRef(null);\n const [view, setView] = useState(\"chart\");\n\n const handleToggleView = useCallback(() => {\n setView((currentView) => (currentView === \"chart\" ? \"table\" : \"chart\"));\n }, []);\n\n const handleDownloadImage = useCallback(() => {\n const element = shellRef.current;\n\n if (!element) {\n return;\n }\n\n void downloadChartAsPng(\n element,\n `${sanitizeFileName(spec.meta?.title ?? \"chart\")}.png`,\n );\n }, [spec]);\n\n const handleDownloadCsv = useCallback(() => {\n downloadTableAsCsv(spec);\n }, [spec]);\n\n const handleDownloadXlsx = useCallback(() => {\n void downloadTableAsXlsx(spec);\n }, [spec]);\n\n return (\n \n \n \n );\n};\n\nexport const RechartsJsonRenderer = ({\n code,\n language,\n isIncomplete,\n}: CustomRendererProps) => {\n const parseResult = useMemo(\n () => parseChartSpec(code, isIncomplete),\n [code, isIncomplete],\n );\n\n if (parseResult.status === \"loading\") {\n return ;\n }\n\n if (parseResult.status === \"invalid\") {\n return (\n \n \n
\n {parseResult.message}\n
\n \n
\n );\n }\n\n return ;\n};\n", "path": "registry/default/streamdown-recharts/renderer.tsx", "target": "components/streamdown-recharts/renderer.tsx", "type": "registry:component" }, { "content": "import { z } from \"zod\";\n\nexport const ChartValueSchema = z.union([z.string(), z.number(), z.null()]);\nexport const ChartDataRowSchema = z.record(z.string(), ChartValueSchema);\n\nexport const ChartSeriesSchema = z.object({\n dataKey: z.string().min(1),\n label: z.string().min(1).optional(),\n valueFormat: z\n .enum([\"integer\", \"compact\", \"raw\", \"currency\", \"percent\"])\n .optional(),\n valuePrefix: z.string().optional(),\n valueSuffix: z.string().optional(),\n});\n\nexport const RechartsChartSpecSchema = z.object({\n chartType: z.enum([\"bar\", \"line\", \"area\", \"pie\", \"scatter\"]),\n layout: z.enum([\"horizontal\", \"vertical\"]).optional(),\n locale: z.string().optional(),\n currency: z.string().optional(),\n meta: z\n .object({\n title: z.string().optional(),\n description: z.string().optional(),\n })\n .optional(),\n xKey: z.string().min(1).optional(),\n nameKey: z.string().min(1).optional(),\n valueKey: z.string().min(1).optional(),\n series: z.array(ChartSeriesSchema).min(1),\n data: z.array(ChartDataRowSchema).min(1),\n});\n\nexport type RechartsChartSpec = z.infer;\nexport type RechartsSeries = z.infer;\nexport type ChartDataRow = z.infer;\nexport type ChartView = \"chart\" | \"table\";\n\nexport type ParseResult =\n | { status: \"loading\" }\n | { status: \"invalid\"; message: string }\n | { status: \"valid\"; spec: RechartsChartSpec };\n\nexport type TableColumn = {\n key: string;\n label: string;\n series?: RechartsSeries;\n};\n", "path": "registry/default/streamdown-recharts/schema.ts", "target": "components/streamdown-recharts/schema.ts", "type": "registry:component" }, { "content": "import { formatNumber } from \"./formatters\";\nimport type { RechartsChartSpec, RechartsSeries, TableColumn } from \"./schema\";\n\nexport const getTableColumns = (spec: RechartsChartSpec): TableColumn[] => {\n if (spec.chartType === \"pie\") {\n const nameKey = spec.nameKey ?? spec.xKey ?? \"name\";\n const valueKey = spec.valueKey ?? spec.series[0].dataKey;\n\n return [\n { key: nameKey, label: nameKey },\n {\n key: valueKey,\n label: spec.series[0].label ?? valueKey,\n series: spec.series[0],\n },\n ];\n }\n\n const xKey = spec.xKey ?? spec.nameKey ?? \"name\";\n\n return [\n { key: xKey, label: xKey },\n ...spec.series.map((series) => ({\n key: series.dataKey,\n label: series.label ?? series.dataKey,\n series,\n })),\n ];\n};\n\nexport const formatTableValue = (\n value: string | number | null | undefined,\n series: RechartsSeries | undefined,\n spec: RechartsChartSpec,\n): string => {\n if (value === undefined || value === null) {\n return \"\";\n }\n\n if (typeof value === \"number\" && series) {\n return formatNumber(value, series, spec);\n }\n\n return String(value);\n};\n\nexport const getTableExportRows = (\n spec: RechartsChartSpec,\n): Array> => {\n const columns = getTableColumns(spec);\n\n return spec.data.map((row) => {\n const exportRow: Record = {};\n\n for (const column of columns) {\n exportRow[column.label] = formatTableValue(row[column.key], column.series, spec);\n }\n\n return exportRow;\n });\n};\n", "path": "registry/default/streamdown-recharts/table.ts", "target": "components/streamdown-recharts/table.ts", "type": "registry:component" } ] }