Skip to content

Commit

Permalink
fix(vis): violin vis selection events (#267)
Browse files Browse the repository at this point in the history
* fix violin plot selection

* partial fix vis stories

* move null value filtering to trace creation function

* disable zoom for violin plot

* small refactor

* pin storybook/node-logger

* remove resolution

* small useEffect dependency array fix

* replace null values in categorical columns with missing

* add scrollarea to multiselect

---------

Co-authored-by: Holger Stitz <[email protected]>
  • Loading branch information
dvmartinweigl and thinkh committed Apr 24, 2024
1 parent 13f5310 commit 7c4adcf
Show file tree
Hide file tree
Showing 11 changed files with 36 additions and 63 deletions.
8 changes: 6 additions & 2 deletions src/vis/sidebar/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CloseButton, Combobox, Input, Pill, PillsInput, Stack, Tooltip, useCombobox, Text, Group } from '@mantine/core';
import { CloseButton, Combobox, Input, Pill, PillsInput, Stack, Tooltip, useCombobox, Text, Group, ScrollArea } from '@mantine/core';
import * as React from 'react';
import { ColumnInfo, EColumnTypes, VisColumn } from '../interfaces';

Expand Down Expand Up @@ -121,7 +121,11 @@ export function MultiSelect({
</Combobox.DropdownTarget>

<Combobox.Dropdown>
<Combobox.Options>{options.length === 0 ? <Combobox.Empty>All options selected</Combobox.Empty> : options}</Combobox.Options>
<Combobox.Options>
<ScrollArea.Autosize type="scroll" mah={200}>
{options.length === 0 ? <Combobox.Empty>All options selected</Combobox.Empty> : options}
</ScrollArea.Autosize>
</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Iris.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
return (
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} columns={columns} selected={selection} selectionCallback={setSelection} />
<Vis {...args} setExternalConfig={() => {}} columns={columns} selected={selection} selectionCallback={setSelection} />
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Vis/Bar/BarRandom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} columns={columns} />
<Vis {...args} setExternalConfig={() => {}} columns={columns} />
</div>
</div>
</VisProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} columns={columns} selected={selection} selectionCallback={setSelection} />
<Vis {...args} setExternalConfig={() => {}} columns={columns} selected={selection} selectionCallback={setSelection} />
</div>
</div>
</VisProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Vis/Heatmap/HeatmapRandom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} selected={selected} selectionCallback={setSelected} columns={columns} />
<Vis {...args} setExternalConfig={() => {}} selected={selected} selectionCallback={setSelected} columns={columns} />
</div>
</div>
</VisProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Vis/Hexbin/HexbinRandom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} columns={columns} />
<Vis {...args} setExternalConfig={() => {}} columns={columns} />
</div>
</div>
</VisProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Vis/Raincloud/RaincloudRandom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} columns={columns} selected={selected} selectionCallback={setSelected} />
<Vis {...args} setExternalConfig={() => {}} columns={columns} selected={selected} selectionCallback={setSelected} />
</div>
</div>
</VisProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Vis/Scatter/ScatterRandom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} selected={selected} selectionCallback={setSelected} columns={columns} />
<Vis {...args} setExternalConfig={() => {}} selected={selected} selectionCallback={setSelected} columns={columns} />
</div>
</div>
</VisProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/vis/stories/Vis/Violin/ViolinIris.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Template: ComponentStory<typeof Vis> = (args) => {
<VisProvider>
<div style={{ height: '100vh', width: '100%', display: 'flex', justifyContent: 'center', alignContent: 'center', flexWrap: 'wrap' }}>
<div style={{ width: '70%', height: '80%' }}>
<Vis {...args} columns={columns} />
<Vis {...args} setExternalConfig={() => {}} columns={columns} />
</div>
</div>
</VisProvider>
Expand Down
56 changes: 10 additions & 46 deletions src/vis/violin/ViolinVis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,16 @@ import { Plotly } from '../../plotly/full';
import { InvalidCols } from '../general';
import { beautifyLayout } from '../general/layoutUtils';
import { ICommonVisProps } from '../interfaces';
import { createViolinTraces } from './utils';
import { IViolinConfig } from './interfaces';
import { createViolinTraces } from './utils';

export function ViolinVis({ config, columns, scales, dimensions, selectedList, selectedMap, selectionCallback }: ICommonVisProps<IViolinConfig>) {
const { value: traces, status: traceStatus, error: traceError } = useAsync(createViolinTraces, [columns, config, scales, selectedList, selectedMap]);
const [clearTimeoutValue, setClearTimeoutValue] = useState(null);

const id = useMemo(() => uniqueId('ViolinVis'), []);

const [layout, setLayout] = useState<Partial<Plotly.Layout>>(null);

// Filter out null values from traces as null values cause the tooltip to not show up
const filteredTraces = useMemo(() => {
if (!traces) return null;
const indexWithNull = traces.plots?.map(
(plot) => (plot?.data.y as PlotlyTypes.Datum[])?.reduce((acc: number[], curr, i) => (curr === null ? [...acc, i] : acc), []) as number[],
);
const filtered = {
...traces,
plots: traces?.plots?.map((p, p_index) => {
return {
...p,
data: {
...p.data,
y: (p.data?.y as PlotlyTypes.Datum[])?.filter((v, i) => !indexWithNull[p_index].includes(i)),
x: (p.data?.x as PlotlyTypes.Datum[])?.filter((v, i) => !indexWithNull[p_index].includes(i)),
ids: p.data?.ids?.filter((v, i) => !indexWithNull[p_index].includes(i)),
transforms: p.data?.transforms?.map(
(t) => (t.groups as unknown[])?.filter((v, i) => !indexWithNull[p_index].includes(i)) as Partial<PlotlyTypes.Transform>,
),
},
};
}),
};
return filtered;
}, [traces]);

const onClick = (e: (Readonly<PlotlyTypes.PlotSelectionEvent> & { event: MouseEvent }) | null) => {
if (!e || !e.points || !e.points[0]) {
selectionCallback([]);
Expand Down Expand Up @@ -87,22 +60,12 @@ export function ViolinVis({ config, columns, scales, dimensions, selectedList, s
if (plotDiv) {
// NOTE: @dv-usama-ansari: This is a hack to update the plotly plots on resize.
// The `setTimeout` is used to pass the resize function to the next event loop, so that the plotly plots are rendered first.
const n = setTimeout(() => Plotly.Plots.resize(plotDiv));
setClearTimeoutValue(n);
setTimeout(() => Plotly.Plots.resize(plotDiv));
}
}, [id, dimensions, traces]);

// NOTE: @dv-usama-ansari: Clear the timeout on unmount.
useEffect(() => {
return () => {
if (clearTimeoutValue) {
clearTimeout(clearTimeoutValue);
}
};
}, [clearTimeoutValue]);

useEffect(() => {
if (!filteredTraces) {
if (!traces) {
return;
}

Expand All @@ -122,13 +85,14 @@ export function ViolinVis({ config, columns, scales, dimensions, selectedList, s
family: 'Roboto, sans-serif',
},
clickmode: 'event+select',
dragmode: false, // Disables zoom (makes no sense in violin plots)
autosize: true,
grid: { rows: filteredTraces.rows, columns: filteredTraces.cols, xgap: 0.3, pattern: 'independent' },
grid: { rows: traces.rows, columns: traces.cols, xgap: 0.3, pattern: 'independent' },
shapes: [],
};

setLayout((prev) => ({ ...prev, ...beautifyLayout(filteredTraces, innerLayout, prev, true) }));
}, [filteredTraces]);
setLayout((prev) => ({ ...prev, ...beautifyLayout(traces, innerLayout, prev, true) }));
}, [traces]);

return (
<Stack
Expand All @@ -144,10 +108,10 @@ export function ViolinVis({ config, columns, scales, dimensions, selectedList, s
},
}}
>
{traceStatus === 'success' && layout && filteredTraces?.plots.length > 0 ? (
{traceStatus === 'success' && layout && traces?.plots.length > 0 ? (
<PlotlyComponent
divId={`plotlyDiv${id}`}
data={[...filteredTraces.plots.map((p) => p.data), ...filteredTraces.legendPlots.map((p) => p.data)]}
data={[...traces.plots.map((p) => p.data), ...traces.legendPlots.map((p) => p.data)]}
layout={layout}
config={{ responsive: true, displayModeBar: false }}
useResizeHandler
Expand All @@ -163,7 +127,7 @@ export function ViolinVis({ config, columns, scales, dimensions, selectedList, s
}}
/>
) : traceStatus !== 'pending' && traceStatus !== 'idle' && layout ? (
<InvalidCols headerMessage={filteredTraces?.errorMessageHeader} bodyMessage={traceError?.message || filteredTraces?.errorMessage} />
<InvalidCols headerMessage={traces?.errorMessageHeader} bodyMessage={traceError?.message || traces?.errorMessage} />
) : null}
</Stack>
);
Expand Down
19 changes: 12 additions & 7 deletions src/vis/violin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ export async function createViolinTraces(
// if we onl have numerical columns, add them individually.
if (catColValues.length === 0) {
for (const numCurr of numColValues) {
const y = numCurr.resolvedValues.map((v) => v.val);
plots.push({
data: {
y: numCurr.resolvedValues.map((v) => v.val),
y,
ids: numCurr.resolvedValues.map((v) => v.id),
xaxis: plotCounter === 1 ? 'x' : `x${plotCounter}`,
yaxis: plotCounter === 1 ? 'y' : `y${plotCounter}`,
Expand Down Expand Up @@ -88,11 +89,15 @@ export async function createViolinTraces(

for (const numCurr of numColValues) {
for (const catCurr of catColValues) {
const y = numCurr.resolvedValues.map((v) => v.val);
// Null values in categorical columns would break the plot --> replace with 'missing'
const categoriesWithMissing = catCurr.resolvedValues?.map((v) => ({ ...v, val: v.val || 'missing' }));
const x = categoriesWithMissing.map((v) => v.val);
plots.push({
data: {
x: catCurr.resolvedValues.map((v) => v.val),
ids: catCurr.resolvedValues.map((v) => v.id),
y: numCurr.resolvedValues.map((v) => v.val),
x,
y,
ids: categoriesWithMissing.map((v) => v.id),
xaxis: plotCounter === 1 ? 'x' : `x${plotCounter}`,
yaxis: plotCounter === 1 ? 'y' : `y${plotCounter}`,
type: 'violin',
Expand All @@ -112,14 +117,14 @@ export async function createViolinTraces(
transforms: [
{
type: 'groupby',
groups: catCurr.resolvedValues.map((v) => v.val) as string[],
styles: [...new Set<string>(catCurr.resolvedValues.map((v) => v.val) as string[])].map((c) => {
groups: x as string[],
styles: [...new Set(x as string[])].map((c) => {
return {
target: c,
value: {
line: {
color:
selectedList.length !== 0 && catCurr.resolvedValues.filter((val) => val.val === c).find((val) => selectedMap[val.id])
selectedList.length !== 0 && categoriesWithMissing.filter((val) => val.val === c).find((val) => selectedMap[val.id])
? SELECT_COLOR
: '#878E95',
},
Expand Down

0 comments on commit 7c4adcf

Please sign in to comment.