preskok
uiarea-chartsource: ok · hit
Area Chart
An area chart component with gradient fills and animations
npx shadcn add "https://ui-three-mu.vercel.app/r/area-chart.json"free
Visual style
gradient
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumchartsrecharts
Registry dependencies
https://ui-three-mu.vercel.app/r/chart.json
Files1 · source cached 10/15/2026, 5:15:44 AM
registry/preskok/ui/preskok-ui/area-chart.tsx
"use client"
import { defineChart } from "@tanstack/charts"
import { areaY, type AreaYOptions } from "@tanstack/charts/area"
import { lineY } from "@tanstack/charts/line"
import { scalePoint } from "@tanstack/charts/scales/point"
import { stack } from "@tanstack/charts/stack"
import {
Chart,
ChartFrame,
dimColor,
defaultValueFormatter,
valueToPercent,
getCartesianScales,
getChartColors,
getChartCurve,
focusNearestSeries,
getChartOptions,
getCrosshair,
getFocusStates,
revealEntranceMotion,
toSeriesData,
useSeriesSelection,
type CartesianChartProps,
type ChartCurveType,
type ChartType,
type SeriesDatum,
} from "./chart"
type AreaChartProps = CartesianChartProps & {
areaProps?: Pick<AreaYOptions<SeriesDatum>, "fillOpacity"> & {
strokeWidth?: number
}
connectNulls?: boolean
fillType?: "gradient" | "solid" | "none"
lineType?: ChartCurveType
type?: ChartType
}
const AREA_MARK_ID = "area-fill"
function AreaChart({
areaProps,
ariaLabel = "Area chart",
colors,
config,
connectNulls = false,
data,
dataKey,
fillType = "gradient",
grid = "visible",
legend,
lineType = "linear",
size,
tooltip,
tooltipProps,
type = "default",
valueFormatter = defaultValueFormatter,
xAxis,
yAxis,
...frameProps
}: AreaChartProps) {
const [selectedSeries, selectSeries] = useSeriesSelection()
const rows = toSeriesData({ config, connectNulls, data, dataKey })
const chartColors = getChartColors(config, colors)
const seriesNames = Object.keys(config)
const gradientId = (series: string) =>
`preskok-area-${seriesNames.indexOf(series)}`
const isDimmed = (series: string) =>
selectedSeries !== null && selectedSeries !== series
const formatValue = type === "percent" ? valueToPercent : valueFormatter
const scales = getCartesianScales({
categoryAxis: xAxis,
categoryScale: scalePoint,
data,
dataKey,
grid,
valueAxis: yAxis,
valueFormatter: formatValue,
})
const fill = (row: SeriesDatum) => {
if (fillType === "none") {
return "transparent"
}
if (fillType === "gradient") {
return `url(#${gradientId(row.series)})`
}
const color = chartColors[row.series] ?? "var(--chart-1)"
return isDimmed(row.series) ? dimColor(color, 10) : color
}
const { strokeWidth = 2, ...areaOptions } = areaProps ?? {}
const stroke = (row: SeriesDatum) => {
const color = chartColors[row.series] ?? "var(--chart-1)"
return isDimmed(row.series) ? dimColor(color, 10) : color
}
const sharedOptions = {
color: "series",
curve: getChartCurve(lineType),
key: (row: SeriesDatum) => `${row.series}-${row.index}`,
motion: revealEntranceMotion,
states: getFocusStates<SeriesDatum>("series"),
x: "category",
z: "series",
} as const
const areaFill = {
...sharedOptions,
id: AREA_MARK_ID,
fill,
fillOpacity: fillType === "solid" ? 0.28 : 1,
...areaOptions,
}
const definition = defineChart({
...getChartOptions(chartColors),
focus: focusNearestSeries(AREA_MARK_ID),
gradients:
fillType === "gradient"
? seriesNames.map((series) => {
const color = chartColors[series] ?? "var(--chart-1)"
return {
id: gradientId(series),
stops: [
{ color, offset: 0.05, opacity: isDimmed(series) ? 0.1 : 0.5 },
{ color, offset: 0.95, opacity: 0 },
],
y1: 0,
y2: 1,
}
})
: undefined,
marks: [
getCrosshair(),
type === "default"
? areaY(rows, { ...areaFill, y1: 0, y2: "value" })
: areaY(rows, {
...areaFill,
layout: stack({
offset: type === "percent" ? "normalize" : "diverging",
}),
y: "value",
}),
// The area has no outline; this traces only its top edge.
lineY(toStackedTops(rows, type), {
...sharedOptions,
stroke,
strokeWidth,
y: "top",
}),
],
// The area's bottom edge is its fill, not a stroke, so it can sit flush.
margin: scales.margin && { ...scales.margin, bottom: 0 },
scales: { x: scales.category, y: scales.value },
})
return (
<ChartFrame
{...frameProps}
colors={colors}
config={config}
legend={legend}
onSelectedSeriesChange={selectSeries}
selectedSeries={selectedSeries}
>
<Chart
ariaLabel={ariaLabel}
className="w-full"
config={config}
definition={definition}
entrance="reveal"
size={size}
tooltip={tooltip}
tooltipProps={tooltipProps}
valueFormatter={formatValue}
/>
</ChartFrame>
)
}
/**
* Adds each row's upper edge: its value, or the running total of same-sign
* values (as a share of the category total for `percent`) when stacked.
*/
function toStackedTops(rows: readonly SeriesDatum[], type: ChartType) {
if (type === "default") {
return rows.map((row) => ({ ...row, top: row.value }))
}
const totals = new Map<number, number>()
if (type === "percent") {
for (const row of rows) {
totals.set(
row.index,
(totals.get(row.index) ?? 0) + Math.abs(row.value ?? 0)
)
}
}
const positive = new Map<number, number>()
const negative = new Map<number, number>()
return rows.map((row) => {
if (row.value === null) {
return { ...row, top: null }
}
const running = row.value < 0 ? negative : positive
const top = (running.get(row.index) ?? 0) + row.value
running.set(row.index, top)
const total = totals.get(row.index)
return { ...row, top: total ? top / total : top }
})
}
export { AreaChart }
export type { AreaChartProps }