Skip to content

Do not mutate plot props when zooming #5066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
30 changes: 7 additions & 23 deletions webview/src/plots/components/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3040,10 +3040,7 @@ describe('App', () => {
const smoothPlot = screen.getByTestId(`plot_${smoothId}`)
await waitForVega(smoothPlot)

// eslint-disable-next-line testing-library/no-node-access
const slider = smoothPlot.querySelector(
'.vega-bindings input[name="smooth"]'
)
const slider = within(smoothPlot).getByRole('slider')
expect(slider).toBeInTheDocument()

fireEvent.change(slider as HTMLInputElement, { target: { value: 0.4 } })
Expand All @@ -3063,10 +3060,7 @@ describe('App', () => {
const popup = screen.getByTestId('zoomed-in-plot')
await waitForVega(popup)

// eslint-disable-next-line testing-library/no-node-access
const slider = popup.querySelector(
'.vega-bindings input[name="smooth"]'
)
const slider = within(popup).getByRole('slider')
expect(slider).toBeInTheDocument()

fireEvent.change(slider as HTMLInputElement, { target: { value: 0.4 } })
Expand All @@ -3082,10 +3076,7 @@ describe('App', () => {
const smoothPlot = screen.getByTestId(`plot_${smoothId}`)
await waitForVega(smoothPlot)

// eslint-disable-next-line testing-library/no-node-access
const slider = smoothPlot.querySelector(
'.vega-bindings input[name="smooth"]'
)
const slider = within(smoothPlot).getByRole('slider')
expect(slider).toBeInTheDocument()

expect(slider).toHaveValue('0.6')
Expand All @@ -3105,10 +3096,8 @@ describe('App', () => {
const popup = screen.getByTestId('zoomed-in-plot')
await waitForVega(popup)

// eslint-disable-next-line testing-library/no-node-access
const slider = popup.querySelector(
'.vega-bindings input[name="smooth"]'
)
const slider = within(popup).getByRole('slider')

expect(slider).toBeInTheDocument()

expect(slider).toHaveValue('0.6')
Expand All @@ -3123,10 +3112,7 @@ describe('App', () => {

await waitForVega(smoothPlot)

// eslint-disable-next-line testing-library/no-node-access
const slider = smoothPlot.querySelector(
'.vega-bindings input[name="smooth"]'
)
const slider = within(smoothPlot).getByRole('slider')

expect(slider).toBeInTheDocument()
expect(slider).toHaveValue('0.2')
Expand All @@ -3138,9 +3124,7 @@ describe('App', () => {
}
})

await waitFor(() => expect(slider).toHaveValue('0.7'), {
timeout: 5000
})
expect(slider).toHaveValue('0.7')
})
})

Expand Down
9 changes: 2 additions & 7 deletions webview/src/plots/components/Plots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,13 @@ const PlotsContent = () => {
return <EmptyState>Loading Plots...</EmptyState>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure what is going on, but when I tested the smooth plots, the plots started behaving really weird:

Screen.Recording.2023-12-05.at.9.46.02.AM.mov

I doubled checked to make sure it wasn't happening on main and main is working correctly.

}

const modal = zoomedInPlot?.plot && (
const modal = zoomedInPlot && (
<Modal
onClose={() => {
dispatch(setZoomedInPlot(undefined))
}}
>
<ZoomedInPlot
isTemplatePlot={zoomedInPlot.isTemplatePlot}
id={zoomedInPlot.id}
props={zoomedInPlot.plot}
openActionsMenu={zoomedInPlot.openActionsMenu}
/>
<ZoomedInPlot {...zoomedInPlot} />
</Modal>
)

Expand Down
51 changes: 14 additions & 37 deletions webview/src/plots/components/ZoomablePlot.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { PlotsSection } from 'dvc/src/plots/webview/contract'
import { SpecWithTitles } from 'dvc/src/plots/vega/util'
import React, { useEffect, useRef } from 'react'
import React from 'react'
import { useDispatch } from 'react-redux'
import VegaLite, { VegaLiteProps } from 'react-vega/lib/VegaLite'
import VegaLite from 'react-vega/lib/VegaLite'
import { ZoomablePlotWrapper } from './ZoomablePlotWrapper'
import { TemplateVegaLite } from './templatePlots/TemplateVegaLite'
import { setZoomedInPlot } from './webviewSlice'
import styles from './styles.module.scss'
import { config } from './constants'
import { createPlotProps } from './constants'
import { changeDragAndDropMode } from './util'
import { zoomPlot } from '../util/messages'
import { useGetPlot } from '../hooks/useGetPlot'
Expand All @@ -29,35 +29,13 @@ export const ZoomablePlot: React.FC<ZoomablePlotProps> = ({
}) => {
const { data, spec, isTemplatePlot } = useGetPlot(section, id)
const dispatch = useDispatch()
const currentPlotProps = useRef<VegaLiteProps>()

const plotProps: VegaLiteProps = {
actions: false,
config,
data,
'data-testid': `${id}-vega`,
renderer: 'svg',
spec
} as VegaLiteProps
currentPlotProps.current = plotProps

useEffect(() => {
dispatch(
setZoomedInPlot({
id,
isTemplatePlot,
plot: currentPlotProps.current,
refresh: true
})
)
}, [data, spec, dispatch, id, isTemplatePlot])
const plotProps = createPlotProps(data, id, spec)

const handleOnClick = (openActionsMenu?: boolean) => {
zoomPlot()

return dispatch(
setZoomedInPlot({ id, isTemplatePlot, openActionsMenu, plot: plotProps })
)
return dispatch(setZoomedInPlot({ id, isTemplatePlot, openActionsMenu }))
}

if (!data && !spec) {
Expand Down Expand Up @@ -103,16 +81,15 @@ export const ZoomablePlot: React.FC<ZoomablePlotProps> = ({
>
<Ellipsis />
</span>
{currentPlotProps.current &&
(isTemplatePlot ? (
<TemplateVegaLite
vegaLiteProps={plotProps}
id={id}
onNewView={onNewView}
/>
) : (
<VegaLite {...plotProps} onNewView={onNewView} />
))}
{isTemplatePlot ? (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

<TemplateVegaLite
vegaLiteProps={plotProps}
id={id}
onNewView={onNewView}
/>
) : (
<VegaLite {...plotProps} onNewView={onNewView} />
)}
</button>
</ZoomablePlotWrapper>
)
Expand Down
27 changes: 18 additions & 9 deletions webview/src/plots/components/ZoomedInPlot.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PlotsSection } from 'dvc/src/plots/webview/contract'
import React, { useEffect, useRef } from 'react'
import VegaLite, { VegaLiteProps } from 'react-vega/lib/VegaLite'
import VegaLite from 'react-vega/lib/VegaLite'
import { Config } from 'vega-lite'
import merge from 'lodash.merge'
import cloneDeep from 'lodash.clonedeep'
Expand All @@ -9,6 +10,7 @@ import {
reverseOfLegendSuppressionUpdate
} from 'dvc/src/plots/vega/util'
import { View } from 'react-vega'
import { createPlotProps } from './constants'
import { TemplateVegaLite } from './templatePlots/TemplateVegaLite'
import styles from './styles.module.scss'
import { ZoomablePlotWrapper } from './ZoomablePlotWrapper'
Expand All @@ -24,10 +26,10 @@ import {
exportPlotDataAsJson,
exportPlotDataAsTsv
} from '../util/messages'
import { useGetPlot } from '../hooks/useGetPlot'

type ZoomedInPlotProps = {
id: string
props: VegaLiteProps
isTemplatePlot: boolean
openActionsMenu?: boolean
}
Expand All @@ -49,16 +51,13 @@ const appendActionToVega = (

export const ZoomedInPlot: React.FC<ZoomedInPlotProps> = ({
id,
props,
isTemplatePlot,
openActionsMenu
}: ZoomedInPlotProps) => {
const isCustomPlot = !isTemplatePlot
const hasSmoothing =
isTemplatePlot &&
(props.spec as { params?: { name: string }[] }).params?.[0]?.name ===
'smooth'

const { data, spec } = useGetPlot(
isTemplatePlot ? PlotsSection.TEMPLATE_PLOTS : PlotsSection.CUSTOM_PLOTS,
id
)
const zoomedInPlotRef = useRef<HTMLDivElement>(null)

useEffect(() => {
Expand All @@ -70,6 +69,16 @@ export const ZoomedInPlot: React.FC<ZoomedInPlotProps> = ({
}
}, [])

if (!spec) {
return
}

const props = createPlotProps(data, id, spec)
const isCustomPlot = !isTemplatePlot
const hasSmoothing =
isTemplatePlot &&
(spec as { params?: { name: string }[] }).params?.[0]?.name === 'smooth'

const onNewView = (view: View) => {
const actions: HTMLDivElement | null | undefined =
zoomedInPlotRef.current?.querySelector('.vega-actions')
Expand Down
18 changes: 17 additions & 1 deletion webview/src/plots/components/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Config } from 'vega-lite'
import { PlainObject, VisualizationSpec } from 'react-vega'
import { VegaLiteProps } from 'react-vega/lib/VegaLite'
import { ThemeProperty } from '../../util/styles'

export const PlOT_FOREGROUND_COLOR = `var(${ThemeProperty.FOREGROUND_COLOR})`
Expand All @@ -13,7 +15,7 @@ const title = {
fontWeight: PLOT_FONT_WEIGHT
}

export const config: Config = {
const config: Config = {
axis: {
domain: false,
gridColor: PlOT_FOREGROUND_COLOR,
Expand Down Expand Up @@ -42,3 +44,17 @@ export const config: Config = {
subtitleColor: PlOT_FOREGROUND_COLOR
}
}

export const createPlotProps = (
data: PlainObject | undefined,
id: string,
spec: VisualizationSpec | undefined
) =>
({
actions: false,
config,
data,
'data-testid': `${id}-vega`,
renderer: 'svg',
spec
}) as VegaLiteProps
66 changes: 20 additions & 46 deletions webview/src/plots/components/templatePlots/TemplateVegaLite.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the full Vega API for signals and views (https://vega.github.io/vega/docs/api/view/#signals) to make things simpler

Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ import { View } from 'react-vega'
import { PlotsState } from '../../store'
import { setSmoothPlotValues } from '../../util/messages'

interface VegaState {
signals?: { [name: string]: number | undefined }
data?: unknown
}

export const TemplateVegaLite = ({
id,
onNewView,
Expand All @@ -20,68 +15,47 @@ export const TemplateVegaLite = ({
vegaLiteProps: VegaLiteProps
}) => {
const vegaView = useRef<View>()
const plotWrapperEl = useRef<HTMLSpanElement>(null)
const smoothPlotValues = useSelector(
(state: PlotsState) => state.template.smoothPlotValues
)
const changeDebounceTimer = useRef(0)
const currentValue = smoothPlotValues[id]

useEffect(() => {
const newValue = smoothPlotValues[id]
if (!newValue || !vegaView.current) {
return
return () => {
vegaView.current?.finalize()
}
}, [])

const currentState: VegaState = vegaView.current.getState()
const currentValue: number | undefined = currentState?.signals?.smooth
if (newValue !== currentValue) {
vegaView.current.setState({
...currentState,
signals: { ...currentState.signals, smooth: newValue }
})
useEffect(() => {
if (!currentValue || !vegaView.current) {
return
}
}, [smoothPlotValues, id])

const addRangeEventListener = () => {
const smoothRange = plotWrapperEl.current?.querySelector(
'input[name="smooth"]'
)

smoothRange?.addEventListener('change', (event: Event) => {
if (event.target) {
window.clearTimeout(changeDebounceTimer.current)
changeDebounceTimer.current = window.setTimeout(() => {
setSmoothPlotValues(
id,
Number((event.target as HTMLInputElement).value)
)
}, 500)
}
})
}
if (vegaView.current.signal('smooth') !== currentValue) {
vegaView.current.signal('smooth', currentValue)
vegaView.current.run()
}
}, [currentValue])

return (
<span ref={plotWrapperEl}>
<span>
<VegaLite
{...vegaLiteProps}
onNewView={view => {
onNewView(view)
vegaView.current = view
const defaultValue = smoothPlotValues[id]
const state = view.getState() as VegaState

if (!state?.signals?.smooth) {
if (!view.signal('smooth')) {
return
}

if (defaultValue) {
view.setState({
...state,
signals: { ...state.signals, smooth: defaultValue }
})
if (currentValue) {
view.signal('smooth', currentValue)
view.run()
}

addRangeEventListener()
view.addSignalListener('smooth', (_, value) => {
setSmoothPlotValues(id, Number(value))
})
}}
/>
</span>
Expand Down
Loading