feat(chart): add @hm-design/chart package with Bar, Line, Donut, Pie, Radar, Area and Radial charts
- 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
This commit is contained in:
1 parent
228944a06b
commit
dc9258599c
56 files changed
+3979
-28
No files matched your search
@@ -0,0 +1,86 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user