-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: init explain data algo and panel * feat: use computation engine for explain data
- Loading branch information
Showing
6 changed files
with
352 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
packages/graphic-walker/src/components/explainData/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
123
packages/graphic-walker/src/lib/insights/explainBySelection.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.
d47707f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
graphic-walker – ./
graphic-walker-git-main-kanaries.vercel.app
graphic-walker.kanaries.net
graphic-walker-kanaries.vercel.app
graphic-walker-app.vercel.app