Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { VisSingleContainer, VisHeatmap, VisHeatmapSelectors } from '@unovis/react'
import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'

export const title = 'Heatmap Accessor Update'
export const subTitle = 'A component-level accessor change must repaint on its own'

type Datum = { column: number; value: number }

const numRows = 7
const numColumns = 10

// Module scope: `data` and every accessor except `color` keep a stable reference across
// re-renders, so a repaint can only be attributed to the new `color` accessor identity.
const data: Datum[] = Array.from({ length: numRows * numColumns }, (_, i) => ({
column: Math.floor(i / numRows),
value: (i % numRows) + 1,
}))

const value = (d: Datum): number => d.value

const selections = [
{ label: 'A', color: 'rgb(233, 71, 60)' },
{ label: 'B', color: 'rgb(58, 123, 232)' },
{ label: 'C', color: 'rgb(238, 174, 39)' },
]

const dimmed = 'rgb(217, 220, 225)'

// eslint-disable-next-line @typescript-eslint/naming-convention
const SelectionContext = createContext(0)

// The selection reaches the component through context, so `VisSingleContainer`'s own props
// (`height` and a `<HeatmapLayer/>` element with unchanged props) stay equal across the
// update. Only `VisHeatmap`'s `color` accessor changes identity.
// eslint-disable-next-line @typescript-eslint/naming-convention
const HeatmapLayer = ({ duration }: { duration?: number }): React.ReactNode => {
const selection = useContext(SelectionContext)
const color = useCallback(
(d: Datum) => (d.column === selection ? selections[selection].color : dimmed),
[selection]
)

return (
<VisHeatmap<Datum>
data={data}
value={value}
color={color}
numRows={numRows}
cellPadding={3}
cellCornerRadius={3}
duration={duration}
/>
)
}

