diff --git a/src/vis/hexbin/HexbinVis.tsx b/src/vis/hexbin/HexbinVis.tsx index d29edf477..ddbaf2426 100644 --- a/src/vis/hexbin/HexbinVis.tsx +++ b/src/vis/hexbin/HexbinVis.tsx @@ -1,13 +1,62 @@ -import { Box, Center, Group, SimpleGrid, Stack } from '@mantine/core'; -import merge from 'lodash/merge'; +import { Box, Center, Chip, Group, ScrollArea, Stack, Tooltip, rem } from '@mantine/core'; +import * as d3v7 from 'd3v7'; import * as React from 'react'; -import { useMemo } from 'react'; +import { useAsync } from '../../hooks/useAsync'; import { i18n } from '../../i18n'; import { InvalidCols } from '../general'; import { EScatterSelectSettings, ICommonVisProps } from '../interfaces'; import { BrushOptionButtons } from '../sidebar'; import { Hexplot } from './Hexplot'; import { IHexbinConfig } from './interfaces'; +import { getHexData } from './utils'; + +function Legend({ + categories, + filteredCategories, + colorScale, + onClick, + height, +}: { + categories: string[]; + filteredCategories: string[]; + colorScale: d3v7.ScaleOrdinal; + onClick: (string) => void; + height: number; +}) { + return ( + + + {categories.map((c) => { + return ( + + + onClick(c)} + checked={false} + styles={{ + label: { + width: '100%', + backgroundColor: filteredCategories.includes(c) ? 'lightgrey' : `${colorScale(c)} !important`, + textAlign: 'center', + paddingLeft: '10px', + paddingRight: '10px', + overflow: 'hidden', + color: filteredCategories.includes(c) ? 'black' : 'white', + textOverflow: 'ellipsis', + }, + }} + > + {c} + + + + ); + })} + + + ); +} export function HexbinVis({ config, @@ -19,6 +68,32 @@ export function HexbinVis({ showDragModeOptions = true, }: ICommonVisProps) { const { width, height } = dimensions; + const { value: allColumns, status: colsStatus } = useAsync(getHexData, [columns, config.numColumnsSelected, config.color]); + + const [filteredCategories, setFilteredCategories] = React.useState([]); + + const currentColorColumn = React.useMemo(() => { + if (config.color && allColumns?.colorColVals) { + return { + allValues: allColumns.colorColVals.resolvedValues, + filteredValues: allColumns.colorColVals.resolvedValues.filter((val) => !filteredCategories.includes(val.val as string)), + }; + } + + return null; + }, [allColumns?.colorColVals, config.color, filteredCategories]); + + const colorScale = React.useMemo(() => { + if (!currentColorColumn?.allValues) { + return null; + } + + const colorOptions = currentColorColumn.allValues.map((val) => val.val as string); + + return d3v7 + .scaleOrdinal(allColumns.colorColVals.color ? Object.keys(allColumns.colorColVals.color) : d3v7.schemeCategory10) + .domain(allColumns.colorColVals.color ? Object.values(allColumns.colorColVals.color) : Array.from(new Set(colorOptions))); + }, [currentColorColumn, allColumns]); return ( @@ -33,13 +108,33 @@ export function HexbinVis({ ) : null} - 2 ? config.numColumnsSelected.length : 1}> - {config.numColumnsSelected.length < 2 ? ( - - ) : ( - <> - {config.numColumnsSelected.length > 2 ? ( - config.numColumnsSelected.map((xCol) => { + + 2 + ? { gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gridTemplateRows: 'repeat(3, minmax(0, 1fr))', gap: '1rem 1rem' } + : { gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gridTemplateRows: 'repeat(1, minmax(0, 1fr))', gap: '1rem 1rem' }), + }} + > + {config.numColumnsSelected.length < 2 ? ( + + ) : null} + + {config.numColumnsSelected.length === 2 && allColumns?.numColVals.length === config.numColumnsSelected.length && colsStatus === 'success' ? ( + + ) : null} + {config.numColumnsSelected.length > 2 && allColumns?.numColVals.length === config.numColumnsSelected.length && colsStatus === 'success' + ? config.numColumnsSelected.map((xCol) => { return config.numColumnsSelected.map((yCol) => { if (xCol.id !== yCol.id) { return ( @@ -48,11 +143,14 @@ export function HexbinVis({ selectionCallback={selectionCallback} selected={selectedMap} config={config} - columns={[ - columns.find((col) => col.info.id === yCol.id), - columns.find((col) => col.info.id === xCol.id), - columns.find((col) => col.info.id === config.color?.id), - ]} + filteredCategories={filteredCategories} + allColumns={{ + numColVals: [ + allColumns.numColVals.find((col) => col.info.id === yCol.id), + allColumns.numColVals.find((col) => col.info.id === xCol.id), + ], + colorColVals: allColumns.colorColVals, + }} /> ); } @@ -60,21 +158,24 @@ export function HexbinVis({ return
; }); }) - ) : ( - col.info.id === config.numColumnsSelected[0].id), - columns.find((col) => col.info.id === config.numColumnsSelected[1].id), - columns.find((col) => col.info.id === config.color?.id), - ]} - /> - )} - - )} - + : null} + + {currentColorColumn ? ( +
+ + filteredCategories.includes(s) + ? setFilteredCategories(filteredCategories.filter((f) => f !== s)) + : setFilteredCategories([...filteredCategories, s]) + } + height={height - 100} + /> +
+ ) : null} + ); } diff --git a/src/vis/hexbin/Hexplot.tsx b/src/vis/hexbin/Hexplot.tsx index aaaf4cd17..ed3bfe64b 100644 --- a/src/vis/hexbin/Hexplot.tsx +++ b/src/vis/hexbin/Hexplot.tsx @@ -7,70 +7,22 @@ import { D3BrushEvent, D3ZoomEvent } from 'd3v7'; import uniqueId from 'lodash/uniqueId'; import * as React from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; -import { useAsync } from '../../hooks/useAsync'; -import { EScatterSelectSettings, VisColumn } from '../interfaces'; +import { EScatterSelectSettings } from '../interfaces'; import { SingleHex } from './SingleHex'; import { XAxis } from './XAxis'; import { YAxis } from './YAxis'; import { IHexbinConfig } from './interfaces'; -import { getHexData } from './utils'; +import { ResolvedHexValues } from './utils'; interface HexagonalBinProps { config: IHexbinConfig; - columns: VisColumn[]; + allColumns: ResolvedHexValues; selectionCallback?: (ids: string[]) => void; selected?: { [key: string]: boolean }; -} - -function Legend({ - categories, - filteredCategories, - colorScale, - onClick, - height, -}: { - categories: string[]; filteredCategories: string[]; - colorScale: d3v7.ScaleOrdinal; - onClick: (string) => void; - height: number; -}) { - return ( - - - {categories.map((c) => { - return ( - - - onClick(c)} - checked={false} - styles={{ - label: { - width: '100%', - backgroundColor: filteredCategories.includes(c) ? 'lightgrey' : `${colorScale(c)} !important`, - textAlign: 'center', - paddingLeft: '10px', - paddingRight: '10px', - overflow: 'hidden', - color: filteredCategories.includes(c) ? 'black' : 'white', - textOverflow: 'ellipsis', - }, - }} - > - {c} - - - - ); - })} - - - ); } -export function Hexplot({ config, columns, selectionCallback = () => null, selected = {} }: HexagonalBinProps) { +export function Hexplot({ config, allColumns, selectionCallback = () => null, selected = {}, filteredCategories }: HexagonalBinProps) { const { ref: hexRef, width: realWidth, height: realHeight } = useElementSize(); const xZoomedScale = useRef>(null); @@ -79,15 +31,11 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec const [yZoomTransform, setYZoomTransform] = useState(0); const [zoomScale, setZoomScale] = useState(1); - const [filteredCategories, setFilteredCategories] = useState([]); - - const { value: allColumns, status: colsStatus } = useAsync(getHexData, [columns, config.numColumnsSelected, config.color]); - const id = React.useMemo(() => uniqueId('HexPlot'), []); // getting current categorical column values, original and filtered const currentColorColumn = useMemo(() => { - if (colsStatus === 'success' && config.color && allColumns.colorColVals) { + if (config.color && allColumns.colorColVals) { return { allValues: allColumns.colorColVals.resolvedValues, filteredValues: allColumns.colorColVals.resolvedValues.filter((val) => !filteredCategories.includes(val.val as string)), @@ -95,23 +43,23 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec } return null; - }, [allColumns?.colorColVals, config.color, colsStatus, filteredCategories]); + }, [allColumns?.colorColVals, config.color, filteredCategories]); const margin = useMemo(() => { return { - left: 52, - right: config.color ? 90 : 25, - top: 50, - bottom: 53, + left: 48, + right: 16, + top: 48, + bottom: 48, }; - }, [config.color]); + }, []); const height = realHeight - margin.top - margin.bottom; const width = realWidth - margin.left - margin.right; // getting currentX data values, both original and filtered. const currentX = useMemo(() => { - if (colsStatus === 'success' && allColumns) { + if (allColumns) { if (config.color && allColumns.colorColVals) { return { allValues: allColumns.numColVals[0].resolvedValues, @@ -127,11 +75,11 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec } return null; - }, [allColumns, config.color, colsStatus, filteredCategories]); + }, [allColumns, config.color, filteredCategories]); // getting currentY data values, both original and filtered. const currentY = useMemo(() => { - if (colsStatus === 'success' && allColumns) { + if (allColumns) { if (config.color && allColumns.colorColVals) { return { allValues: allColumns.numColVals[1].resolvedValues, @@ -147,24 +95,7 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec } return null; - }, [allColumns, colsStatus, config.color, filteredCategories]); - - // resize observer for setting size of the svg and updating on size change - /** useEffect(() => { - const ro = new ResizeObserver((entries: ResizeObserverEntry[]) => { - setHeight(entries[0].contentRect.height - margin.top - margin.bottom); - setWidth(entries[0].contentRect.width - margin.left - margin.right); - }); - - if (ref) { - ro.observe(ref.current); - } - - return () => { - ro.disconnect(); - }; - }, [margin]); -*/ + }, [allColumns, config.color, filteredCategories]); // create x scale const xScale = useMemo(() => { @@ -235,32 +166,28 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec // simple radius scale for the hexes const radiusScale = useMemo(() => { - if (colsStatus === 'success') { - const [min, max] = d3v7.extent(hexes, (h) => h.length); + const [min, max] = d3v7.extent(hexes, (h) => h.length); - return d3v7 - .scaleLinear() - .domain([min, max]) - .range([config.hexRadius / 2, config.hexRadius]); - } + return d3v7 + .scaleLinear() + .domain([min, max]) + .range([config.hexRadius / 2, config.hexRadius]); return null; - }, [colsStatus, hexes, config.hexRadius]); + }, [hexes, config.hexRadius]); // simple opacity scale for the hexes const opacityScale = useMemo(() => { - if (colsStatus === 'success') { - const [min, max] = d3v7.extent(hexes, (h) => h.length); + const [min, max] = d3v7.extent(hexes, (h) => h.length); - return d3v7.scaleLinear().domain([min, max]).range([0.1, 1]); - } + return d3v7.scaleLinear().domain([min, max]).range([0.1, 1]); return null; - }, [colsStatus, hexes]); + }, [hexes]); // Create a default color scale const colorScale = useMemo(() => { - if (colsStatus !== 'success' || !currentColorColumn?.allValues) { + if (!currentColorColumn?.allValues) { return null; } @@ -269,7 +196,7 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec return d3v7 .scaleOrdinal(allColumns.colorColVals.color ? Object.keys(allColumns.colorColVals.color) : d3v7.schemeCategory10) .domain(allColumns.colorColVals.color ? Object.values(allColumns.colorColVals.color) : Array.from(new Set(colorOptions))); - }, [allColumns, colsStatus, currentColorColumn]); + }, [allColumns, currentColorColumn]); // memoize the actual hexes since they do not need to change on zoom/drag const hexObjects = React.useMemo(() => { @@ -386,7 +313,7 @@ export function Hexplot({ config, columns, selectionCallback = () => null, selec }, [width, height, id, hexes, selectionCallback, config.dragMode, xScale, yScale, margin]); return ( - + null, selec pointerEvents={config.dragMode === EScatterSelectSettings.PAN ? 'auto' : 'none'} /> -
- - filteredCategories.includes(s) - ? setFilteredCategories(filteredCategories.filter((f) => f !== s)) - : setFilteredCategories([...filteredCategories, s]) - } - height={200} - /> -
); diff --git a/src/vis/hexbin/utils.tsx b/src/vis/hexbin/utils.tsx index d996c9a35..32f73f8e1 100644 --- a/src/vis/hexbin/utils.tsx +++ b/src/vis/hexbin/utils.tsx @@ -40,11 +40,7 @@ export function hexinbMergeDefaultConfig(columns: VisColumn[], config: IHexbinCo return merged; } -export async function getHexData( - columns: VisColumn[], - numColumnsSelected: ColumnInfo[], - colorColumn: ColumnInfo | null, -): Promise<{ +export type ResolvedHexValues = { numColVals: { resolvedValues: (VisNumericalValue | VisCategoricalValue)[]; type: EColumnTypes.NUMERICAL | EColumnTypes.CATEGORICAL; @@ -56,8 +52,10 @@ export async function getHexData( color?: Record; info: ColumnInfo; }; -}> { - const numCols: VisNumericalColumn[] = [columns[0] as VisNumericalColumn, columns[1] as VisNumericalColumn]; +}; + +export async function getHexData(columns: VisColumn[], numColumnsSelected: ColumnInfo[], colorColumn: ColumnInfo | null): Promise { + const numCols: VisNumericalColumn[] = columns.filter((col) => numColumnsSelected.find((e) => e.id === col.info.id)) as VisNumericalColumn[]; const numColVals = await resolveColumnValues(numCols);