feat(chart): add solid (thick) pie and donut charts
Add HMSolidPieChart and HMSolidDonutChart: a tilted, SVG-drawn disc with an outer wall and, for the donut, an inner wall seen through the hole. Same data, slice angles, order, legend and tooltip as the 2D charts; the tooltip is the same on the top face and the walls and follows the pointer. The existing 2D charts and their props are unchanged. - Side walls use darker HM tokens (--hm-chart-1-side .. --hm-chart-5-side) and a color-mix darkening for custom colors; shared chart tokens are untouched. - Add a "입체형" example to the Pie Chart and Donut Chart stories (PieSolidExample, DonutSolidExample); ExampleCard description is now optional. - Document usage and constraints in the chart package README.
This commit is contained in:
1 parent
dc9258599c
commit
db0bfe2d34
10 files changed
+332
-8
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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-<key>`), so use letters, digits, `-` or `_` only. */
|
||||
export type ChartSeries = {
|
||||
key: string
|
||||
|
||||
@@ -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 (
|
||||
<g data-slot="chart-solid-slice">
|
||||
{drawn && frontTo - frontFrom > MIN_SPAN ? (
|
||||
<path d={wallPath(p, 1, frontFrom, frontTo)} style={{ fill: sideColor }} stroke={seam} strokeWidth={seamWall} strokeLinejoin="round" />
|
||||
) : null}
|
||||
{drawn && hole > 0 && backTo - backFrom > MIN_SPAN ? (
|
||||
<>
|
||||
<clipPath id={clipId}>
|
||||
<path d={ellipse(p, hole)} />
|
||||
</clipPath>
|
||||
<path d={wallPath(p, hole, backFrom, backTo)} clipPath={`url(#${clipId})`} style={{ fill: sideColor }} stroke={seam} strokeWidth={seamWall} strokeLinejoin="round" />
|
||||
</>
|
||||
) : null}
|
||||
{drawn ? (
|
||||
<path d={topPath(p, hole, from, to)} fillRule="evenodd" style={{ fill: topColor }} stroke={seam} strokeWidth={seamTop} strokeLinejoin="round" />
|
||||
) : null}
|
||||
{typeof center === "function" ? center(p) : center}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<SVGElement>) => {
|
||||
const svg = event.currentTarget.ownerSVGElement
|
||||
const wrapper = svg?.closest(".recharts-wrapper")
|
||||
if (!svg || !wrapper) return
|
||||
const box = svg.getBoundingClientRect()
|
||||
const tip = wrapper.querySelector<HTMLElement>(".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 (
|
||||
<ChartContainer config={config} role="group" aria-label={label} data-solid="pie" className={cn("aspect-auto h-72 w-full sm:h-80", className)}>
|
||||
<PieChart accessibilityLayer>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel nameKey="id" />} position={follow.position} />
|
||||
<ChartLegend itemSorter={null} content={<ChartLegendContent nameKey="id" className="flex-wrap gap-x-4 gap-y-1" />} />
|
||||
{total <= 0 ? (
|
||||
<Pie
|
||||
data={[{ value: 1 }]}
|
||||
dataKey="value"
|
||||
isAnimationActive={false}
|
||||
legendType="none"
|
||||
tooltipType="none"
|
||||
shape={(sector: PieSectorShapeProps) => <SolidSlice sector={sector} hole={0} topColor={EMPTY_TOP} sideColor={EMPTY_SIDE} />}
|
||||
/>
|
||||
) : null}
|
||||
<Pie
|
||||
data={slices}
|
||||
dataKey="value"
|
||||
nameKey="id"
|
||||
isAnimationActive={false}
|
||||
onMouseEnter={follow.onMouseMove}
|
||||
onMouseMove={follow.onMouseMove}
|
||||
shape={(sector: PieSectorShapeProps, index: number) => (
|
||||
<SolidSlice sector={sector} hole={0} topColor={colors[index].top} sideColor={colors[index].side} />
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<g pointerEvents="none" data-slot="chart-solid-center">
|
||||
<CenterText cx={p.cx} cy={p.cy} innerRadius={holeRx * 0.92} value={total} caption={totalLabel} maxSize={maxSize} dy={p.h * 0.62} />
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartContainer config={config} role="group" aria-label={label} data-solid="donut" className={cn("aspect-auto h-72 w-full sm:h-80", className)}>
|
||||
<PieChart accessibilityLayer>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel nameKey="id" />} position={follow.position} />
|
||||
<ChartLegend itemSorter={null} content={<ChartLegendContent nameKey="id" className="flex-wrap gap-x-4 gap-y-1" />} />
|
||||
{total <= 0 ? (
|
||||
<Pie
|
||||
data={[{ value: 1 }]}
|
||||
dataKey="value"
|
||||
isAnimationActive={false}
|
||||
legendType="none"
|
||||
tooltipType="none"
|
||||
shape={(sector: PieSectorShapeProps) => (
|
||||
<SolidSlice sector={sector} hole={HOLE} topColor={EMPTY_TOP} sideColor={EMPTY_SIDE} center={centerOf} />
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
<Pie
|
||||
data={slices}
|
||||
dataKey="value"
|
||||
nameKey="id"
|
||||
isAnimationActive={false}
|
||||
onMouseEnter={follow.onMouseMove}
|
||||
onMouseMove={follow.onMouseMove}
|
||||
shape={(sector: PieSectorShapeProps, index: number) => (
|
||||
<SolidSlice
|
||||
sector={sector}
|
||||
hole={HOLE}
|
||||
topColor={colors[index].top}
|
||||
sideColor={colors[index].side}
|
||||
center={total > 0 && index === centerIndex ? centerOf : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
@@ -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 <Badge variant="outline" data-slot="chart-sample-badge" title={sampleDataNotice}>예시 데이터</Badge>
|
||||
}
|
||||
|
||||
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.
|
||||
<Card className={cn("h-full", className)} data-chart-example={title}>
|
||||
<CardHeader>
|
||||
<CardTitle className="break-keep">{title}</CardTitle>
|
||||
<CardDescription className="break-keep">{description}</CardDescription>
|
||||
{description ? <CardDescription className="break-keep">{description}</CardDescription> : null}
|
||||
<CardAction><SampleDataBadge /></CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 flex-col">
|
||||
@@ -165,6 +165,14 @@ export function DonutCenterExample({ className }: { className?: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DonutSolidExample({ className }: { className?: string }) {
|
||||
return (
|
||||
<ExampleCard title="입체형" className={className}>
|
||||
<HMSolidDonutChart data={sampleDonutStates.data} totalLabel={sampleDonutStates.totalLabel} label="업무 상태 입체형 도넛 차트, 가운데 전체 합계(예시 데이터)" />
|
||||
</ExampleCard>
|
||||
)
|
||||
}
|
||||
|
||||
export function DonutActiveExample({ className }: { className?: string }) {
|
||||
const selected = sampleDonutStates.data.find(item => item.id === sampleDonutStates.activeId)
|
||||
return (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<ExampleCard title="입체형" className={className}>
|
||||
<HMSolidPieChart {...samplePie} label="업무 상태 입체형 파이 차트(예시 데이터)" />
|
||||
</ExampleCard>
|
||||
)
|
||||
}
|
||||
|
||||
export function RadarPlainExample({ className }: Props) {
|
||||
return (
|
||||
<ExampleCard title="Radar" description="방사형 차트 · 영역" className={className}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user