Usage
FundamentalsBuild with Plotcn.
Install a visualization, import the source into your application, connect it to your data, and customize the implementation as deeply as your product requires.
import { PlotLineChart } from "@/components/charts/plot-line-chart"Plotcn Visualization Lifecycle
The 6-step developer workflow from source installation to production deployment.
- 01DoneInstall Source
Run shadcn add @plotcn/<item> to copy pure TypeScript into your application.
- 02DoneImport Locally
Import the source component from @/components/charts/ using project aliases.
- 03DonePass Typed Data
Feed strongly-typed application data arrays matching the component contract.
- 04ActiveResponsive Shell
Render inside an explicit height container (e.g. h-[320px] or aspect-video).
- 05ActiveCustomize Locally
Adjust SVG geometry, tooltips, axis formats, or animations directly in source.
- 06DoneShip to Production
Compile standard React code with zero third-party Plotcn runtime lock-in.
Basic usage
Working with an installed Plotcn component is identical to working with any local React component in your codebase. Once installed via the CLI, you import the source file, provide your data array, and place the component inside your page layout.
Here is a minimal, complete example using the Cartesian line chart (PlotLineChart):
import { PlotLineChart, type PlotDatum } from "@/components/charts/plot-line-chart"const revenueData: PlotDatum[] = [ { label: "Jan", value: 12400, previous: 10200 }, { label: "Feb", value: 14800, previous: 11400 }, { label: "Mar", value: 16200, previous: 13100 }, { label: "Apr", value: 15900, previous: 14500 }, { label: "May", value: 18400, previous: 15200 }, { label: "Jun", value: 21200, previous: 17800 },]export function RevenueChart() { return ( <div className="h-[280px] w-full"> <PlotLineChart data={revenueData} label="Monthly Revenue" /> </div> )}Importing a component
Plotcn visualizations are distributed directly into your project's components/charts/ folder. The import statement resolves to your local filesystem using your configured TypeScript path aliases.
import { PlotLineChart } from "@/components/charts/plot-line-chart"Source Path Resolution Hierarchy
How your components.json configuration maps physical chart source files to clean application imports.
Never import from a "plotcn" package
Plotcn does not publish a heavyweight npm runtime package. Do not write:
// ❌ Incorrect: Plotcn is not an npm runtime packageimport { PlotLineChart } from "plotcn"Because the source lives in your repository, you maintain full control over imports, file locations, bundle splitting, and renaming. If you reorganize your project folders, update your local import path accordingly.
Passing data
Visualization contracts should always be strongly typed. Avoid loose Record<string, any> objects or untyped arrays that obscure data bugs until runtime.
Explicit data contracts
export type RevenuePoint = { label: string // Independent axis (month, category, date) value: number // Primary metric previous?: number // Optional comparison benchmark}Then supply an array matching that contract:
const data: RevenuePoint[] = [ { label: "Q1", value: 45000, previous: 38000 }, { label: "Q2", value: 52000, previous: 44000 },]<PlotLineChart data={data} label="Quarterly Performance" />Plotcn does not own your data-fetching layer
Plotcn components are pure presentation leaves. They do not dictate how your data is fetched, cached, or refreshed. Feed your charts using whichever pattern fits your architecture:
- React Server Components: Fetch data in
page.tsxand pass serialized arrays down. - TanStack Query / SWR: Consume client hooks with automatic revalidation.
- Route Loaders: Provide data through Next.js or React Router loaders.
Sizing and responsiveness
Visualizations do not force hardcoded pixel dimensions. Instead, they dynamically scale to fill the width and height of their immediate parent container.
Responsive Sizing & Container Hierarchy
Visualizations dynamically fill the parent box. Always reserve vertical space with an explicit height.
- 01DoneParent Container
Application sets explicit height: <div className="h-[320px] w-full">.
- 02DoneChartContainer Shell
Observes parent box dimensions via native ResizeObserver or SVG viewBox.
- 03ActiveMeasured Dimensions
Width: 640px, Height: 320px non-zero dimensions computed for coordinate scales.
- 04DoneCrisp SVG Marks
Lines, grids, areas, and tooltips render pixel-perfect vectors with zero layout shift.
Sizing container contract
To ensure proper rendering and responsive recalculations, always wrap the chart in a container with an explicit height:
{/* Explicit height parent using Tailwind CSS */}<div className="h-[320px] w-full"> <PlotLineChart data={data} /></div>You can use fixed pixel heights (h-[360px]), Tailwind scale utilities (h-64, h-80), or responsive aspect ratios (aspect-[16/9], aspect-video).
Component states
In production applications, charts must handle four distinct operational states: loading, empty, error, and ready. Never display only the happy path.
Component Lifecycle State Machine
Truthful RepresentationRender an accessible skeleton or fallback with role='status'. Never render fake metrics or dummy graphs that mislead users.
State composition pattern
Implement truthful state branches using standard React conditionals or by passing state props:
import { PlotLineChart } from "@/components/charts/plot-line-chart"import { useQuery } from "@tanstack/react-query"export function ActiveUsersCard() { const { data, isLoading, error } = useQuery({ queryKey: ["active-users"], queryFn: fetchActiveUsers, }) // 1. Error state (highest priority) if (error) { return ( <div className="h-[280px] flex items-center justify-center rounded-xl border border-rose-500/20 bg-rose-950/10 p-6 text-center text-xs text-rose-300"> Failed to load activity metrics. Please try again. </div> ) } // 2. Loading state if (isLoading) { return ( <div className="h-[280px] flex items-center justify-center rounded-xl border border-white/[0.06] bg-zinc-900/30 text-xs text-zinc-500"> Loading visualization… </div> ) } // 3. Empty state if (!data?.length) { return ( <div className="h-[280px] flex items-center justify-center rounded-xl border border-white/[0.06] bg-zinc-900/20 text-xs text-zinc-500"> No user activity recorded for this period. </div> ) } // 4. Ready state return ( <div className="h-[280px] w-full"> <PlotLineChart data={data} label="Active Users" /> </div> )}No fake data during loading
Never populate loading skeletons with random placeholder lines or simulated metrics. Faux data misleads users and can be mistaken for actual telemetry. Use truthful, neutral loading skeletons.
Configure the visualization
Plotcn components expose a concise, sensible public prop surface for common adjustments while keeping deep customization where it belongs: in your local source code.
Standard prop surface
<PlotLineChart data={revenueData} label="Monthly Revenue" compact={false} // Hide secondary axes for dense widgets area={true} // Enable soft gradient background fill loading={false} // Pass loading state directly error={undefined} // Pass error message directly/>Customize source
Because installed components belong to your repository, you can tailor them to your product's unique design system, backend requirements, and interaction patterns.
Example: Adding custom currency formatting
To format tooltip values as USD currency, locate the installed component file in components/charts/plot-line-chart.tsx and adjust the tick and tooltip formatters:
// 1. Define your localized currency formatterconst formatCurrency = (val: number) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format(val)// 2. Apply directly to YAxis ticks<YAxis tickFormatter={(val) => `$${val / 1000}k`} tick={{ fill: "var(--plot-muted)", fontSize: 11 }}/>// 3. Apply to Tooltip values<Tooltip formatter={(value: number) => [formatCurrency(value), "Revenue"]}/>The Golden Rule of Source Customization
- Generic improvements: Keep layout containers, ARIA screen-reader labels, and theme variable references upstream-compatible.
- Product-specific logic: Put brand-specific currency rules, custom SVG icons, and company domain logic directly in your local chart files.
Shared vs engine-specific APIs
A foundational Plotcn invariant is: "Consistency around the chart. Freedom inside the chart."
Shared Experience vs Engine-Specific APIs
Architectural InvariantPlotcn unifies the developer experience around the chart—theming, responsive sizing, loading fallbacks, and accessibility—while preserving the raw power and idiomatic APIs of the underlying visualization engine.
Plotcn standardizes the outer shell (theming tokens, container responsiveness, error boundaries, accessible screen-reader tables), but does not force Recharts, D3.js, and Google Charts into an artificial unified API. Each engine retains its native strengths.
Recharts
Recharts is the recommended choice for standard product dashboards, SaaS metrics, and declarative React composition.
Why Recharts?
- Fastest implementation: Composable React elements (
<AreaChart>,<XAxis>,<Tooltip>). - Declarative: Sizing and layout are handled seamlessly through React props.
- Lightweight bundle: Only includes Cartesian layout logic.
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"export function PlotLineChart({ data }: PlotLineChartProps) { return ( <ResponsiveContainer width="100%" height="100%"> <AreaChart data={data}> <XAxis dataKey="label" stroke="var(--chart-muted)" /> <YAxis stroke="var(--chart-muted)" /> <Tooltip /> <Area type="monotone" dataKey="value" stroke="currentColor" fill="url(#gradient)" /> </AreaChart> </ResponsiveContainer> )}When you need to adjust grid dash arrays, margins, or active dot radiuses, you modify these JSX primitives directly.
D3.js
Plotcn approaches D3.js with a clear philosophy: D3 calculates the math and geometry; React renders the SVG elements.
D3.js + React Separation of Concerns
D3 calculates the mathematics and layout geometry; React renders the SVG elements directly with native virtual DOM reconciliation.
Why avoid d3.select() in React?
Manipulating the DOM directly with d3.select().append("circle") breaks React's virtual DOM reconciliation, breaks server-side rendering, and makes component cleanup error-prone. By letting React render the SVG tags, you gain full access to React state, props, and transitions.
Example: Modular D3 path generation
import { useMemo } from "react"import { pie, arc } from "d3-shape"export function D3Donut({ values }: { values: number[] }) { const arcs = useMemo(() => { const pieGen = pie<number>().sort(null).value(d => d) const arcGen = arc<any>().innerRadius(60).outerRadius(90).cornerRadius(3) return pieGen(values).map(slice => arcGen(slice)) }, [values]) return ( <svg viewBox="0 0 200 200" className="w-full h-full"> <g transform="translate(100, 100)"> {arcs.map((d, i) => ( <path key={i} d={d ?? ""} fill={`var(--chart-${(i % 5) + 1})`} /> ))} </g> </svg> )}Always use modular packages (d3-shape, d3-scale) rather than the umbrella d3 package to keep bundle sizes minimal.
Google Charts
Google Charts provides enterprise visualization types—most notably the Google GeoChart for interactive country and regional choropleth maps.
Hosted runtime considerations
Google Charts renders via a hosted Google CDN script (google.visualization). The Plotcn wrapper manages this external dependency gracefully:
- Script deduplication: Injects the Google loader once and reuses the shared Promise.
- Package isolation: Loads only the requested visualization package (e.g.
geochart). - Responsive resize: Re-draws charts automatically on window resize.
import { GoogleGeoChart } from "@/components/charts/google/google-geochart"const countryTraffic = [ { region: "US", users: 12450 }, { region: "DE", users: 8920 }, { region: "IN", users: 14300 }, { region: "GB", users: 6710 }, { region: "JP", users: 9540 },]export function RegionalTraffic() { return ( <div className="h-[380px] w-full"> <GoogleGeoChart data={countryTraffic} regionKey="region" valueKey="users" region="world" displayMode="regions" /> </div> )}Keep client boundaries narrow
Because Google Charts requires the browser DOM and external script execution, the component carries "use client". However, keep your parent route (page.tsx) as a Server Component and import the chart as an interactive client leaf.
Composition
Charts should integrate seamlessly into your broader product interface. Compose them inside standard dashboard surfaces:
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"import { PlotLineChart } from "@/components/charts/plot-line-chart"import { ChartBoundary } from "@/components/charts/chart-boundary"export default function AnalyticsDashboard({ revenueData }) { return ( <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <Card> <CardHeader> <CardTitle>MRR Growth</CardTitle> <CardDescription>Monthly recurring revenue over the past 6 months</CardDescription> </CardHeader> <CardContent> <ChartBoundary> <div className="h-[280px] w-full"> <PlotLineChart data={revenueData} label="MRR" /> </div> </ChartBoundary> </CardContent> </Card> </div> )}Interaction
Plotcn charts support rich user interaction while exposing clean, typed event callbacks to your application.
Strongly typed event handlers
<GoogleGeoChart data={countryTraffic} regionKey="region" valueKey="users" onRegionSelect={(regionCode: string) => { console.log(`Selected region: ${regionCode}`) router.push(`/analytics/regions/${regionCode.toLowerCase()}`) }}/>Event handler stability
Avoid creating inline anonymous functions for expensive event pipelines:
// ✅ Good: Stable callback referenceconst handleSelect = useCallback((region: string) => { setSelectedRegion(region)}, [])<GoogleGeoChart onRegionSelect={handleSelect} />Data transformation
Prepare application data before passing it to the visualization component. Keep business rules, pricing logic, and filtering outside of generic chart code.
Separation of concerns
// 1. Raw backend responseinterface OrderEntity { id: string totalCents: number status: "completed" | "pending" | "refunded" createdAt: string}// 2. Transformation utility outside componentfunction prepareRevenueData(orders: OrderEntity[]): PlotDatum[] { return orders .filter((o) => o.status === "completed") .map((o) => ({ label: new Date(o.createdAt).toLocaleDateString("en-US", { month: "short" }), value: o.totalCents / 100, }))}When to use useMemo
Only memoize transformations that process thousands of records or execute non-trivial D3 math (e.g. Voronoi tessellations, force simulations). For small arrays under 100 items, standard JavaScript array mappings are fast enough that useMemo introduces unnecessary overhead.
Performance
High-performance dashboard rendering relies on disciplined architectural practices:
- Keep Client Boundaries Narrow: Do not place
"use client"at the route root. Keep data fetching on the server and render charts as isolated client components. - Stable Dimensions: Always specify container heights to avoid cumulative layout shift (CLS) during chart mounting.
- Modular D3 Imports: Import only the specific submodules needed (
d3-scale,d3-shape) to minimize JavaScript bundle size. - Deduplicate External Loaders: Google Charts scripts are shared across all instances on a page through a single singleton promise.
- Contain Exceptions with ChartBoundary: Wrap complex visualizations in React error boundaries so a rendering error in one widget does not break the rest of the dashboard.
Common mistakes
Diagnostic guidance for common visualization integration pitfalls:
Common Pitfalls & Architectural Solutions
Diagnostic ChecklistImporting from Wrong Path
PROBLEM:Installed source exists in project but TypeScript compiler throws 'Cannot find module'.
CAUSE:Import statement uses incorrect alias or does not match components.json target paths.
Zero-Height Container
PROBLEM:Chart appears blank, throws ResizeObserver loop limit errors, or collapses to 0px.
CAUSE:Parent layout lacks an explicit height (e.g. unmeasured flex container or collapsed accordion).
Passing Raw API Responses
PROBLEM:Chart becomes deeply coupled to backend database schemas and breaks on minor API changes.
CAUSE:Directly passing nested database entities instead of a focused, typed chart data array.
Over-Generalizing Component Props
PROBLEM:A simple chart accumulates 35 configuration props to support every bespoke styling variation.
CAUSE:Treating installed Plotcn source like an inflexible third-party npm package.
Mixing Engine Dependencies
PROBLEM:Installing a Recharts component pulls D3 packages or causes bundle size bloat.
CAUSE:Importing an internal D3 helper inside a Recharts chart rather than using the shared layer.
Making the Entire Route 'use client'
PROBLEM:Adding one chart causes the entire Next.js page, headers, and data loaders to run client-side.
CAUSE:Placing 'use client' at the top of page.tsx rather than isolating the interactive visualization.
Fake Loading Metrics
PROBLEM:Placeholder chart displays dummy values during data fetching that users mistake for real data.
CAUSE:Rendering a mock dataset during loading rather than a truthful loading skeleton or fallback.
Hardcoding Hex Colors in SVG
PROBLEM:Chart looks crisp in dark mode but becomes illegible or invisible when toggled to light mode.
CAUSE:Hardcoding '#18181b' or '#fafafa' in SVG fill/stroke attributes instead of CSS tokens.
Recommended workflow
Follow this 10-step sequence when building production visualizations with Plotcn:
10-Step Production Development Workflow
The standard engineering sequence for building, styling, and shipping production visualizations with Plotcn.
- 01DoneChoose Engine
Recharts for dashboards, D3 for custom geometry, Google for GeoCharts.
- 02DoneInstall Source
Run `shadcn add @plotcn/<chart>` to copy component source into your app.
- 03DoneInspect Code
Review local source in @/components/charts/ to understand props and markup.
- 04DoneType Your Data
Define explicit TypeScript interfaces for all data series and records.
- 05ActiveSet Layout Height
Wrap chart in a container with measurable dimensions (e.g. h-[320px]).
- 06ActiveHandle States
Implement loading, empty, and error fallback states explicitly.
- 07ActiveCustomize Locally
Adjust tooltips, axis tick formatting, and margins directly in source.
- 08DoneTest Interactions
Verify keyboard navigation, focus outlines, and tooltip hit targets.
- 09DoneVerify Theming
Confirm charts adapt seamlessly between dark and light color modes.
- 10DoneShip with Confidence
Commit source into Git alongside the rest of your application code.
Next steps
Now that you understand day-to-day chart usage and integration patterns, explore styling and accessible rendering: