diff --git a/packages/chart/README.md b/packages/chart/README.md index 66e4293..5fafb73 100644 --- a/packages/chart/README.md +++ b/packages/chart/README.md @@ -44,6 +44,7 @@ | 시간 흐름의 양·구성비 | `HMAreaChart` | `data` 행 + `categoryKey` + `series[]` | | 전체 대비 비중 | `HMDonutChart`, `HMPieChart` | `data: { id, label, value, color? }[]` | | 두 구성의 동심원 비교 | `HMStackedPieChart` | `rings: [내부, 외부]` | +| 두께가 보이는 입체형 비중 | `HMSolidDonutChart`, `HMSolidPieChart` | 위 Donut 데이터 (`HMSolidDonutChart`는 `totalLabel` 선택) | | 여러 지표의 균형 | `HMRadarChart` | `data` 행 + `categoryKey` + `series[]` | | 목표 대비 진행률·항목별 막대 | `HMRadialBarChart`, `HMRadialMeterChart`, `HMRadialStackedChart` | 위 Donut 데이터 / `item` / `series[]`+`row` | @@ -94,7 +95,7 @@ const received = [{ month: "1월", received: 186, completed: 142 }] 도넛 차트(`HMDonutChart`) 옵션: `totalLabel`(가운데에 전체 합계와 캡션), `activeId`(선택한 조각을 바깥쪽으로 확장하고 가운데에 그 이름·값 표시), `onActiveChange`(조각 클릭 시 id 전달; 상태는 소비 앱이 보관하며 호버로는 바뀌지 않음). 가운데 텍스트는 도넛 안쪽을 넘지 않도록 숫자를 줄이고(`1234.6억` 같은 축약) 이름은 `…`로 줄이며, 합계가 0이면 빈 링과 `0`을 표시합니다. 전체 이름은 범례와 툴팁에서 확인합니다. -파이(`HMPieChart`, `HMStackedPieChart`): `labels`(`"outside"` 조각 바깥 "이름 값"과 연결선, `"inside"` 조각 안 값)는 놓을 자리가 없으면 해당 Label만 생략하고 툴팁에는 전체 정보가 나옵니다. `HMStackedPieChart`는 `rings`(안쪽·바깥쪽 두 시리즈, 같은 `id`는 같은 색)로 동심원 링을 그립니다. 방사형(`HMRadarChart`): `filled`(기본 영역, `false`면 선만), `dots`, `customLabels`(축 이름을 두 줄로 줄이거나 `…`로 줄임), `maxValue`(공통 축 상한, 기본 100). 영역(`HMAreaChart`): `curve`(`monotone`·`linear`·`step`), `dots`, `pointLabels`·`pointColorKey`(Line과 같은 Dot·Label·충돌 처리), `stacked`(값 누적), `expanded`(각 시점 합계를 100%로 정규화, Y축 %, 툴팁에 원래 값과 비율; 합계 0이면 0%). 영역 채움은 시리즈 색의 세로 그라데이션(위 진하게, 아래 옅게)입니다. +파이(`HMPieChart`, `HMStackedPieChart`): `labels`(`"outside"` 조각 바깥 "이름 값"과 연결선, `"inside"` 조각 안 값)는 놓을 자리가 없으면 해당 Label만 생략하고 툴팁에는 전체 정보가 나옵니다. `HMStackedPieChart`는 `rings`(안쪽·바깥쪽 두 시리즈, 같은 `id`는 같은 색)로 동심원 링을 그립니다. 입체형 파이·도넛(`HMSolidPieChart`, `HMSolidDonutChart`): 기존 2D 차트와 같은 `data`(`id`·`label`·`value`·`color?`)·항목 순서·조각 각도·범례·툴팁을 쓰고, 조각을 약간 기울여 내려다본 두께 있는 원판(도넛은 바깥 옆면과 구멍 안쪽 벽)으로 그립니다. 회전 가능한 3D가 아니라 SVG로 두께와 깊이감만 표현하며 기존 2D 컴포넌트·Props는 바뀌지 않습니다. 윗면은 항목 색(`--color-`), 옆면은 같은 계열의 한 단계 어두운 HM 토큰(`--hm-chart-1-side` ~ `--hm-chart-5-side`, 공용 `--hm-chart-N` 토큰은 그대로)을 쓰며, HM 색이 아닌 `color`는 그 색을 어둡게 섞어 옆면에 씁니다. 앞쪽 옆면과 도넛 안쪽 뒤 벽은 서로·윗면과 겹치지 않는 영역만 그리므로 가려지는 면이 비치지 않고 조각 사이에 틈이 없습니다. 두께와 크기는 차트 영역 너비·높이에 맞춰 줄어듭니다(차트 높이 `h-72`/`sm:h-80`은 `className`으로 조정). `HMSolidDonutChart`의 `totalLabel`은 기울어진 구멍 안에 합계와 캡션으로 표시하고 없으면 가운데를 비웁니다. 제약: 조각 클릭·Active(`activeId`)·Label(`labels`)은 지원하지 않으며, 값 0인 항목은 그리지 않고(범례에는 남음), 합계가 0이면 빈 원판·링을 표시합니다. 툴팁은 윗면·옆면 어디에서나 같은 항목을 표시하고 포인터를 따라갑니다. 방사형(`HMRadarChart`): `filled`(기본 영역, `false`면 선만), `dots`, `customLabels`(축 이름을 두 줄로 줄이거나 `…`로 줄임), `maxValue`(공통 축 상한, 기본 100). 영역(`HMAreaChart`): `curve`(`monotone`·`linear`·`step`), `dots`, `pointLabels`·`pointColorKey`(Line과 같은 Dot·Label·충돌 처리), `stacked`(값 누적), `expanded`(각 시점 합계를 100%로 정규화, Y축 %, 툴팁에 원래 값과 비율; 합계 0이면 0%). 영역 채움은 시리즈 색의 세로 그라데이션(위 진하게, 아래 옅게)입니다. 래디얼(`HMRadialBarChart`, `HMRadialMeterChart`, `HMRadialStackedChart`)은 공식 shadcn/ui radial 예제(simple·label·grid·text·shape·stacked)의 각도·반지름·트랙·중앙 Text를 따릅니다(`labels`, `grid`, `startAngle`/`endAngle`, `innerRadius`/`outerRadius`, `trackRadii`, `cornerRadius`). 중앙 숫자는 `CenterText`가 구멍 폭에 맞춰 줄이거나 `1234.6억`처럼 축약합니다. 도넛·래디얼의 링·막대 두께는 공식 예제의 1.5배입니다(2026-11-06): 단일 막대(text·shape·stacked)는 바깥 반지름을 두고 안쪽 반지름을 줄였고, 여러 막대(simple·label·grid)는 `barSize`(18, grid 16.5)·`ringGap`(4, grid 3, 기존 간격)과 기존 중앙 구멍(`innerRadius` 30)을 유지해 `outerRadius`를 `innerRadius + 항목 수 × (barSize + ringGap)`(5개: 140, grid 127.5)로 계산합니다. 래디얼 차트 영역 높이는 300px로 같고 범례는 차트 영역 아래에 따로 놓이며, 폭이 모자라면(320px) simple·label·grid의 반지름·두께·간격이 비례해 줄어듭니다. `HMRadialMeterChart`의 `trackRadii`는 막대와 같은 두께로 맞춥니다. `labels`(항목명 Label)는 막대 안쪽 시작 부분에 호를 따라 그려지며(`insideStart`) 막대 호 길이에 들어가지 않는 긴 이름은 `…`로 줄입니다(전체 이름은 범례와 툴팁). @@ -115,7 +116,7 @@ npm run storybook # Chart 그룹: Bar / Line / Donut / P ### 예제 컴포넌트 -소비 예제(`examples/chart/main.tsx`)와 Storybook은 `@hm-design/chart/examples`의 예제 카드를 사용합니다: Bar(`BarChartExample`, `StackedBarChartExample`, `HorizontalBarChartExample`, `SignedBarChartExample`), Line(`LineChartExample` 외 Curve·Straight·Step 변형), Donut(`DonutChartExample` 외 Plain·Center·Active·Interactive), Pie(`PiePlainExample`, `PieLabelExample`, `PieLabelListExample`, `PieStackedExample`), Radar(`RadarPlainExample`, `RadarDotExample`, `RadarLinesOnlyExample`, `RadarCustomLabelExample`), Area(`AreaCurveExample` 외 Straight·Step·Stacked·Expanded), Radial(`RadialSimpleExample`, `RadialLabelExample`, `RadialGridExample`, `RadialTextExample`, `RadialShapeExample`, `RadialStackedExample`), 묶음 `ChartExamples`. 예시 데이터(`sampleBar`, `samplePie`, `sampleRadial` 등)는 같은 진입점에서 export됩니다. 예제 카드는 `ExampleCard`(내부 컴포넌트)로 감싸며 실제 업무 데이터가 아님을 표시합니다. +소비 예제(`examples/chart/main.tsx`)와 Storybook은 `@hm-design/chart/examples`의 예제 카드를 사용합니다: Bar(`BarChartExample`, `StackedBarChartExample`, `HorizontalBarChartExample`, `SignedBarChartExample`), Line(`LineChartExample` 외 Curve·Straight·Step 변형), Donut(`DonutChartExample` 외 Plain·Center·Active·Interactive·`DonutSolidExample`(입체형)), Pie(`PiePlainExample`, `PieLabelExample`, `PieLabelListExample`, `PieStackedExample`, `PieSolidExample`(입체형)), Radar(`RadarPlainExample`, `RadarDotExample`, `RadarLinesOnlyExample`, `RadarCustomLabelExample`), Area(`AreaCurveExample` 외 Straight·Step·Stacked·Expanded), Radial(`RadialSimpleExample`, `RadialLabelExample`, `RadialGridExample`, `RadialTextExample`, `RadialShapeExample`, `RadialStackedExample`), 묶음 `ChartExamples`. 예시 데이터(`sampleBar`, `samplePie`, `sampleRadial` 등)는 같은 진입점에서 export됩니다. 예제 카드는 `ExampleCard`(내부 컴포넌트)로 감싸며 실제 업무 데이터가 아님을 표시합니다. ## 공식 shadcn Chart와의 차이 diff --git a/packages/chart/src/chart.css b/packages/chart/src/chart.css index 9d09a40..0ce9560 100644 --- a/packages/chart/src/chart.css +++ b/packages/chart/src/chart.css @@ -17,6 +17,13 @@ --hm-chart-4-light: var(--hm-sem-palette-brown-light-4); --hm-chart-5-light: var(--hm-sem-palette-warm-neutral-light); + /* Side walls of the solid (thick) pie and donut: the next palette step darker in the same family as the series color. */ + --hm-chart-1-side: var(--hm-sem-palette-green-light-1); + --hm-chart-2-side: var(--hm-sem-palette-brown-light-1); + --hm-chart-3-side: var(--hm-sem-palette-green-light-2); + --hm-chart-4-side: var(--hm-sem-palette-brown-light-3); + --hm-chart-5-side: var(--hm-sem-palette-warm-neutral-dark); + --hm-chart-tooltip-background: var(--hm-sem-bg-surface); --hm-chart-tooltip-border-color: var(--hm-sem-stroke-default); --hm-chart-tooltip-radius: var(--hm-radius-container); diff --git a/packages/chart/src/components/series.ts b/packages/chart/src/components/series.ts index 7d8a2b9..79fe36f 100644 --- a/packages/chart/src/components/series.ts +++ b/packages/chart/src/components/series.ts @@ -18,6 +18,25 @@ export const chartLightColors = [ "var(--hm-chart-5-light)", ] as const +/** Darker tone of each HM series color, for the side walls of the solid (thick) pie and donut: the same palette family, the next step darker (see chart.css). */ +export const chartSideColors = [ + "var(--hm-chart-1-side)", + "var(--hm-chart-2-side)", + "var(--hm-chart-3-side)", + "var(--hm-chart-4-side)", + "var(--hm-chart-5-side)", +] as const + +/** + * Side-wall color of a solid slice. An HM series color (default or passed explicitly) maps to its darker HM token; any other CSS + * color is darkened where it is drawn, so no shared token changes. + */ +export function seriesSideColor(index: number, color?: string) { + if (color === undefined) return chartSideColors[index % chartSideColors.length] + const known = (chartColors as readonly string[]).indexOf(color) + return known >= 0 ? chartSideColors[known] : `color-mix(in srgb, ${color} 68%, black)` +} + /** One data series. `key` becomes a CSS variable name (`--color-`), so use letters, digits, `-` or `_` only. */ export type ChartSeries = { key: string diff --git a/packages/chart/src/components/solid-chart.tsx b/packages/chart/src/components/solid-chart.tsx new file mode 100644 index 0000000..1190ccc --- /dev/null +++ b/packages/chart/src/components/solid-chart.tsx @@ -0,0 +1,271 @@ +import { useId, useState, type MouseEvent, type ReactNode } from "react" +import { cn } from "cn" +import { Pie, PieChart, usePlotArea, type PieSectorShapeProps } from "recharts" +import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "./chart" +import { CenterText, type HMDonutDatum } from "./donut-chart" +import { seriesSideColor, seriesToConfig } from "./series" + +/* + * Solid (thick) Pie and Donut, drawn as SVG - not a rotatable 3D scene. + * + * The disc is seen from above at a fixed tilt: the top face is an ellipse (rx, ry = rx * TILT) and the walls are that ellipse + * shifted down by the thickness. Only Recharts' sector angles are used (angles, order and tooltip stay the real data); the + * geometry is projected here, so text and tooltips are never skewed. + * + * Draw order inside one slice, which is also the whole occlusion rule (slices are flush, so no slice hides another): + * 1. front part of the outer wall (angles 180-360, the half that faces the viewer; the back wall is hidden by the top face) + * 2. back part of the inner wall (donut only, angles 0-180, the half seen through the hole; clipped to the hole opening so + * it never paints over the front part of the ring) + * 3. top face + * Walls and the top face of different slices never overlap, so the layer order of the slices does not matter. Every part sits in + * the slice's own Recharts layer, so hovering the top or a wall gives the same tooltip. + */ + +const RAD = Math.PI / 180 +const TILT = 0.6 // ry / rx of the top ellipse +const THICKNESS = 0.2 // wall height / rx +const MIN_THICKNESS = 8 +const HOLE = 0.55 // donut hole radius / outer radius +const SIDE_PAD = 4 +const MIN_SPAN = 0.01 // degrees; narrower slices are not drawn +const FULL = 359.99 + +type Projection = { cx: number; cy: number; rx: number; ry: number; h: number } + +/** Largest tilted disc (with its wall) that fits the plot area, centered in it. Scales with the chart width on small screens. */ +function project(plot: { x: number; y: number; width: number; height: number }): Projection { + const fit = Math.min((plot.width - SIDE_PAD * 2) / 2, (plot.height - SIDE_PAD * 2) / (2 * TILT + THICKNESS), plot.height * 0.55) + const rx = Math.max(fit, 8) + const h = Math.max(rx * THICKNESS, MIN_THICKNESS) + const ry = rx * TILT + const top = plot.y + (plot.height - (2 * ry + h)) / 2 + return { cx: plot.x + plot.width / 2, cy: top + ry, rx, ry, h } +} + +const num = (value: number) => Math.round(value * 100) / 100 +/** Point on the (optionally scaled) top ellipse at a Recharts angle (degrees, counter-clockwise from 3 o'clock). */ +const at = (p: Projection, scale: number, angle: number, dy = 0) => + `${num(p.cx + p.rx * scale * Math.cos(angle * RAD))},${num(p.cy - p.ry * scale * Math.sin(angle * RAD) + dy)}` + +/** Elliptical arc between two angles. `forward` follows increasing angles (counter-clockwise on screen). */ +function arc(p: Projection, scale: number, from: number, to: number, forward: boolean, dy = 0) { + const span = Math.abs(to - from) + return `A${num(p.rx * scale)},${num(p.ry * scale)} 0 ${span > 180 ? 1 : 0} ${forward ? 0 : 1} ${at(p, scale, to, dy)}` +} + +/** Closed outline of a full ellipse (two half arcs, since one arc cannot start and end at the same point). */ +const ellipse = (p: Projection, scale: number, dy = 0) => + `M${at(p, scale, 0, dy)}A${num(p.rx * scale)},${num(p.ry * scale)} 0 1 0 ${at(p, scale, 180, dy)}A${num(p.rx * scale)},${num(p.ry * scale)} 0 1 0 ${at(p, scale, 360, dy)}Z` + +/** Top face of one slice: a wedge (pie) or a ring segment (donut). */ +function topPath(p: Projection, hole: number, from: number, to: number) { + if (to - from >= FULL) return hole > 0 ? `${ellipse(p, 1)}${ellipse(p, hole)}` : ellipse(p, 1) + if (hole <= 0) return `M${p.cx},${p.cy}L${at(p, 1, from)}${arc(p, 1, from, to, true)}Z` + return `M${at(p, 1, from)}${arc(p, 1, from, to, true)}L${at(p, hole, to)}${arc(p, hole, to, from, false)}Z` +} + +/** Wall band along an ellipse between two angles: the top edge, then the same edge `h` lower, walked back. */ +function wallPath(p: Projection, scale: number, from: number, to: number) { + return `M${at(p, scale, from)}${arc(p, scale, from, to, true)}L${at(p, scale, to, p.h)}${arc(p, scale, to, from, false, p.h)}Z` +} + +type SolidSliceProps = { + sector: PieSectorShapeProps + hole: number + sideColor: string + topColor: string + center?: ReactNode | ((p: Projection) => ReactNode) +} + +function SolidSlice({ sector, hole, sideColor, topColor, center }: SolidSliceProps) { + const plot = usePlotArea() + const clipId = `solid-hole-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}` + if (!plot || plot.width <= 0 || plot.height <= 0) return null + const p = project(plot) + const from = Math.max(Math.min(sector.startAngle, sector.endAngle), 0) + const to = Math.min(Math.max(sector.startAngle, sector.endAngle), 360) + const drawn = to - from > MIN_SPAN + const frontFrom = Math.max(from, 180) + const frontTo = Math.min(to, 360) + const backFrom = Math.max(from, 0) + const backTo = Math.min(to, 180) + const seam = "var(--hm-sem-bg-surface)" + // The seam between slices is a surface-colored stroke; on a very thin slice it thins out so the slice stays visible. + const arcPx = (to - from) * RAD * p.rx + const seamTop = arcPx < 4 ? 0.5 : arcPx < 10 ? 1 : 2 + const seamWall = Math.min(1.5, seamTop) + return ( + + {drawn && frontTo - frontFrom > MIN_SPAN ? ( + + ) : null} + {drawn && hole > 0 && backTo - backFrom > MIN_SPAN ? ( + <> + + + + + + ) : null} + {drawn ? ( + + ) : null} + {typeof center === "function" ? center(p) : center} + + ) +} + +/** + * Tooltip position that follows the pointer. Recharts places a pie tooltip from the circle it would draw (not from the tilted + * ellipse), which would put it away from the hovered face; the pointer is the same point for the top face and the walls. + * Only the position changes: the tooltip content and style are the regular ones. + */ +function useFollowTooltip() { + const [position, setPosition] = useState<{ x: number; y: number } | undefined>() + const onMouseMove = (_data: unknown, _index: number, event: MouseEvent) => { + const svg = event.currentTarget.ownerSVGElement + const wrapper = svg?.closest(".recharts-wrapper") + if (!svg || !wrapper) return + const box = svg.getBoundingClientRect() + const tip = wrapper.querySelector(".recharts-tooltip-wrapper") + const width = tip?.offsetWidth ?? 0 + const height = tip?.offsetHeight ?? 0 + const px = event.clientX - box.left + const py = event.clientY - box.top + let x = px + 14 + if (x + width > box.width) x = px - 14 - width + let y = py + 14 + if (y + height > box.height) y = py - 14 - height + setPosition({ x: Math.max(0, x), y: Math.max(0, y) }) + } + return { position, onMouseMove } +} + +const sliceColors = (data: readonly HMDonutDatum[]) => + data.map((item, index) => ({ id: item.id, top: `var(--color-${item.id})`, side: seriesSideColor(index, item.color) })) + +/** Neutral stand-in drawn when the total is 0, so an empty chart still shows a disc / ring. */ +const EMPTY_TOP = "var(--hm-sem-bg-subtle)" +const EMPTY_SIDE = "var(--hm-sem-stroke-default)" + +export type HMSolidPieChartProps = { + data: readonly HMDonutDatum[] + /** Accessible name of the chart. */ + label: string + className?: string +} + +/** + * Solid pie chart: a thick tilted disc with HM series colors on the top face and a darker tone of the same family on the sides. + * Same data, order, angles, tooltip and legend as `HMPieChart`. Drawn as SVG (no rotation). + */ +export function HMSolidPieChart({ data, label, className }: HMSolidPieChartProps) { + const follow = useFollowTooltip() + const total = data.reduce((sum, item) => sum + Math.max(item.value, 0), 0) + const slices = data.map(item => ({ ...item, fill: `var(--color-${item.id})` })) + const config = seriesToConfig(data.map(item => ({ key: item.id, label: item.label, color: item.color }))) + const colors = sliceColors(data) + return ( + + + } position={follow.position} /> + } /> + {total <= 0 ? ( + } + /> + ) : null} + ( + + )} + /> + + + ) +} + +export type HMSolidDonutChartProps = { + data: readonly HMDonutDatum[] + /** Caption under the total in the hole. Omit it to hide the center total. */ + totalLabel?: string + /** Accessible name of the chart. */ + label: string + className?: string +} + +/** + * Solid donut chart: a thick tilted ring with an outer wall and an inner wall (the back of the hole). The center total sits in + * the tilted hole. Same data, order, angles, tooltip and legend as `HMDonutChart`. A total of 0 draws an empty ring. + */ +export function HMSolidDonutChart({ data, totalLabel, label, className }: HMSolidDonutChartProps) { + const follow = useFollowTooltip() + const total = data.reduce((sum, item) => sum + Math.max(item.value, 0), 0) + const slices = data.map(item => ({ ...item, fill: `var(--color-${item.id})` })) + const config = seriesToConfig(data.map(item => ({ key: item.id, label: item.label, color: item.color }))) + const colors = sliceColors(data) + // The center text is drawn once, with the first slice that is actually drawn (a 0 slice has no layer). + const centerIndex = data.findIndex(item => item.value > 0) + + const centerOf = (p: Projection) => { + if (!totalLabel) return null + const holeRx = p.rx * HOLE + // The wall hides the top `h` of the hole opening, so the text sits lower; its size follows the free height of the opening. + const free = 2 * p.ry * HOLE - p.h + const maxSize = Math.max(14, Math.min(24, Math.floor((free * 0.9 - 26) / 0.75))) + return ( + + + + ) + } + + return ( + + + } position={follow.position} /> + } /> + {total <= 0 ? ( + ( + + )} + /> + ) : null} + ( + 0 && index === centerIndex ? centerOf : undefined} + /> + )} + /> + + + ) +} diff --git a/packages/chart/src/examples/chart-examples.tsx b/packages/chart/src/examples/chart-examples.tsx index 8f47d78..e8d9d09 100644 --- a/packages/chart/src/examples/chart-examples.tsx +++ b/packages/chart/src/examples/chart-examples.tsx @@ -14,7 +14,7 @@ import { SelectTrigger, SelectValue, } from "@hm-design/ui" -import { HMBarChart, HMDonutChart, HMHorizontalBarChart, HMLineChart, HMSignedBarChart } from "../index" +import { HMBarChart, HMDonutChart, HMHorizontalBarChart, HMLineChart, HMSignedBarChart, HMSolidDonutChart } from "../index" import { sampleBar, sampleDataNotice, sampleDonut, sampleDonutStates, sampleHorizontalBar, sampleLine, sampleLineCompare, sampleLinePointColors, sampleSignedBar } from "./sample-data" /** Marks a chart as sample data. Use it next to every chart that is fed with `sample-data`. */ @@ -22,13 +22,13 @@ export function SampleDataBadge() { return 예시 데이터 } -export function ExampleCard({ title, description, className, toolbar, children }: { title: string; description: string; className?: string; toolbar?: ReactNode; children: ReactNode }) { +export function ExampleCard({ title, description, className, toolbar, children }: { title: string; description?: string; className?: string; toolbar?: ReactNode; children: ReactNode }) { return ( // h-full lets a card fill its grid cell; the notice sits at the card bottom so cards of one row line up. {title} - {description} + {description ? {description} : null} @@ -165,6 +165,14 @@ export function DonutCenterExample({ className }: { className?: string }) { ) } +export function DonutSolidExample({ className }: { className?: string }) { + return ( + + + + ) +} + export function DonutActiveExample({ className }: { className?: string }) { const selected = sampleDonutStates.data.find(item => item.id === sampleDonutStates.activeId) return ( diff --git a/packages/chart/src/examples/index.ts b/packages/chart/src/examples/index.ts index a79473e..6e338d4 100644 --- a/packages/chart/src/examples/index.ts +++ b/packages/chart/src/examples/index.ts @@ -1,12 +1,13 @@ // Sample data and ready-made example cards. Everything here is illustrative, not business data. export { sampleBar, sampleLine, sampleDonut, sampleDonutStates, sampleHorizontalBar, sampleSignedBar, sampleLineCompare, sampleLinePointColors, sampleDataNotice } from "./sample-data" -export { BarChartExample, StackedBarChartExample, HorizontalBarChartExample, SignedBarChartExample, LineChartExample, LineCurveExample, LineCurveDotLabelExample, LineStraightExample, LineStraightColorLabelExample, LineStepExample, LineStepDotLabelExample, DonutChartExample, DonutPlainExample, DonutCenterExample, DonutActiveExample, DonutInteractiveExample, ChartExamples, SampleDataBadge } from "./chart-examples" +export { BarChartExample, StackedBarChartExample, HorizontalBarChartExample, SignedBarChartExample, LineChartExample, LineCurveExample, LineCurveDotLabelExample, LineStraightExample, LineStraightColorLabelExample, LineStepExample, LineStepDotLabelExample, DonutChartExample, DonutPlainExample, DonutCenterExample, DonutSolidExample, DonutActiveExample, DonutInteractiveExample, ChartExamples, SampleDataBadge } from "./chart-examples" export { samplePie, sampleStackedPie, sampleRadar } from "./sample-data" export { PiePlainExample, PieLabelExample, PieLabelListExample, PieStackedExample, + PieSolidExample, RadarPlainExample, RadarDotExample, RadarLinesOnlyExample, diff --git a/packages/chart/src/examples/pie-radar-area-examples.tsx b/packages/chart/src/examples/pie-radar-area-examples.tsx index 246a5a3..b7a9919 100644 --- a/packages/chart/src/examples/pie-radar-area-examples.tsx +++ b/packages/chart/src/examples/pie-radar-area-examples.tsx @@ -1,4 +1,4 @@ -import { HMAreaChart, HMPieChart, HMRadarChart, HMStackedPieChart } from "../index" +import { HMAreaChart, HMPieChart, HMRadarChart, HMSolidPieChart, HMStackedPieChart } from "../index" import { ExampleCard } from "./chart-examples" import { sampleLineCompare, sampleLinePointColors, samplePie, sampleRadar, sampleStackedPie } from "./sample-data" @@ -34,6 +34,14 @@ export function PieStackedExample({ className }: Props) { ) } +export function PieSolidExample({ className }: Props) { + return ( + + + + ) +} + export function RadarPlainExample({ className }: Props) { return ( diff --git a/packages/chart/src/index.ts b/packages/chart/src/index.ts index dcc6007..4ad5267 100644 --- a/packages/chart/src/index.ts +++ b/packages/chart/src/index.ts @@ -27,6 +27,8 @@ export type { ChartSeries } from "./components/series" export { HMPieChart, HMStackedPieChart } from "./components/pie-chart" export type { HMPieChartProps, HMPieRing, HMStackedPieChartProps } from "./components/pie-chart" +export { HMSolidPieChart, HMSolidDonutChart } from "./components/solid-chart" +export type { HMSolidPieChartProps, HMSolidDonutChartProps } from "./components/solid-chart" export { HMRadarChart } from "./components/radar-chart" export type { HMRadarChartProps } from "./components/radar-chart" export { HMAreaChart } from "./components/area-chart" diff --git a/src/chart/donut-chart.stories.tsx b/src/chart/donut-chart.stories.tsx index e0b1c9f..e061828 100644 --- a/src/chart/donut-chart.stories.tsx +++ b/src/chart/donut-chart.stories.tsx @@ -4,6 +4,7 @@ import { DonutCenterExample, DonutInteractiveExample, DonutPlainExample, + DonutSolidExample, } from "@hm-design/chart/examples" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" @@ -34,6 +35,9 @@ export const Donut: Story = { + + + ), diff --git a/src/chart/pie-chart.stories.tsx b/src/chart/pie-chart.stories.tsx index 796247c..f991896 100644 --- a/src/chart/pie-chart.stories.tsx +++ b/src/chart/pie-chart.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite" -import { PieLabelExample, PieLabelListExample, PiePlainExample, PieStackedExample } from "@hm-design/chart/examples" +import { PieLabelExample, PieLabelListExample, PiePlainExample, PieSolidExample, PieStackedExample } from "@hm-design/chart/examples" import { StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" // Sample data only. All pies share one item list and color mapping; the stacked pie draws two series as concentric rings. @@ -29,6 +29,9 @@ export const Pie: Story = { + + + ),