()
| 113 | } |
| 114 | |
| 115 | export const ChartRenderService: FC = () => { |
| 116 | const dispatch = useDispatch(); |
| 117 | |
| 118 | const charts = useSelector(dfSelectors.getAllCharts); |
| 119 | const tables = useSelector((state: DataFormulatorState) => state.tables); |
| 120 | const conceptShelfItems = useSelector((state: DataFormulatorState) => state.conceptShelfItems); |
| 121 | const chartSynthesisInProgress = useSelector((state: DataFormulatorState) => state.chartSynthesisInProgress); |
| 122 | const maxStretchFactor = useSelector((state: DataFormulatorState) => state.config.maxStretchFactor); |
| 123 | // Re-run when the focused canvas caches a fresh display-row sample so |
| 124 | // thumbnails can use the same richer data the main chart is rendering. |
| 125 | const displayRowsTick = useSelector((state: DataFormulatorState) => state.displayRowsTick); |
| 126 | // Read the thumbnails map via a ref so we can check current values inside |
| 127 | // the effect without adding the map to the dep list (the dispatch we |
| 128 | // issue below mutates it, and including it would re-enter the effect). |
| 129 | const chartThumbnailsRef = useRef<Record<string, string>>({}); |
| 130 | chartThumbnailsRef.current = useSelector((state: DataFormulatorState) => state.chartThumbnails) || {}; |
| 131 | |
| 132 | // Track which charts are currently being rendered to avoid duplicates |
| 133 | const renderingRef = useRef<Set<string>>(new Set()); |
| 134 | |
| 135 | // Track previous chart count for cleanup |
| 136 | const prevChartIdsRef = useRef<Set<string>>(new Set()); |
| 137 | |
| 138 | const processChart = useCallback(async (job: RenderJob) => { |
| 139 | const { chart, table, conceptShelfItems: items, cacheKey } = job; |
| 140 | |
| 141 | // Skip if already rendering this chart |
| 142 | if (renderingRef.current.has(chart.id)) return; |
| 143 | renderingRef.current.add(chart.id); |
| 144 | |
| 145 | try { |
| 146 | // --- Prepare data (mirror MemoizedChartObject's pipeline) --- |
| 147 | // Prefer the same sample VisualizationView fetched for the |
| 148 | // focused canvas — for virtual tables `table.rows` is only a |
| 149 | // small preview slice, so rendering from it produces a |
| 150 | // thumbnail that doesn't match the main chart. The canvas |
| 151 | // populates `displayRowsCache` with up to 1000 server-sampled |
| 152 | // rows; reuse that when present. |
| 153 | const dispKey = computeDisplayRowsCacheKey(table, chart, items); |
| 154 | const cachedDisplay = displayRowsCache.get(dispKey); |
| 155 | let visTableRows: any[] = cachedDisplay |
| 156 | ? structuredClone(cachedDisplay.rows) |
| 157 | : structuredClone(table.rows); |
| 158 | |
| 159 | // Pre-aggregate for the encoding map |
| 160 | visTableRows = prepVisTable(visTableRows, items, chart.encodingMap); |
| 161 | |
| 162 | // --- Resolve the spec to render --- |
| 163 | // If a style variant is active, render its stored Vega-Lite spec so |
| 164 | // the thumbnail matches what the user sees in the focused canvas. |
| 165 | // Otherwise assemble the default spec from the encoding map. |
| 166 | // (See design-docs/28-chart-style-refinement-agent.md.) |
| 167 | const activeVariant = chart.activeVariantId |
| 168 | ? chart.styleVariants?.find(v => v.id === chart.activeVariantId) |
| 169 | : undefined; |
| 170 | |
| 171 | let fullSpec: any; |
| 172 | if (activeVariant) { |
nothing calls this directly
no test coverage detected