@@ -5,33 +5,41 @@ import { getErrorMessage } from '@sim/utils/errors'
55import type { EChartsOption } from 'echarts'
66import { useTheme } from 'next-themes'
77import { buildChartRenderOption } from '@/lib/charts/option'
8- import { type ChartSpec , parseChartSpec , shapeTableRows } from '@/lib/charts/spec'
9- import { getColumnId } from '@/lib/table/column-keys'
8+ import {
9+ CHART_ROWS_DEFAULT ,
10+ CHART_ROWS_MAX ,
11+ type ChartSpec ,
12+ mapRowsToColumnNames ,
13+ parseChartSpec ,
14+ shapeTableRows ,
15+ } from '@/lib/charts/spec'
1016import { useTable , useTableRowsSample } from '@/hooks/queries/tables'
1117import { PreviewLoadingFrame } from './preview-shared'
1218
13- /** Hard cap on rows a table-backed chart pulls; the spec's `limit` clamps under it. */
14- const CHART_ROWS_MAX = 5000
15- const CHART_ROWS_DEFAULT = 1000
16-
1719function buildOption ( spec : ChartSpec , rows : Array < Record < string , unknown > > | null ) : EChartsOption {
1820 return buildChartRenderOption ( { title : spec . title , option : spec . option , rows } ) as EChartsOption
1921}
2022
23+ function ChartErrorCard ( { message, content } : { message : string ; content : string } ) {
24+ return (
25+ < div className = 'overflow-hidden rounded-lg border border-[var(--border)]' >
26+ < div className = 'flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-3)] px-3 py-1.5' >
27+ < span className = 'text-[11px] text-[var(--text-tertiary)]' > chart</ span >
28+ < span className = 'text-[11px] text-[var(--text-muted)]' > { message } </ span >
29+ </ div >
30+ < div className = 'code-editor-theme bg-[var(--surface-5)]' >
31+ < pre className = 'm-0 overflow-x-auto whitespace-pre p-4 font-mono text-[13px] text-[var(--text-primary)] leading-[1.6]' >
32+ < code > { content } </ code >
33+ </ pre >
34+ </ div >
35+ </ div >
36+ )
37+ }
38+
2139function ChartErrorPanel ( { message, content } : { message : string ; content : string } ) {
2240 return (
2341 < div className = 'min-h-0 flex-1 overflow-auto p-6' >
24- < div className = 'overflow-hidden rounded-lg border border-[var(--border)]' >
25- < div className = 'flex items-center justify-between border-[var(--border)] border-b bg-[var(--surface-3)] px-3 py-1.5' >
26- < span className = 'text-[11px] text-[var(--text-tertiary)]' > chart</ span >
27- < span className = 'text-[11px] text-[var(--text-muted)]' > { message } </ span >
28- </ div >
29- < div className = 'code-editor-theme bg-[var(--surface-5)]' >
30- < pre className = 'm-0 overflow-x-auto whitespace-pre p-4 font-mono text-[13px] text-[var(--text-primary)] leading-[1.6]' >
31- < code > { content } </ code >
32- </ pre >
33- </ div >
34- </ div >
42+ < ChartErrorCard message = { message } content = { content } />
3543 </ div >
3644 )
3745}
@@ -92,20 +100,7 @@ export const ChartPreview = memo(function ChartPreview({
92100 const fetched = rowsQuery . data ?. rows
93101 const columns = tableQuery . data ?. schema . columns
94102 if ( ! fetched || ! columns ) return null
95- // Row data is stored keyed by column ID (an opaque uuid); chart specs —
96- // like the mothership table tool and the public API — speak column NAMES.
97- // Remap so `encode`/dimension references in the option match what the
98- // author sees in the table UI.
99- const nameByStorageKey = new Map < string , string > ( )
100- for ( const col of columns ) nameByStorageKey . set ( getColumnId ( col ) , col . name )
101- const named = fetched . map ( ( row ) => {
102- const out : Record < string , unknown > = { }
103- for ( const [ key , value ] of Object . entries ( row . data ) ) {
104- out [ nameByStorageKey . get ( key ) ?? key ] = value
105- }
106- return out
107- } )
108- return shapeTableRows ( named , tableSource )
103+ return shapeTableRows ( mapRowsToColumnNames ( fetched , columns ) , tableSource )
109104 } , [ spec , tableSource , rowsQuery . data , tableQuery . data ] )
110105
111106 const option = useMemo ( ( ) => ( spec ? buildOption ( spec , rows ) : null ) , [ spec , rows ] )
@@ -141,7 +136,6 @@ export const ChartPreview = memo(function ChartPreview({
141136 return < ChartErrorPanel message = { parseError } content = { content } />
142137 }
143138 if ( loadError ) return < ChartErrorPanel message = { loadError } content = { content } />
144- if ( renderError ) return < ChartErrorPanel message = { renderError } content = { content } />
145139 if ( tableSource && rowsQuery . isError ) {
146140 return (
147141 < ChartErrorPanel
@@ -164,9 +158,16 @@ export const ChartPreview = memo(function ChartPreview({
164158 // Width-driven aspect box, not full-bleed: a chart stretched to the whole
165159 // panel height is unreadable in a tall resource pane. ECharts follows the
166160 // box through the ResizeObserver above.
161+ //
162+ // A render error (setOption threw) HIDES the chart box rather than
163+ // unmounting it: the render effect only re-runs when the option changes,
164+ // and it needs the container mounted at that moment to re-initialize —
165+ // an unmounted container would leave the fixed chart blank until a
166+ // second edit.
167167 return (
168168 < div className = 'min-h-0 flex-1 overflow-auto p-6' >
169- < div className = 'relative mx-auto w-full max-w-[1024px]' >
169+ { renderError !== null && < ChartErrorCard message = { renderError } content = { content } /> }
170+ < div className = { renderError !== null ? 'hidden' : 'relative mx-auto w-full max-w-[1024px]' } >
170171 { ( ! echartsLib || waitingOnRows ) && (
171172 < PreviewLoadingFrame className = 'absolute inset-0 z-10' />
172173 ) }
0 commit comments