export const component = (props: ExampleViewerDurationProps): React.ReactNode => {
const [selection, setSelection] = useState(0)
const [status, setStatus] = useState('measuring…')

// Read the fills actually committed to the DOM and compare them to the expected ones. The container
// defers its update by one frame and `render()` defers the paint by another, so poll rather than
// sample once — the wall-clock length of a frame isn't something we can assume.
useEffect(() => {
const expected = selections[selection].color
const deadline = 2000
const step = 100
let waited = 0

setStatus('measuring…')
const id = setInterval(() => {
waited += step
const cells = Array.from(document.querySelectorAll<SVGPathElement>(`.${VisHeatmapSelectors.cell}`))
const highlighted = cells.filter(c => c.style.fill !== dimmed)
const fills = Array.from(new Set(highlighted.map(c => c.style.fill)))
const ok = fills.length === 1 && fills[0] === expected && highlighted.length === numRows

if (ok) {
clearInterval(id)
setStatus(`PASS — ${highlighted.length} cells filled with ${expected} after ${waited}ms`)
} else if (waited >= deadline) {
clearInterval(id)
setStatus(`FAIL — expected ${numRows}× ${expected}, got ${highlighted.length}× [${fills.join(', ') || 'none'}]`)
}
}, step)

return () => clearInterval(id)
}, [selection])

return (
<>
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 12 }}>
{selections.map((s, i) => (
<button
key={s.label}
onClick={() => setSelection(i)}
style={{ fontWeight: i === selection ? 700 : 400, borderLeft: `6px solid ${s.color}` }}
>
Selection {s.label}
</button>
))}
<span style={{ fontFamily: 'monospace' }}>{status}</span>
</div>
<SelectionContext.Provider value={selection}>
<VisSingleContainer height={300}>
<HeatmapLayer duration={props.duration} />
</VisSingleContainer>
</SelectionContext.Provider>
</>
)
}
3 changes: 2 additions & 1 deletion packages/dev/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ module.exports = {

// Unovis React
'@unovis/react': path.resolve(__dirname, '../react/src/'),
'src/utils/react': path.resolve(__dirname, '../react/src/utils/react'),
// The react wrappers import their helpers as `src/utils/...` (tsconfig `baseUrl`)
'src/utils': path.resolve(__dirname, '../react/src/utils'),

// Unovis Shared
'@unovis/shared': path.resolve(__dirname, '../shared/'),
Expand Down
19 changes: 17 additions & 2 deletions packages/react/autogen/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,27 @@ export function getComponentCode (
? '{ ...props, renderIntoProvidedDomNode: true }'
: 'props'

// Container-hosted components can't render themselves consistently — the container computes their
// size, margins, scales and shared domains — so a config change asks the container to re-render.
// Stand-alone components own their DOM node and render from `setConfig` themselves.
const containerRenderImport = isStandAlone
? ''
: "\nimport { useContainerRenderOnUpdate } from 'src/utils/container'"
const containerRenderHook = isStandAlone
? ''
: `
// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See \`useContainerRenderOnUpdate\` for how updates are detected.
useContainerRenderOnUpdate()
`

return `// !!! This code was automatically generated. You should not change it !!!
import React, { ForwardedRef, ReactElement, Ref, useImperativeHandle, useEffect, useRef, useState } from 'react'
${importStatements.map(s => `import { ${s.elements.join(', ')} } from '${s.source}'`).join('\n')}

// Utils
import { arePropsEqual } from 'src/utils/react'
import { arePropsEqual } from 'src/utils/react'${containerRenderImport}

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -65,7 +80,7 @@ function Vis${componentName}FC${genericsDefStr} (props: Vis${componentName}Props
${dataType ? 'if (props.data) component?.setData(props.data)' : ''}
component?.setConfig(props)
})

${containerRenderHook}
useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <${isStandAlone ? 'div className={props.className}' : `vis-${elementSuffix}`} ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/annotations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AnnotationsConfigInterface } from '@unovis/ts/components/annotations/co

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -45,6 +46,11 @@ function VisAnnotationsFC (props: VisAnnotationsProps, fRef: ForwardedRef<VisAnn
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-annotations ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AreaConfigInterface } from '@unovis/ts/components/area/config'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisAreaFC<Datum> (props: VisAreaProps<Datum>, fRef: ForwardedRef<VisAre
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/axis/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AxisConfigInterface } from '@unovis/ts/components/axis/config'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisAxisFC<Datum> (props: VisAxisProps<Datum>, fRef: ForwardedRef<VisAxi
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-axis ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/boxplot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BoxplotConfigInterface } from '@unovis/ts/components/boxplot/config'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisBoxplotFC<Datum> (props: VisBoxplotProps<Datum>, fRef: ForwardedRef<
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/brush/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BrushConfigInterface } from '@unovis/ts/components/brush/config'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisBrushFC<Datum> (props: VisBrushProps<Datum>, fRef: ForwardedRef<VisB
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/chord-diagram/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ChordInputNode, ChordInputLink } from '@unovis/ts/components/chord-diag

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -47,6 +48,11 @@ function VisChordDiagramFC<N extends ChordInputNode, L extends ChordInputLink> (
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/crosshair/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CrosshairConfigInterface } from '@unovis/ts/components/crosshair/config

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisCrosshairFC<Datum> (props: VisCrosshairProps<Datum>, fRef: Forwarded
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-crosshair ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/donut/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DonutConfigInterface } from '@unovis/ts/components/donut/config'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisDonutFC<Datum> (props: VisDonutProps<Datum>, fRef: ForwardedRef<VisD
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/free-brush/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FreeBrushConfigInterface } from '@unovis/ts/components/free-brush/confi

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisFreeBrushFC<Datum> (props: VisFreeBrushProps<Datum>, fRef: Forwarded
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/graph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { GraphInputNode, GraphInputLink } from '@unovis/ts/types/graph'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -47,6 +48,11 @@ function VisGraphFC<N extends GraphInputNode, L extends GraphInputLink> (props:
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/grouped-bar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GroupedBarConfigInterface } from '@unovis/ts/components/grouped-bar/con

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisGroupedBarFC<Datum> (props: VisGroupedBarProps<Datum>, fRef: Forward
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/components/heatmap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { HeatmapConfigInterface } from '@unovis/ts/components/heatmap/config'

// Utils
import { arePropsEqual } from 'src/utils/react'
import { useContainerRenderOnUpdate } from 'src/utils/container'

// Types
import { VisComponentElement } from 'src/types/dom'
Expand Down Expand Up @@ -46,6 +47,11 @@ function VisHeatmapFC<Datum> (props: VisHeatmapProps<Datum>, fRef: ForwardedRef<
component?.setConfig(props)
})

// A config change has to drive the render itself: the container re-renders only when its own props
// change, which doesn't happen when new config reaches this component through React context or a
// parent's state. See `useContainerRenderOnUpdate` for how updates are detected.
useContainerRenderOnUpdate()

useImperativeHandle(fRef, () => ({ get component () { return componentRef.current } }), [])
return <vis-component ref={ref} />
}
Expand Down
Loading
Loading