Skip to content
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

[Enhancement] Support common brush tool in PCP #95

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
123 changes: 114 additions & 9 deletions geoda-ai/src/components/plots/parallel-coordinate-plot.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {RefObject, useEffect, useMemo, useRef} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import {Card, CardHeader, CardBody} from '@nextui-org/react';
import ReactEChartsCore from 'echarts-for-react/lib/core';
import * as echarts from 'echarts/core';
Expand All @@ -16,6 +17,15 @@ import {GeojsonLayer, Layer} from '@kepler.gl/layers';
// Register the required components
echarts.use([CanvasRenderer, ParallelChart]);

// PCP chart constants (uint: pixels)
const DEFAULT_PCP_HEIGHT = 175;
// const DEFAULT_PCP_WIDTH = 308;
const DEFAULT_PCP_LEFT = 10;
const DEFAULT_PCP_RIGHT = 50;
const DEFAULT_PCP_TOP = 30;
const DEFAULT_PCP_BOTTOM = 20;
const PCP_HEIGHT_PER_VARIABLE = 25;

function getChartOption(
filteredIndex: Uint8ClampedArray | null,
props: ParallelCoordinateProps,
Expand All @@ -25,18 +35,24 @@ function getChartOption(
let dataCols: number[][] = [];
if (rawDataArray) {
const transposedData = rawDataArray[0].map((_, colIndex) =>
// filter row by filteredIndex
rawDataArray.map(row => row[colIndex])
);
dataCols = transposedData;
}

// filter dataCols by filteredIndex
if (filteredIndex) {
dataCols = dataCols.filter((_, index) => filteredIndex[index] === 1);
}

// build option for echarts
const option: EChartsOption = {
parallel: {
left: '5%',
right: '35%',
top: '23%',
bottom: '15%',
left: DEFAULT_PCP_LEFT,
right: DEFAULT_PCP_RIGHT,
top: DEFAULT_PCP_TOP,
bottom: DEFAULT_PCP_BOTTOM,
layout: 'vertical',
parallelAxisDefault: {
axisLabel: {
Expand All @@ -50,6 +66,12 @@ function getChartOption(
}
},
parallelAxis: axis,
brush: {
toolbox: ['rect', 'clear'],
xAxisIndex: 'all',
yAxisIndex: 'all',
seriesIndex: 'all'
},
series: {
type: 'parallel',
lineStyle: {
Expand Down Expand Up @@ -158,7 +180,6 @@ export const ParallelCoordinatePlot = ({props}: {props: ParallelCoordinateProps}
// Ensure that the chart instance is available
if (eChartsRef.current) {
const chartInstance = eChartsRef.current.getEchartsInstance();

// Define the event handler function
const onAxisAreaSelected = () => {
// @ts-ignore todo: will fix later
Expand All @@ -183,18 +204,101 @@ export const ParallelCoordinatePlot = ({props}: {props: ParallelCoordinateProps}
};

// Attach the event listener
chartInstance.on('axisareaselected', onAxisAreaSelected);
// chartInstance.on('axisareaselected', onAxisAreaSelected);
return () => {
chartInstance.off('axisareaselected', onAxisAreaSelected);
// chartInstance.off('axisareaselected', onAxisAreaSelected);
};
} else {
return undefined;
}
}, [dispatch, props, rawDataArray, tableName, validPlot]);

const bindEvents = {
brushSelected: function (params: any) {
console.log('brushSelected', params);
const chart = eChartsRef.current;
const chartInstance = chart?.getEchartsInstance();
console.log('chartInstance width', chartInstance?.getWidth());
console.log('chartInstance height', chartInstance?.getHeight());
const chartWidth = chartInstance?.getWidth() || 0 - DEFAULT_PCP_LEFT - DEFAULT_PCP_RIGHT;
const chartHeight = chartInstance?.getHeight() || 0 - DEFAULT_PCP_TOP - DEFAULT_PCP_BOTTOM;

const brushed = [];
const brushComponent = params.batch[0];
const brushAreas = brushComponent.areas;
// loop through brushAreas to get the range of each selected area
for (let i = 0; i < brushAreas.length; i++) {
const area = brushAreas[i];
const range = area.range;
const dim = rawDataArray.length;
// the range is in the pixel coordinate
const leftRight = range[0];
const topBottom = range[1];
const top = topBottom[0] - DEFAULT_PCP_TOP;
const bottom = topBottom[1] - DEFAULT_PCP_TOP;
// get the increamental heights of each axis using dim
const incrementalHeight = chartHeight / dim;
const heights = Array.from({length: dim}, (_, i) => i * incrementalHeight);
// check which indexes of heights are in the range of top and bottom
let startDataIndex = 0;
let endDataIndex = 0;
for (let j = 0; j < dim; j++) {
if (top >= heights[j]) {
startDataIndex = j;
}
if (bottom >= heights[j]) {
endDataIndex = j + 1 < dim ? j + 1 : j;
}
}
// project the range to the data coordinate
const left = leftRight[0] - DEFAULT_PCP_LEFT;
const right = leftRight[1] - DEFAULT_PCP_LEFT;
// get the percentage of the range in the chart
const leftPercent = left / chartWidth;
const rightPercent = right / chartWidth;

// get the intersection of the indices of the data that are in the range
for (let i = startDataIndex; i <= endDataIndex; i++) {
const data = rawDataArray[i];
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min;
const leftValue = min + range * leftPercent;
const rightValue = min + range * rightPercent;
// get the indices of the data that are in the range
for (let k = 0; k < data.length; k++) {
if (data[k] >= leftValue && data[k] <= rightValue) {
brushed.push(k);
}
}
}
}
// remove duplicates in brushed
const uniqueBrushed = [...new Set(brushed)];
console.log('brushed', uniqueBrushed);

// // check if this plot is in state.plots
// if (validPlot && brushed.length === 0) {
// // reset options
// const chart = eChartsRef.current;
// if (chart) {
// const chartInstance = chart.getEchartsInstance();
// const updatedOption = getChartOption(null, props, rawDataArray);
// chartInstance.setOption(updatedOption);
// }
// }
// dispatch action to highlight the selected ids
dispatch({
type: 'SET_FILTER_INDEXES',
payload: {dataLabel: tableName, filteredIndex: uniqueBrushed}
});
},
brushEnd: function (params: any) {
console.log('brushEnd');
}
};

// dynamically increase height with set max
const DEFAULT_PCP_HEIGHT = 175;
const PCP_HEIGHT_PER_VARIABLE = 25;
const height =
DEFAULT_PCP_HEIGHT + Math.min(props.variables.length - 2, 3) * PCP_HEIGHT_PER_VARIABLE;

Expand All @@ -213,6 +317,7 @@ export const ParallelCoordinatePlot = ({props}: {props: ParallelCoordinateProps}
theme={theme}
style={{height: height + 'px', width: '100%'}}
ref={eChartsRef}
onEvents={bindEvents}
/>
{validPlot && (
<EChartsUpdater
Expand Down
Loading