{
  "$schema": "https://pdfx.akashpise.dev/schema/registry-item.json",
  "name": "graph",
  "type": "registry:ui",
  "title": "Graph",
  "description": "SVG chart component with bar, line, area, pie, donut, and horizontal-bar variants",
  "files": [
    {
      "path": "components/pdfx/graph/pdfx-graph.tsx",
      "content": "import { Circle, G, Line, Path, Rect, Svg, Text as SvgText } from '@react-pdf/renderer';\nimport { Text as PDFText, View } from '@react-pdf/renderer';\nimport type { Style } from '@react-pdf/types';\nimport type React from 'react';\nimport { usePdfxTheme, useSafeMemo } from '../lib/pdfx-theme-context';\ntype PdfxTheme = ReturnType<typeof usePdfxTheme>;\nimport { createGraphStyles } from './pdfx-graph.styles';\nimport type { ChartLayout, GraphProps, GraphSeries } from './pdfx-graph.types';\nimport {\n  GRAPH_SAFE_WIDTHS,\n  arcPath,\n  buildLayout,\n  fmtNum,\n  getDefaultPalette,\n  getGraphWidth,\n  normalizeData,\n  polarToCartesian,\n  smoothPath,\n  truncate,\n} from './graph.utils';\n\n/**\n * Shared Y-axis grid lines and tick labels for cartesian charts (bar, line, area).\n * Extracted to avoid duplicating the identical block across render functions.\n */\nfunction renderGridAndYAxis(\n  ticks: number[],\n  toY: (v: number) => number,\n  chartX: number,\n  chartW: number,\n  showGrid: boolean,\n  gridColor: string,\n  textColor: string\n) {\n  return (\n    <>\n      {ticks.map((tick) => {\n        const ty = toY(tick);\n        return (\n          <G key={`grid-${tick}`}>\n            {showGrid && (\n              <Line\n                x1={chartX}\n                y1={ty}\n                x2={chartX + chartW}\n                y2={ty}\n                stroke={gridColor}\n                strokeWidth={0.5}\n                strokeDasharray=\"3 3\"\n              />\n            )}\n            <SvgText\n              x={chartX - 4}\n              y={ty + 3}\n              fill={textColor}\n              textAnchor=\"end\"\n              style={{ fontSize: 7 }}\n            >\n              {fmtNum(tick)}\n            </SvgText>\n          </G>\n        );\n      })}\n    </>\n  );\n}\n\nfunction renderBarChart(\n  series: GraphSeries[],\n  layout: ChartLayout,\n  palette: string[],\n  showGrid: boolean,\n  showValues: boolean,\n  theme: PdfxTheme\n) {\n  const { chartX, chartY, chartW, chartH, yMin, yMax, yTicks, xLabels } = layout;\n  const nCategories = xLabels.length;\n  const nSeries = series.length;\n  const groupGap = 0.25;\n  const groupW = chartW / nCategories;\n  const barW = (groupW * (1 - groupGap)) / nSeries;\n  const textColor = theme.colors.mutedForeground;\n  const gridColor = theme.colors.border;\n  const axisColor = theme.colors.foreground;\n  const range = yMax - yMin || 1;\n  const toY = (v: number) => chartY + chartH - ((v - yMin) / range) * chartH;\n\n  return (\n    <>\n      {renderGridAndYAxis(yTicks, toY, chartX, chartW, showGrid, gridColor, textColor)}\n\n      <Line\n        x1={chartX}\n        y1={chartY + chartH}\n        x2={chartX + chartW}\n        y2={chartY + chartH}\n        stroke={axisColor}\n        strokeWidth={1}\n      />\n\n      {xLabels.map((label, ci) => {\n        const groupLeft = chartX + ci * groupW + groupW * (groupGap / 2);\n        return (\n          // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n          <G key={`group-${ci}`}>\n            {series.map((s, si) => {\n              const val = s.data[ci]?.value ?? 0;\n              const color = s.data[ci]?.color ?? s.color ?? palette[si % palette.length];\n              const barH = ((val - yMin) / range) * chartH;\n              const bx = groupLeft + si * barW;\n              const by = chartY + chartH - barH;\n              return (\n                // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n                <G key={`bar-${ci}-${si}`}>\n                  <Rect x={bx} y={by} width={barW - 1} height={barH} fill={color} />\n                  {showValues && barH > 10 && (\n                    <SvgText\n                      x={bx + barW / 2 - 0.5}\n                      y={by - 2}\n                      fill={axisColor}\n                      textAnchor=\"middle\"\n                      style={{ fontSize: 6 }}\n                    >\n                      {fmtNum(val)}\n                    </SvgText>\n                  )}\n                </G>\n              );\n            })}\n            <SvgText\n              x={groupLeft + (nSeries * barW) / 2}\n              y={chartY + chartH + 10}\n              fill={textColor}\n              textAnchor=\"middle\"\n              style={{ fontSize: 7 }}\n            >\n              {truncate(label, 10)}\n            </SvgText>\n          </G>\n        );\n      })}\n    </>\n  );\n}\n\nfunction renderHorizontalBarChart(\n  series: GraphSeries[],\n  layout: ChartLayout,\n  palette: string[],\n  showValues: boolean,\n  theme: PdfxTheme\n) {\n  const { chartX, chartY, chartW, chartH, xLabels } = layout;\n  const nCategories = xLabels.length;\n  const allValues = series.flatMap((s) => s.data.map((d) => d.value));\n  const maxVal = Math.max(...allValues, 1);\n  const rowH = chartH / nCategories;\n  const barH = rowH * 0.5;\n  const textColor = theme.colors.mutedForeground;\n  const axisColor = theme.colors.foreground;\n  const labelW = 60;\n\n  return (\n    <>\n      {xLabels.map((label, ci) => {\n        const rowY = chartY + ci * rowH;\n        const val = series[0]?.data[ci]?.value ?? 0;\n        const color =\n          series[0]?.data[ci]?.color ?? series[0]?.color ?? palette[ci % palette.length];\n        const barW = (val / maxVal) * (chartW - labelW);\n        return (\n          // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n          <G key={`hbar-${ci}`}>\n            <SvgText\n              x={chartX + labelW - 4}\n              y={rowY + rowH / 2 + 3}\n              fill={textColor}\n              textAnchor=\"end\"\n              style={{ fontSize: 7 }}\n            >\n              {truncate(label, 14)}\n            </SvgText>\n            <Rect\n              x={chartX + labelW}\n              y={rowY + (rowH - barH) / 2}\n              width={Math.max(barW, 1)}\n              height={barH}\n              fill={color}\n            />\n            {showValues && (\n              <SvgText\n                x={chartX + labelW + barW + 3}\n                y={rowY + rowH / 2 + 3}\n                fill={axisColor}\n                textAnchor=\"start\"\n                style={{ fontSize: 6 }}\n              >\n                {fmtNum(val)}\n              </SvgText>\n            )}\n          </G>\n        );\n      })}\n      <Line\n        x1={chartX + labelW}\n        y1={chartY}\n        x2={chartX + labelW}\n        y2={chartY + chartH}\n        stroke={axisColor}\n        strokeWidth={1}\n      />\n    </>\n  );\n}\n\nfunction renderLineAreaChart(\n  series: GraphSeries[],\n  layout: ChartLayout,\n  palette: string[],\n  showGrid: boolean,\n  showValues: boolean,\n  showDots: boolean,\n  smooth: boolean,\n  isArea: boolean,\n  theme: PdfxTheme\n) {\n  const { chartX, chartY, chartW, chartH, yMin, yMax, yTicks, xLabels } = layout;\n  const range = yMax - yMin || 1;\n  const textColor = theme.colors.mutedForeground;\n  const gridColor = theme.colors.border;\n  const axisColor = theme.colors.foreground;\n  const nPoints = xLabels.length;\n\n  const xFor = (i: number) => chartX + (i / Math.max(nPoints - 1, 1)) * chartW;\n  const yFor = (v: number) => chartY + chartH - ((v - yMin) / range) * chartH;\n\n  return (\n    <>\n      {renderGridAndYAxis(yTicks, yFor, chartX, chartW, showGrid, gridColor, textColor)}\n\n      <Line\n        x1={chartX}\n        y1={chartY + chartH}\n        x2={chartX + chartW}\n        y2={chartY + chartH}\n        stroke={axisColor}\n        strokeWidth={1}\n      />\n\n      {series.map((s, si) => {\n        const color = s.color ?? palette[si % palette.length];\n        const points = s.data.map((d, i) => ({ x: xFor(i), y: yFor(d.value) }));\n\n        const lineDStr = smooth\n          ? smoothPath(points)\n          : `M ${points.map((p) => `${p.x} ${p.y}`).join(' L ')}`;\n\n        const areaPath =\n          isArea && points.length > 1\n            ? `${lineDStr} L ${points[points.length - 1].x} ${chartY + chartH} L ${points[0].x} ${chartY + chartH} Z`\n            : null;\n\n        return (\n          // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n          <G key={`series-${si}`}>\n            {isArea && areaPath && (\n              <Path d={areaPath} fill={color} fillOpacity={0.2} stroke=\"none\" />\n            )}\n            <Path d={lineDStr} stroke={color} strokeWidth={2} fill=\"none\" />\n            {showDots &&\n              points.map((p, pi) => (\n                // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n                <Circle key={`dot-${pi}`} cx={p.x} cy={p.y} r={3} fill={color} />\n              ))}\n            {showValues &&\n              points.map((p, pi) => (\n                <SvgText\n                  // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n                  key={`val-${pi}`}\n                  x={p.x}\n                  y={p.y - 5}\n                  fill={color}\n                  textAnchor=\"middle\"\n                  style={{ fontSize: 6 }}\n                >\n                  {fmtNum(s.data[pi].value)}\n                </SvgText>\n              ))}\n          </G>\n        );\n      })}\n\n      {xLabels.map((label, i) => (\n        <SvgText\n          key={`xlabel-${label}`}\n          x={xFor(i)}\n          y={chartY + chartH + 10}\n          fill={textColor}\n          textAnchor=\"middle\"\n          style={{ fontSize: 7 }}\n        >\n          {truncate(label, 8)}\n        </SvgText>\n      ))}\n    </>\n  );\n}\n\nfunction renderPieDonutChart(\n  series: GraphSeries[],\n  layout: ChartLayout,\n  palette: string[],\n  centerLabel: string | undefined,\n  isDonut: boolean,\n  theme: PdfxTheme\n) {\n  const { svgW, svgH } = layout;\n  const cx = svgW / 2;\n  const cy = svgH / 2;\n  const r = Math.min(svgW, svgH) / 2 - 20;\n  const innerR = isDonut ? r * 0.52 : 0;\n  const textColor = theme.colors.mutedForeground;\n\n  const data = series[0]?.data ?? [];\n  const total = data.reduce((sum, d) => sum + d.value, 0) || 1;\n\n  let currentAngle = 0;\n\n  return (\n    <>\n      {data.map((d, i) => {\n        const color = d.color ?? palette[i % palette.length];\n        const sweep = (d.value / total) * 360;\n        const midAngle = currentAngle + sweep / 2;\n        const path = arcPath(cx, cy, r, currentAngle, currentAngle + sweep, innerR);\n        currentAngle += sweep;\n\n        const labelR = r * 1.18;\n        const lp = polarToCartesian(cx, cy, labelR, midAngle);\n        const anchor = lp.x > cx ? 'start' : 'end';\n\n        return (\n          // biome-ignore lint/suspicious/noArrayIndexKey: static PDF chart data — index is the stable identity\n          <G key={`slice-${i}`}>\n            <Path d={path} fill={color} stroke=\"white\" strokeWidth={1} />\n            {/* Show label only if slice is large enough to label */}\n            {sweep > 15 && (\n              <SvgText\n                x={lp.x}\n                y={lp.y + 3}\n                fill={textColor}\n                textAnchor={anchor}\n                style={{ fontSize: 7 }}\n              >\n                {truncate(d.label, 10)}\n              </SvgText>\n            )}\n          </G>\n        );\n      })}\n\n      {isDonut && centerLabel && (\n        <>\n          <Circle cx={cx} cy={cy} r={innerR} fill=\"white\" />\n          <SvgText\n            x={cx}\n            y={cy + 4}\n            fill={theme.colors.foreground}\n            textAnchor=\"middle\"\n            style={{ fontSize: 9, fontWeight: 'bold' }}\n          >\n            {centerLabel}\n          </SvgText>\n        </>\n      )}\n    </>\n  );\n}\n\nfunction Legend({\n  series,\n  palette,\n  styles,\n  position = 'bottom',\n}: {\n  series: GraphSeries[];\n  palette: string[];\n  styles: ReturnType<typeof createGraphStyles>;\n  position?: 'bottom' | 'right';\n}) {\n  const containerStyle = position === 'right' ? styles.legendColumn : styles.legendRow;\n\n  return (\n    <View style={containerStyle}>\n      {series.map((s, i) => (\n        <View key={s.name} style={styles.legendItem}>\n          <Svg width={10} height={10}>\n            <Rect x={0} y={2} width={8} height={8} fill={s.color ?? palette[i % palette.length]} />\n          </Svg>\n          <PDFText style={styles.legendText}>{s.name}</PDFText>\n        </View>\n      ))}\n    </View>\n  );\n}\n\n/**\n * PdfGraph — renders bar, horizontal-bar, line, area, pie, and donut charts\n * natively inside react-pdf documents using SVG primitives.\n *\n * No external chart libraries are required or used — all rendering is done via\n * react-pdf's built-in SVG support (`<Svg>`, `<Rect>`, `<Path>`, `<Line>`, etc.).\n *\n * @example Bar chart\n * ```tsx\n * <PdfGraph\n *   variant=\"bar\"\n *   title=\"Monthly Revenue\"\n *   data={[\n *     { label: 'Jan', value: 42000 },\n *     { label: 'Feb', value: 38000 },\n *     { label: 'Mar', value: 55000 },\n *   ]}\n * />\n * ```\n *\n * @example Donut chart with center label\n * ```tsx\n * <PdfGraph\n *   variant=\"donut\"\n *   data={[\n *     { label: 'Product A', value: 45 },\n *     { label: 'Product B', value: 30 },\n *     { label: 'Other', value: 25 },\n *   ]}\n *   centerLabel=\"$1.2M\"\n * />\n * ```\n *\n * **Limitations (by design):**\n * - No interactivity (PDFs are static)\n * - No animations\n * - SVG Text inside charts uses SVG font attributes (not react-pdf StyleSheet fonts)\n * - For print PDFs use SVG-friendly fonts registered with Font.register()\n */\nexport function PdfGraph({\n  variant = 'bar',\n  data,\n  title,\n  subtitle,\n  xLabel,\n  yLabel,\n  width: explicitWidth,\n  height = 260,\n  fullWidth = false,\n  containerPadding = 0,\n  wrapperPadding = 0,\n  colors,\n  showValues = false,\n  showGrid = true,\n  legend = 'bottom',\n  centerLabel,\n  showDots = true,\n  smooth = false,\n  yTicks: yTickCount = 5,\n  noWrap = true,\n  style,\n}: GraphProps) {\n  const theme = usePdfxTheme();\n  const styles = useSafeMemo(() => createGraphStyles(theme), [theme]);\n  const palette = colors ?? getDefaultPalette(theme);\n  const series = normalizeData(data);\n\n  const width = useSafeMemo(() => {\n    if (fullWidth) return getGraphWidth(theme, { containerPadding, wrapperPadding });\n    return explicitWidth ?? GRAPH_SAFE_WIDTHS.default;\n  }, [fullWidth, explicitWidth, theme, containerPadding, wrapperPadding]);\n\n  const isPieOrDonut = variant === 'pie' || variant === 'donut';\n  const layout = buildLayout(series, width, height, isPieOrDonut, yTickCount);\n  const { chartX, chartW } = layout;\n\n  let chartContent: React.ReactNode = null;\n\n  switch (variant) {\n    case 'bar':\n      chartContent = renderBarChart(series, layout, palette, showGrid, showValues, theme);\n      break;\n    case 'horizontal-bar':\n      chartContent = renderHorizontalBarChart(series, layout, palette, showValues, theme);\n      break;\n    case 'line':\n    case 'area':\n      chartContent = renderLineAreaChart(\n        series,\n        layout,\n        palette,\n        showGrid,\n        showValues,\n        showDots,\n        smooth,\n        variant === 'area',\n        theme\n      );\n      break;\n    case 'pie':\n      chartContent = renderPieDonutChart(series, layout, palette, undefined, false, theme);\n      break;\n    case 'donut':\n      chartContent = renderPieDonutChart(series, layout, palette, centerLabel, true, theme);\n      break;\n  }\n\n  const showLegend = legend !== 'none' && !isPieOrDonut;\n\n  const containerStyles: Style[] = [styles.container];\n  if (style) containerStyles.push(style);\n\n  const content = (\n    <View style={containerStyles}>\n      {title && <PDFText style={styles.title}>{title}</PDFText>}\n      {subtitle && <PDFText style={styles.subtitle}>{subtitle}</PDFText>}\n      <View style={legend === 'right' ? styles.chartWithRightLegend : undefined}>\n        <Svg width={width} height={height}>\n          {chartContent}\n          {!isPieOrDonut && xLabel && (\n            <SvgText\n              x={chartX + chartW / 2}\n              y={height - 2}\n              fill={theme.colors.mutedForeground}\n              textAnchor=\"middle\"\n              style={{ fontSize: 8 }}\n            >\n              {xLabel}\n            </SvgText>\n          )}\n          {!isPieOrDonut && yLabel && (\n            <SvgText\n              x={2}\n              y={10}\n              fill={theme.colors.mutedForeground}\n              textAnchor=\"start\"\n              style={{ fontSize: 8 }}\n            >\n              {yLabel}\n            </SvgText>\n          )}\n        </Svg>\n        {showLegend && legend === 'right' && Legend({ series, palette, styles, position: 'right' })}\n      </View>\n      {showLegend && legend === 'bottom' && Legend({ series, palette, styles, position: 'bottom' })}\n    </View>\n  );\n\n  return noWrap ? <View wrap={false}>{content}</View> : content;\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/pdfx/graph/pdfx-graph.types.ts",
      "content": "import type { Style } from '@react-pdf/types';\n\n/**\n * Options for calculating graph width based on theme page margins and container context.\n * Props - `containerPadding` | `wrapperPadding` | `pageWidth`\n * @see {@link GraphWidthOptions}\n */\nexport interface GraphWidthOptions {\n  containerPadding?: number;\n  wrapperPadding?: number;\n  pageWidth?: number;\n}\n\nexport type GraphVariant = 'bar' | 'horizontal-bar' | 'line' | 'area' | 'pie' | 'donut';\n\nexport type GraphLegendPosition = 'bottom' | 'right' | 'none';\n\n/**\n * A single data point with a label, numeric value, and optional color override.\n * Props - `label` | `value` | `color`\n * @see {@link GraphDataPoint}\n */\nexport interface GraphDataPoint {\n  label: string;\n  value: number;\n  color?: string;\n}\n\n/**\n * A named data series containing multiple data points, used for multi-series charts.\n * Props - `name` | `data` | `color`\n * @see {@link GraphSeries}\n */\nexport interface GraphSeries {\n  name: string;\n  data: GraphDataPoint[];\n  color?: string;\n}\n\n/**\n * Multi-variant PDF chart (bar, line, area, pie, donut) rendered with SVG primitives.\n * Props - `variant` | `data` | `title` | `subtitle` | `xLabel` | `yLabel` | `width` | `height` | `fullWidth` | `containerPadding` | `wrapperPadding` | `colors` | `showValues` | `showGrid` | `legend` | `centerLabel` | `showDots` | `smooth` | `yTicks` | `noWrap` | `style`\n * @see {@link GraphProps}\n */\nexport interface GraphProps {\n  /**\n   * @default 'bar'\n   */\n  variant?: GraphVariant;\n  data: GraphDataPoint[] | GraphSeries[];\n  title?: string;\n  subtitle?: string;\n  xLabel?: string;\n  yLabel?: string;\n  /**\n   * @default 420\n   */\n  width?: number;\n  /**\n   * @default 260\n   */\n  height?: number;\n  /**\n   * @default false\n   */\n  fullWidth?: boolean;\n  containerPadding?: number;\n  wrapperPadding?: number;\n  colors?: string[];\n  /**\n   * @default false\n   */\n  showValues?: boolean;\n  /**\n   * @default true\n   */\n  showGrid?: boolean;\n  /**\n   * @default 'bottom'\n   */\n  legend?: GraphLegendPosition;\n  centerLabel?: string;\n  /**\n   * @default true\n   */\n  showDots?: boolean;\n  /**\n   * @default false\n   */\n  smooth?: boolean;\n  /**\n   * @default 5\n   */\n  yTicks?: number;\n  /**\n   * @default true\n   */\n  noWrap?: boolean;\n  style?: Style;\n}\n\n/** Internal chart layout dimensions computed from props and data. */\nexport interface ChartLayout {\n  svgW: number;\n  svgH: number;\n  chartX: number;\n  chartY: number;\n  chartW: number;\n  chartH: number;\n  yMin: number;\n  yMax: number;\n  yTicks: number[];\n  xLabels: string[];\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/pdfx/graph/pdfx-graph.styles.ts",
      "content": "import { StyleSheet } from '@react-pdf/renderer';\nimport { usePdfxTheme } from '../lib/pdfx-theme-context';\ntype PdfxTheme = ReturnType<typeof usePdfxTheme>;\n\nexport function createGraphStyles(t: PdfxTheme) {\n  return StyleSheet.create({\n    container: {\n      display: 'flex',\n      flexDirection: 'column',\n      marginBottom: t.spacing.componentGap,\n    },\n    title: {\n      fontFamily: t.typography.heading.fontFamily,\n      fontSize: t.primitives.typography.base,\n      fontWeight: t.primitives.fontWeights.semibold,\n      color: t.colors.foreground,\n      marginBottom: 2,\n    },\n    subtitle: {\n      fontFamily: t.typography.body.fontFamily,\n      fontSize: t.primitives.typography.xs,\n      color: t.colors.mutedForeground,\n      marginBottom: 6,\n    },\n    legendRow: {\n      display: 'flex',\n      flexDirection: 'row',\n      flexWrap: 'wrap',\n      gap: 12,\n      marginTop: 6,\n    },\n    legendColumn: {\n      display: 'flex',\n      flexDirection: 'column',\n      gap: 8,\n      marginLeft: 12,\n      marginTop: 18,\n      minWidth: 120,\n    },\n    legendItem: {\n      display: 'flex',\n      flexDirection: 'row',\n      alignItems: 'center',\n      gap: 4,\n    },\n    legendText: {\n      fontFamily: t.typography.body.fontFamily,\n      fontSize: t.primitives.typography.xs,\n      color: t.colors.mutedForeground,\n    },\n    chartWithRightLegend: {\n      display: 'flex',\n      flexDirection: 'row',\n      alignItems: 'flex-start',\n    },\n  });\n}\n",
      "type": "registry:component"
    },
    {
      "path": "components/pdfx/graph/pdfx-graph.utils.ts",
      "content": "import type { ChartLayout, GraphDataPoint, GraphSeries, GraphWidthOptions } from './pdfx-graph.types';\n\n/** Standard A4 page width in PDF points. */\nexport const A4_WIDTH = 595;\n\n/**\n * Pre-calculated safe graph widths for common scenarios.\n * These values work with all built-in themes on A4 pages.\n */\nexport const GRAPH_SAFE_WIDTHS = {\n  /** Safe width for graph directly in page content (no extra containers) */\n  default: 420,\n  /** Safe width for graph inside a Section with md padding */\n  inSection: 400,\n  /** Safe width for graph inside a Section + bordered wrapper (like graphShell) */\n  inSectionWithWrapper: 380,\n} as const;\n\n/**\n * Calculate the safe graph width based on theme page margins and container context.\n *\n * This utility ensures graphs don't overflow their container by accounting for:\n * - Page margins (from theme)\n * - Container padding (e.g., Section component)\n * - Wrapper padding (e.g., a bordered graphShell View)\n *\n * @example\n * ```tsx\n * const width = getGraphWidth(theme);\n * // For a graph inside a Section with padding=\"md\" (12pt) and a graphShell (12pt):\n * const width = getGraphWidth(theme, { containerPadding: 12, wrapperPadding: 12 });\n * ```\n */\nexport function getGraphWidth(theme: PdfxTheme, options: GraphWidthOptions = {}): number {\n  const { containerPadding = 0, wrapperPadding = 0, pageWidth = A4_WIDTH } = options;\n  const { marginLeft, marginRight } = theme.spacing.page;\n  const availableWidth =\n    pageWidth - marginLeft - marginRight - containerPadding * 2 - wrapperPadding * 2;\n  return Math.max(Math.floor(availableWidth), 100);\n}\n\nexport const CHART_MARGINS = {\n  axisLeft: 40,\n  pieLeft: 10,\n  right: 10,\n  top: 10,\n  axisBottom: 24,\n  pieBottom: 10,\n} as const;\n\n/** Normalize to always work with GraphSeries[]. */\nexport function normalizeData(data: GraphDataPoint[] | GraphSeries[]): GraphSeries[] {\n  if (data.length === 0) return [];\n  if ('label' in data[0] && 'value' in data[0]) {\n    return [{ name: 'Series 1', data: data as GraphDataPoint[] }];\n  }\n  return data as GraphSeries[];\n}\n\n/** Compute nice Y-axis ticks for a given value range. */\nexport function computeYTicks(min: number, max: number, count: number): number[] {\n  if (min === max) return [0, max || 1];\n  const step = (max - min) / (count - 1);\n  return Array.from({ length: count }, (_, i) => {\n    const v = min + i * step;\n    return Math.round(v * 100) / 100;\n  });\n}\n\n/** Format a number for display on axis labels. */\nexport function fmtNum(v: number): string {\n  if (Math.abs(v) >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;\n  if (Math.abs(v) >= 1_000) return `${(v / 1_000).toFixed(1)}K`;\n  if (!Number.isInteger(v)) return v.toFixed(1);\n  return String(v);\n}\n\n/** Truncate a label to at most maxLen characters. */\nexport function truncate(s: string, maxLen: number): string {\n  return s.length > maxLen ? `${s.slice(0, maxLen - 1)}…` : s;\n}\n\n/** Polar to Cartesian coordinate conversion for pie/donut arcs. */\nexport function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {\n  const rad = ((angleDeg - 90) * Math.PI) / 180;\n  return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };\n}\n\n/** Build an SVG arc path for a pie/donut slice. */\nexport function arcPath(\n  cx: number,\n  cy: number,\n  r: number,\n  startAngle: number,\n  endAngle: number,\n  innerR = 0\n): string {\n  const safeEnd = Math.min(endAngle, startAngle + 359.999);\n  const large = safeEnd - startAngle > 180 ? 1 : 0;\n  const s = polarToCartesian(cx, cy, r, safeEnd);\n  const e = polarToCartesian(cx, cy, r, startAngle);\n  if (innerR === 0) {\n    return `M ${cx} ${cy} L ${s.x} ${s.y} A ${r} ${r} 0 ${large} 0 ${e.x} ${e.y} Z`;\n  }\n  const si = polarToCartesian(cx, cy, innerR, safeEnd);\n  const ei = polarToCartesian(cx, cy, innerR, startAngle);\n  return [\n    `M ${s.x} ${s.y}`,\n    `A ${r} ${r} 0 ${large} 0 ${e.x} ${e.y}`,\n    `L ${ei.x} ${ei.y}`,\n    `A ${innerR} ${innerR} 0 ${large} 1 ${si.x} ${si.y}`,\n    'Z',\n  ].join(' ');\n}\n\n/** Compute smooth bezier control points (Catmull-Rom → cubic bezier). */\nexport function smoothPath(points: { x: number; y: number }[], tension = 0.4): string {\n  if (points.length < 2) return '';\n  if (points.length === 2) {\n    return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;\n  }\n  const parts: string[] = [`M ${points[0].x} ${points[0].y}`];\n  for (let i = 0; i < points.length - 1; i++) {\n    const p0 = points[Math.max(i - 1, 0)];\n    const p1 = points[i];\n    const p2 = points[i + 1];\n    const p3 = points[Math.min(i + 2, points.length - 1)];\n    const cp1x = p1.x + (p2.x - p0.x) * tension;\n    const cp1y = p1.y + (p2.y - p0.y) * tension;\n    const cp2x = p2.x - (p3.x - p1.x) * tension;\n    const cp2y = p2.y - (p3.y - p1.y) * tension;\n    parts.push(`C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`);\n  }\n  return parts.join(' ');\n}\n\n/** Get the default chart color palette from the theme. */\nexport function getDefaultPalette(t: PdfxTheme): string[] {\n  return [\n    t.colors.primary,\n    t.colors.info ?? '#3B82F6',\n    t.colors.success ?? '#22C55E',\n    t.colors.warning ?? '#F59E0B',\n    t.colors.destructive ?? '#EF4444',\n    '#8B5CF6',\n    '#F97316',\n    '#14B8A6',\n  ];\n}\n\n/** Derive ChartLayout from series data, SVG dimensions, and variant. */\nexport function buildLayout(\n  series: GraphSeries[],\n  width: number,\n  height: number,\n  isPieOrDonut: boolean,\n  yTickCount: number\n): ChartLayout {\n  const mL = isPieOrDonut ? CHART_MARGINS.pieLeft : CHART_MARGINS.axisLeft;\n  const mB = isPieOrDonut ? CHART_MARGINS.pieBottom : CHART_MARGINS.axisBottom;\n  const chartX = mL;\n  const chartY = CHART_MARGINS.top;\n  const chartW = width - mL - CHART_MARGINS.right;\n  const chartH = height - CHART_MARGINS.top - mB;\n  const allValues = series.flatMap((s) => s.data.map((d) => d.value));\n  const rawMin = Math.min(...allValues, 0);\n  const rawMax = Math.max(...allValues, 1);\n  const yMin = rawMin >= 0 ? 0 : rawMin;\n  const yMax = rawMax + (rawMax - yMin) * 0.08;\n  return {\n    svgW: width,\n    svgH: height,\n    chartX,\n    chartY,\n    chartW,\n    chartH,\n    yMin,\n    yMax,\n    yTicks: computeYTicks(yMin, yMax, yTickCount),\n    xLabels: series[0]?.data.map((d) => d.label) ?? [],\n  };\n}\n",
      "type": "registry:component"
    }
  ],
  "dependencies": [
    "@react-pdf/renderer"
  ],
  "registryDependencies": [
    "theme"
  ]
}