- new workspace package packages/chart (shadcn/ui Chart + Recharts, HM Theme tokens), examples entry with sample data - Storybook Chart group (src/chart), consumer example (examples/chart), build:chart / build:chart-example scripts - README rewritten as the AI usage guide (install, CSS wiring, chart selection, data rules, verification) - docs: package count/list, Submodule guide (chart workspace, React dedupe), shared AI_GUIDE row, MAINTAINING, story inventory
87 lines
4.3 KiB
TypeScript
87 lines
4.3 KiB
TypeScript
import { useId } from "react"
|
|
import { cn } from "cn"
|
|
import { Bar, BarChart, CartesianGrid, Rectangle, ReferenceLine, XAxis, YAxis, type BarShapeProps } from "recharts"
|
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "./chart"
|
|
import { chartColors, chartLightColors } from "./series"
|
|
|
|
export type HMSignedBarChartProps = {
|
|
data: ReadonlyArray<Record<string, string | number>>
|
|
/** Field of each data row used for the category (X) axis. */
|
|
categoryKey: string
|
|
/** Numeric field of each data row. Positive values rise above the 0 line, negative values fall below it. */
|
|
valueKey: string
|
|
/** Name of the value, used as the tooltip row label. */
|
|
valueLabel: string
|
|
/** Accessible name of the chart. */
|
|
label: string
|
|
className?: string
|
|
}
|
|
|
|
const POSITIVE = 0 // HM series 1 (green)
|
|
const NEGATIVE = 1 // HM series 2 (brown)
|
|
|
|
/**
|
|
* Vertical bar chart for positive and negative values (shadcn Chart + Recharts) around a clearly marked 0 line.
|
|
* Color follows the sign only: positive uses the first HM series color, negative the second. Each bar is a gradient that is
|
|
* light at the 0 line and full color at the outer end (opaque at both ends, light end = the palette's lighter step of the same
|
|
* family), and only the outer end is rounded. The tooltip indicator uses the same solid color as the bar.
|
|
*/
|
|
export function HMSignedBarChart({ data, categoryKey, valueKey, valueLabel, label, className }: HMSignedBarChartProps) {
|
|
const scope = `hm-signed-${useId().replace(/:/g, "")}`
|
|
const positiveGradient = `${scope}-positive`
|
|
const negativeGradient = `${scope}-negative`
|
|
const config: ChartConfig = {
|
|
[valueKey]: { label: valueLabel, color: chartColors[POSITIVE] },
|
|
positive: { label: "양수", color: chartColors[POSITIVE] },
|
|
negative: { label: "음수", color: chartColors[NEGATIVE] },
|
|
}
|
|
// `fill` on the row feeds the tooltip indicator, so it always matches the bar's solid color.
|
|
const rows = data.map(row => ({ ...row, fill: Number(row[valueKey]) < 0 ? "var(--color-negative)" : "var(--color-positive)" }))
|
|
const isNegative = (value: BarShapeProps["value"]) => Number(Array.isArray(value) ? value[1] : value) < 0
|
|
|
|
return (
|
|
<ChartContainer
|
|
config={config}
|
|
role="group"
|
|
aria-label={label}
|
|
className={cn("aspect-auto h-64 w-full sm:h-80", className)}
|
|
>
|
|
<BarChart accessibilityLayer data={rows} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
|
<defs>
|
|
{/* Positive: full color at the top (outer end), light at the bottom (0 line). */}
|
|
<linearGradient id={positiveGradient} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" style={{ stopColor: "var(--color-positive)", stopOpacity: 1 }} />
|
|
<stop offset="100%" style={{ stopColor: chartLightColors[POSITIVE], stopOpacity: 1 }} />
|
|
</linearGradient>
|
|
{/* Negative: light at the top (0 line), full color at the bottom (outer end). */}
|
|
<linearGradient id={negativeGradient} x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" style={{ stopColor: chartLightColors[NEGATIVE], stopOpacity: 1 }} />
|
|
<stop offset="100%" style={{ stopColor: "var(--color-negative)", stopOpacity: 1 }} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid vertical={false} />
|
|
<XAxis dataKey={categoryKey} tickLine={false} axisLine={false} tickMargin={8} />
|
|
<YAxis tickLine={false} axisLine={false} tickMargin={4} width={44} />
|
|
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
|
<ReferenceLine y={0} stroke="var(--hm-chart-zero-line)" strokeWidth={1.5} />
|
|
<Bar
|
|
dataKey={valueKey}
|
|
fill="var(--color-positive)"
|
|
shape={(props: BarShapeProps) => {
|
|
const negative = isNegative(props.value)
|
|
return (
|
|
<Rectangle
|
|
{...props}
|
|
fill={`url(#${negative ? negativeGradient : positiveGradient})`}
|
|
// Flat at the 0 line, rounded only at the outer end. Recharts gives a negative bar a negative height, which
|
|
// mirrors the corner order, so the same [4, 4, 0, 0] rounds the outer (bottom) end of a negative bar too.
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
</BarChart>
|
|
</ChartContainer>
|
|
)
|
|
}
|