import { Line, Path, Rect, Svg, Text, View } from '@react-pdf/renderer' import type { ReactElement } from 'react' import { pdfColors, pdfStyles } from './theme' import { formatLogTick, formatYAxisTick, getCumulativeResults, } from '@/components/ui/QueryBlock/QueryBlock.utils' const CHART_WIDTH = 480 const CHART_HEIGHT = 200 const MAX_X_LABELS = 12 const SERIES_COLORS = [pdfColors.brand, '#3b82f6', '#f59e0b'] interface NotebookPdfChartConfig { type: 'bar' | 'line' x_column: string y_series: readonly string[] cumulative: boolean scale: 'linear' | 'log' show_labels: boolean } interface NotebookPdfChartProps { chart: NotebookPdfChartConfig | undefined rows: readonly Record[] } function buildLogTicks(min: number, max: number): number[] { const ticks: number[] = [] let tick = 10 ** Math.floor(Math.log10(min)) while (tick <= max) { if (tick >= min) ticks.push(tick) tick *= 10 } return ticks.length > 0 ? ticks : [min, max] } export function NotebookPdfChart({ chart, rows }: NotebookPdfChartProps): ReactElement { const { type = 'bar', x_column, y_series = [], cumulative = false, scale = 'linear', show_labels = false, } = chart ?? {} if (!x_column || y_series.length === 0) { return ( Chart is not configured. ) } const baseRows = rows.map((row) => { const next: Record = { [x_column]: row[x_column] } y_series.forEach((column) => { next[column] = Number(row[column]) || 0 }) return next }) const plotRows: Record[] = cumulative ? getCumulativeResults({ rows: baseRows }, { yKey: [...y_series] }) : baseRows // Logarithmic scale only makes sense for a single series, mirroring DisplaySettingsButton's // own rule (QueryResultChart resets `scale` to linear once a second Y column is added). const isLogScale = y_series.length === 1 && scale === 'log' let dataMax = 0 let dataMin = 0 let smallestPositive = Infinity plotRows.forEach((row) => { y_series.forEach((column) => { const value = Number(row[column]) || 0 if (value > dataMax) dataMax = value if (value < dataMin) dataMin = value if (value > 0 && value < smallestPositive) smallestPositive = value }) }) const domainMin = isLogScale ? Math.max(1, smallestPositive === Infinity ? 1 : smallestPositive) : dataMin const domainMax = isLogScale ? Math.max(domainMin * 10, dataMax) : Math.max(dataMax, domainMin + 1) const formatTick = isLogScale ? formatLogTick : formatYAxisTick const yAxisWidth = show_labels ? Math.max(formatTick(domainMax).length, formatTick(domainMin).length) * 6 + 14 : 0 const margin = { top: 8, right: 8, bottom: show_labels ? 24 : 8, left: yAxisWidth } const plotWidth = CHART_WIDTH - margin.left - margin.right const plotHeight = CHART_HEIGHT - margin.top - margin.bottom const mapValueToY = (value: number): number => { if (isLogScale) { const clamped = Math.max(value, domainMin) const logMin = Math.log10(domainMin) const logMax = Math.log10(domainMax) const ratio = logMax === logMin ? 0 : (Math.log10(clamped) - logMin) / (logMax - logMin) return margin.top + plotHeight * (1 - ratio) } const ratio = domainMax === domainMin ? 0 : (value - domainMin) / (domainMax - domainMin) return margin.top + plotHeight * (1 - ratio) } const categoryCount = plotRows.length const slotWidth = categoryCount > 0 ? plotWidth / categoryCount : plotWidth const categoryCenterX = (index: number) => margin.left + index * slotWidth + slotWidth / 2 const yTicks = isLogScale ? buildLogTicks(domainMin, domainMax) : Array.from({ length: 5 }, (_, index) => domainMin + ((domainMax - domainMin) * index) / 4) const xLabelStep = Math.max(1, Math.ceil(categoryCount / MAX_X_LABELS)) const zeroY = mapValueToY(0) return ( {y_series.length > 1 && ( {y_series.map((column, index) => ( {column} ))} )} {show_labels && yTicks.map((tick, index) => ( ))} {type === 'bar' && plotRows.map((row, rowIndex) => { const groupWidth = slotWidth * 0.7 const barWidth = groupWidth / y_series.length const groupStart = margin.left + rowIndex * slotWidth + (slotWidth - groupWidth) / 2 return y_series.map((column, seriesIndex) => { const value = Number(row[column]) || 0 const y = mapValueToY(value) const barTop = Math.min(y, zeroY) const barHeight = Math.max(1, Math.abs(y - zeroY)) return ( ) }) })} {type === 'line' && y_series.map((column, seriesIndex) => { const points = plotRows.map((row, rowIndex) => { const value = Number(row[column]) || 0 return `${rowIndex === 0 ? 'M' : 'L'} ${categoryCenterX(rowIndex)},${mapValueToY(value)}` }) return ( ) })} {show_labels && yTicks.map((tick, index) => ( {formatTick(tick)} ))} {show_labels && plotRows.map((row, rowIndex) => rowIndex % xLabelStep === 0 ? ( {String(row[x_column])} ) : null )} ) }