Skip to content

Commit

Permalink
wip: explain data (#140)
Browse files Browse the repository at this point in the history
* feat: init explain data algo and panel

* feat: use computation engine for explain data
  • Loading branch information
bruceyyu authored Aug 31, 2023
1 parent ce6b4a0 commit d47707f
Show file tree
Hide file tree
Showing 6 changed files with 352 additions and 57 deletions.
24 changes: 15 additions & 9 deletions packages/graphic-walker/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ import DatasetConfig from './dataSource/datasetConfig';
import { useCurrentMediaTheme } from './utils/media';
import CodeExport from './components/codeExport';
import VisualConfig from './components/visualConfig';
import ExplainData from './components/explainData';
import GeoConfigPanel from './components/leafletRenderer/geoConfigPanel';
import type { ToolbarItemProps } from './components/toolbar';
import ClickMenu from './components/clickMenu';
import {
LightBulbIcon,
} from "@heroicons/react/24/outline";
import AskViz from './components/askViz';
import { getComputation } from './computation/clientComputation';

Expand Down Expand Up @@ -78,7 +83,7 @@ const App = observer<IGWProps>(function App(props) {
} = props;
const { commonStore, vizStore } = useGlobalStore();

const { datasets, segmentKey } = commonStore;
const { datasets, segmentKey, vizEmbededMenu } = commonStore;

const { t, i18n } = useTranslation();
const curLang = i18n.language;
Expand Down Expand Up @@ -186,6 +191,7 @@ const App = observer<IGWProps>(function App(props) {
)}
<VisualSettings rendererHandler={rendererRef} darkModePreference={dark} exclude={toolbar?.exclude} extra={toolbar?.extra} />
<CodeExport />
<ExplainData themeKey={themeKey} dark={darkMode}/>
<VisualConfig />
{commonStore.showGeoJSONConfigPanel && <GeoConfigPanel />}
<div className="md:grid md:grid-cols-12 xl:grid-cols-6">
Expand All @@ -203,17 +209,17 @@ const App = observer<IGWProps>(function App(props) {
<div
className="m-0.5 p-1 border border-gray-200 dark:border-gray-700"
style={{ minHeight: '600px', overflow: 'auto' }}
// onMouseLeave={() => {
// vizEmbededMenu.show && commonStore.closeEmbededMenu();
// }}
// onClick={() => {
// vizEmbededMenu.show && commonStore.closeEmbededMenu();
// }}
onMouseLeave={() => {
vizEmbededMenu.show && commonStore.closeEmbededMenu();
}}
onClick={() => {
vizEmbededMenu.show && commonStore.closeEmbededMenu();
}}
>
{datasets.length > 0 && (
<ReactiveRenderer ref={rendererRef} themeKey={themeKey} themeConfig={themeConfig} dark={dark} computationFunction={vizStore.computationFunction} />
)}
{/* {vizEmbededMenu.show && (
{vizEmbededMenu.show && (
<ClickMenu x={vizEmbededMenu.position[0]} y={vizEmbededMenu.position[1]}>
<div
className="flex items-center whitespace-nowrap py-1 px-4 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
Expand All @@ -228,7 +234,7 @@ const App = observer<IGWProps>(function App(props) {
<LightBulbIcon className="ml-1 w-3 flex-grow-0 flex-shrink-0" />
</div>
</ClickMenu>
)} */}
)}
</div>
</div>
</div>
Expand Down
199 changes: 199 additions & 0 deletions packages/graphic-walker/src/components/explainData/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { useEffect, useState, useRef, useMemo } from "react";
import Modal from "../modal";
import { observer } from "mobx-react-lite";
import { useGlobalStore } from "../../store";
import { useTranslation } from "react-i18next";
import { getMeaAggKey } from '../../utils';
import styled from 'styled-components';
import embed from "vega-embed";
import { VegaGlobalConfig, IDarkMode, IThemeKey, IField, IRow, IPredicate } from "../../interfaces";
import { builtInThemes } from '../../vis/theme';
import { explainBySelection } from "../../lib/insights/explainBySelection"

const Container = styled.div`
height: 50vh;
overflow-y: hidden;
`;
const TabsList = styled.div`
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
height: 100%;
overflow-y: scroll
`;

const Tab = styled.div`
margin-block: 0.2em;
margin-inline: 0.2em;
padding: 0.5em;
border: 2px solid gray;
cursor: pointer;
`;

const getCategoryName = (row: IRow, field: IField) => {
if (field.semanticType === "quantitative") {
let id = field.fid;
return `${row[id][0].toFixed(2)}-${row[id][1].toFixed(2)}`;
} else {
return row[field.fid];
}
}

const ExplainData: React.FC<{
dark: IDarkMode,
themeKey: IThemeKey
}> = observer(({dark, themeKey}) => {
const { vizStore, commonStore } = useGlobalStore();
const { allFields, viewMeasures, viewDimensions, viewFilters, computationFunction } = vizStore;
const { showInsightBoard, selectedMarkObject } = commonStore;
const [ explainDataInfoList, setExplainDataInfoList ] = useState<{
score: number;
measureField: IField;
targetField: IField;
normalizedData: IRow[];
normalizedParentData: IRow[]
}[]>([]);
const [ selectedInfoIndex, setSelectedInfoIndex ] = useState(0);
const chartRef = useRef<HTMLDivElement>(null);

const vegaConfig = useMemo<VegaGlobalConfig>(() => {
const config: VegaGlobalConfig = {
...builtInThemes[themeKey ?? 'vega']?.[dark],
}
return config;
}, [themeKey, dark])

const { t } = useTranslation();

const explain = async (predicates) => {
const explainInfoList = await explainBySelection({predicates, viewFilters, allFields, viewMeasures, viewDimensions, computationFunction});
setExplainDataInfoList(explainInfoList);
}

useEffect(() => {
if (!showInsightBoard || Object.keys(selectedMarkObject).length === 0) return;
const predicates: IPredicate[] = viewDimensions.map((field) => {
return {
key: field.fid,
type: "discrete",
range: new Set([selectedMarkObject[field.fid]])
} as IPredicate
});
explain(predicates)
}, [viewMeasures, viewDimensions, showInsightBoard, selectedMarkObject]);

useEffect(() => {
if (chartRef.current && explainDataInfoList.length > 0) {
const { normalizedData, normalizedParentData, targetField, measureField } = explainDataInfoList[selectedInfoIndex];
const { semanticType: targetType, name: targetName, fid: targetId } = targetField;
const data = [
...normalizedData.map((row) => ({
category: getCategoryName(row, targetField),
...row,
type: "child",
})),
...normalizedParentData.map((row) => ({
category: getCategoryName(row, targetField),
...row,
type: "parent",
})),
];
const xField = {
x: {
field: "category",
type: targetType === "quantitative" ? "ordinal" : targetType,
axis: {
title: `Distribution of Values for ${targetName}`,
},
},
};
const spec:any = {
data: {
values: data,
},
width: 320,
height: 200,
encoding: {
...xField,
color: {
legend: {
orient: "bottom",
},
},
},
layer: [
{
mark: {
type: "bar",
width: 15,
opacity: 0.7,
},
encoding: {
y: {
field: getMeaAggKey(measureField.fid, measureField.aggName),
type: "quantitative",
title: `${measureField.aggName} ${measureField.name} for All Marks`,
},
color: { datum: "All Marks" },
},
transform: [{ filter: "datum.type === 'parent'" }],
},
{
mark: {
type: "bar",
width: 10,
opacity: 0.7,
},
encoding: {
y: {
field: getMeaAggKey(measureField.fid, measureField.aggName),
type: "quantitative",
title: `${measureField.aggName} ${measureField.name} for Selected Mark`,
},
color: { datum: "Selected Mark" },
},
transform: [{ filter: "datum.type === 'child'" }],
},
],
resolve: { scale: { y: "independent" } },
};

embed(chartRef.current, spec, { mode: 'vega-lite', actions: false, config: vegaConfig });
}
}, [explainDataInfoList, chartRef.current, selectedInfoIndex, vegaConfig]);

return (
<Modal
show={showInsightBoard}
onClose={() => {
commonStore.setShowInsightBoard(false);
setSelectedInfoIndex(0);
}}
>
<Container className="grid grid-cols-4">
<TabsList className="col-span-1">
{
explainDataInfoList.map((option, i) => {
return (
<Tab
key={i}
className={`${selectedInfoIndex === i ? 'border-indigo-400' : ''
} text-xs`}
onClick={() => setSelectedInfoIndex(i)}
>
{option.targetField.name} {option.score.toFixed(2)}
</Tab>
)
})
}
</TabsList>
<div className="col-span-3 text-center overflow-y-scroll">
<div ref={chartRef}></div>
</div>
</Container>
</Modal>
);
});

export default ExplainData;
123 changes: 75 additions & 48 deletions packages/graphic-walker/src/lib/insights/explainBySelection.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,83 @@
import { IAggregator, IExplainProps, IField } from '../../interfaces';
import { IAggregator, IExplainProps, IPredicate, IField, IRow, IViewField, IFilterField, IComputationFunction, IViewWorkflowStep } from '../../interfaces';
import { filterByPredicates, getMeaAggKey } from '../../utils';
import { compareDistribution, normalizeWithParent } from '../../utils/normalization';
import { compareDistribution, compareDistributionKL, compareDistributionJS, normalizeWithParent } from '../../utils/normalization';
import { IBinQuery } from '../interfaces';
import { aggregate } from '../op/aggregate';
import { bin } from '../op/bin';
import { VizSpecStore } from "../../store/visualSpecStore";
import { complementaryFields, groupByAnalyticTypes } from './utils';
import { toWorkflow } from '../../utils/workflow';
import { dataQueryServer } from '../../computation/serverComputation';

export function explainBySelection(props: IExplainProps) {
const { metas, dataSource, viewFields, predicates } = props;
const { dimensions: dimsInView, measures: measInView } = groupByAnalyticTypes(viewFields);
const QUANT_BIN_NUM = 10;

export async function explainBySelection(props: {
predicates: IPredicate[],
viewFilters: VizSpecStore['viewFilters'],
allFields: IViewField[],
viewMeasures: IViewField[],
viewDimensions: IViewField[],
computationFunction: IComputationFunction
}) {
const { allFields, viewFilters, viewMeasures, viewDimensions, predicates, computationFunction } = props;
const complementaryDimensions = complementaryFields({
all: metas.filter((f) => f.analyticType === 'dimension'),
selection: dimsInView,
all: allFields.filter((f) => f.analyticType === 'dimension'),
selection: viewDimensions,
});
const outlierList: Array<{ score: number; viiewFields: IField[] }> = complementaryDimensions.map(extendDim => {
const overallData = aggregate(dataSource, {
groupBy: [extendDim.fid],
op: 'aggregate',
measures: measInView.map((mea) => ({
field: mea.fid,
agg: (mea.aggName ?? 'sum') as IAggregator,
asFieldKey: getMeaAggKey(mea.fid, (mea.aggName ?? 'sum') as IAggregator),
})),

});
const viewData = aggregate(dataSource, {
groupBy: dimsInView.map((f) => f.fid),
op: 'aggregate',
measures: measInView.map((mea) => ({
field: mea.fid,
agg: (mea.aggName ?? 'sum') as IAggregator,
asFieldKey: getMeaAggKey(mea.fid, (mea.aggName ?? 'sum') as IAggregator),
}))
});
const subData = filterByPredicates(viewData, predicates);

let outlierNormalization = normalizeWithParent(
subData,
overallData,
measInView.map((mea) => mea.fid),
false
);

let outlierScore = compareDistribution(
outlierNormalization.normalizedData,
outlierNormalization.normalizedParentData,
[extendDim.fid],
measInView.map((mea) => mea.fid)
);
return {
viiewFields: measInView.concat(extendDim),
score: outlierScore,
const outlierList: {
score: number;
measureField: IField;
targetField: IField;
normalizedData: IRow[];
normalizedParentData: IRow[];
}[] = [];
for (let extendDim of complementaryDimensions) {
let extendDimFid = extendDim.fid;
let extraPreWorkflow: IViewWorkflowStep[] = [];
if (extendDim.semanticType === "quantitative") {
extraPreWorkflow.push({
type: "view",
query: [
{
op: "bin",
binBy: extendDim.fid,
binSize: QUANT_BIN_NUM,
newBinCol: extendDimFid
}
]
})
}
for (let mea of viewMeasures) {
const overallWorkflow = toWorkflow(viewFilters, allFields, [extendDim], [mea], true, 'none');
const fullOverallWorkflow = extraPreWorkflow ? [...extraPreWorkflow, ...overallWorkflow] : overallWorkflow
const overallData = await dataQueryServer(computationFunction, fullOverallWorkflow)
const viewWorkflow = toWorkflow(viewFilters, allFields, [...viewDimensions, extendDim], [mea], true, 'none');
const fullViewWorkflow = extraPreWorkflow ? [...extraPreWorkflow, ...viewWorkflow] : viewWorkflow
const viewData = await dataQueryServer(computationFunction, fullViewWorkflow);
const subData = filterByPredicates(viewData, predicates);
let outlierNormalization = normalizeWithParent(
subData,
overallData,
[getMeaAggKey(mea.fid, (mea.aggName ?? 'sum'))],
false
);
console.log(outlierNormalization)
let outlierScore = compareDistributionJS(
outlierNormalization.normalizedData,
outlierNormalization.normalizedParentData,
[extendDim.fid],
getMeaAggKey(mea.fid, (mea.aggName ?? 'sum'))
);
outlierList.push(
{
measureField: mea,
targetField: extendDim,
score: outlierScore,
normalizedData: subData,
normalizedParentData: overallData
}
)
}
}).sort((a, b) => b.score - a.score)

return outlierList;
}
return outlierList.sort((a, b) => b.score - a.score);
}
Loading

1 comment on commit d47707f

@vercel
Copy link

@vercel vercel bot commented on d47707f Aug 31, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.