Skip to content

Commit

Permalink
Data selection feedback for scatter plots (#5857)
Browse files Browse the repository at this point in the history
* Data selection feedback for scatter plots
Add `feedbackBrushes`, from PH-TESS, to the scatter plot viewer's `Selections` component. Show a green bar for each success, a red bar for each failure.

Disable the subject viewer so that annotations can't be edited in the feedback popup.

Add a selection feedback story, with mock feedback brushes.

* Add tests
Refactor `ScatterPlotViewer` with `<ParentSize>` so that it can be rendered in JSDOM.

---------

Co-authored-by: Mark Bouslog <mcbouslog@gmail.com>
  • Loading branch information
eatyourgreens and mcbouslog authored Jan 22, 2024
1 parent b6d5c4f commit a73489f
Showing 6 changed files with 152 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@ function Graph2dRangeFeedback() {

return (
<JSONDataViewer
disabled
subject={subject}
feedback={feedback}
feedbackBrushes={[...annotationBrushes, ...ruleBrushes]}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { forwardRef } from 'react';
import PropTypes from 'prop-types'
import { withParentSize } from '@visx/responsive'
import { ParentSize } from '@visx/responsive'
import ZoomingScatterPlot from './components/ZoomingScatterPlot'
import ScatterPlot from './components/ScatterPlot'
import ZoomControlButton from '../ZoomControlButton'
@@ -13,14 +13,20 @@ const ScatterPlotViewer = forwardRef(function ScatterPlotViewer (props, ref) {
const Plot = (zooming) ? ZoomingScatterPlot : ScatterPlot

return (
<>
{zoomControlFn &&
<ZoomControlButton onClick={zoomControlFn} position='absolute' zooming={zooming} />}
<Plot
forwardedRef={ref}
{...props}
/>
</>
<ParentSize>
{(parent) => (
<>
{zoomControlFn &&
<ZoomControlButton onClick={zoomControlFn} position='absolute' zooming={zooming} />}
<Plot
forwardedRef={ref}
parentHeight={parent.height}
parentWidth={parent.width}
{...props}
/>
</>
)}
</ParentSize>
)
})

@@ -29,10 +35,7 @@ ScatterPlotViewer.defaultProps = {
}

ScatterPlotViewer.propTypes = {
parentHeight: PropTypes.number.isRequired,
parentWidth: PropTypes.number.isRequired,
zooming: PropTypes.bool
}

export default withParentSize(ScatterPlotViewer)
export { ScatterPlotViewer }
export default ScatterPlotViewer
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { composeStory } from '@storybook/react'
import { render } from '@testing-library/react'
import Meta, { Default, ErrorBars, KeplerLightCurve } from './ScatterPlotViewer.stories.js'
import { render, waitFor } from '@testing-library/react'
import Meta, { Default, ErrorBars, KeplerLightCurve, SelectionFeedback } from './ScatterPlotViewer.stories.js'

describe('Component > ScatterPlotViewer', function () {
describe('default plot', function () {
@@ -83,4 +83,27 @@ describe('Component > ScatterPlotViewer', function () {
expect(chart.querySelectorAll('g.chartAxes')).to.have.lengthOf(1)
})
})

describe('with data selection feedback', function () {
let chart

beforeEach(async function () {
const MockScatterPlotViewer = composeStory(SelectionFeedback, Meta)
render(<MockScatterPlotViewer initialHeight={500} initialWidth={500} />)
await waitFor(() => expect(document.querySelector('svg.scatterPlot')).to.exist())
chart = document.querySelector('svg.scatterPlot')
})

it('should show successful matches', function () {
expect(chart.querySelectorAll('rect.selection[fill=green]')).to.have.lengthOf(1)
})

it('should show failed matches', function () {
expect(chart.querySelectorAll('rect.selection[fill=red]')).to.have.lengthOf(1)
})

it('should show volunteer selections', function () {
expect(chart.querySelectorAll('rect.selection[fill=transparent]')).to.have.lengthOf(3)
})
})
})
Original file line number Diff line number Diff line change
@@ -323,3 +323,83 @@ SelectedXRanges.store = mockStore({
subject: superWaspSubject,
workflow: dataSelectionWorkflow
})

export function SelectionFeedback() {
const [task] = SelectedXRanges.store.workflowSteps.findTasksByType('dataVisAnnotation')
SelectedXRanges.store.classifications.addAnnotation(task, [
{
tool: 0,
toolType: 'graph2dRangeX',
x: 98,
width: 6,
zoomLevelOnCreation: 0
},
{
tool: 0,
toolType: 'graph2dRangeX',
x: 110,
width: 6,
zoomLevelOnCreation: 0
},
{
tool: 1,
toolType: 'graph2dRangeX',
x: 116,
width: 4,
zoomLevelOnCreation: 0
}
])
const feedbackBrushes = [
{
id: 1,
minX: 95,
maxX: 101
},
{
id: 2,
minX: 107,
maxX: 113
},
{
id: 3,
minX: 114,
maxX: 118
},
{
id: 'simulated_rule',
minX: 90,
maxX: 102,
success: true
},
{
id: 'simulated_rule',
minX: 124,
maxX: 134,
success: false
}
]

return (
<ViewerContext store={SelectedXRanges.store}>
<Box direction='row' height='medium' width='large'>
<JSONDataViewer
disabled
experimentalSelectionTool
feedbackBrushes={feedbackBrushes}
zoomConfiguration={{
direction: 'x',
minZoom: 1,
maxZoom: 10,
zoomInValue: 1.2,
zoomOutValue: 0.8
}}
/>
<ImageToolbar width='4rem' />
</Box>
</ViewerContext>
)
}
SelectedXRanges.store = mockStore({
subject: superWaspSubject,
workflow: dataSelectionWorkflow
})
Original file line number Diff line number Diff line change
@@ -46,8 +46,6 @@ const TRANSFORM_MATRIX = {
translateY: 0
}



export default function ScatterPlot({
axisColor = '',
backgroundColor = '',
@@ -57,6 +55,7 @@ export default function ScatterPlot({
dataPointSize = 25,
disabled = false,
experimentalSelectionTool = false,
feedbackBrushes = [],
highlightedSeries,
initialSelections = [],
interactionMode = 'annotate',
@@ -191,6 +190,7 @@ export default function ScatterPlot({
{children}
{experimentalSelectionTool && <Selections
disabled={disabled || interactionMode !== 'annotate'}
feedbackBrushes={feedbackBrushes}
height={plotHeight}
margin={margin}
transformMatrix={transformMatrix}
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ const DEFAULT_HANDLER = () => true

function Selections({
disabled = false,
feedbackBrushes=[],
height,
margin,
transformMatrix = TRANSFORM_MATRIX,
@@ -203,6 +204,27 @@ function Selections({
eventRoot?.current.focus()
}

function FeedbackBrush({ feedbackBrush }) {
const width = feedbackBrush.maxX - feedbackBrush.minX
const x = feedbackBrush.minX + width / 2
let fill = 'transparent'
if (feedbackBrush.success === true) {
fill = 'green'
}
if (feedbackBrush.success === false) {
fill = 'red'
}
return (
<Selection
disabled
selection={{ x, width }}
fill={fill}
height={height}
xScale={xScale}
/>
)
}

return (
<g
ref={eventRoot}
@@ -251,6 +273,12 @@ function Selections({
stroke={colors['dark-3']}
/>
))}
{feedbackBrushes?.map(feedbackBrush => (
<FeedbackBrush
key={feedbackBrush.minX}
feedbackBrush={feedbackBrush}
/>
))}
</g>
)
}

0 comments on commit a73489f

Please sign in to comment.