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

feat: global filters #90

Merged
merged 10 commits into from
Jun 25, 2022
Merged
2 changes: 1 addition & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@
"vega": "^5.22.1",
"vega-embed": "^6.20.8",
"vega-lite": "^5.2.0",
"visual-insights": "0.7.17",
"visual-insights": "0.8.10",
"web-vitals": "^0.2.4",
"worker-loader": "^3.0.7"
},
2 changes: 1 addition & 1 deletion packages/frontend/public/index.html
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Rath</title>
<title>RATH: Automated knowledge discovery in data</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
4 changes: 3 additions & 1 deletion packages/frontend/public/locales/en-US.json
Original file line number Diff line number Diff line change
@@ -3,7 +3,8 @@
"dimension": "dimension",
"measure": "measure",
"name": "Name",
"home": "Home"
"home": "Home",
"settings": "Settings"
},
"menu": {
"dataSource": "DataSource",
@@ -12,6 +13,7 @@
"dashBoard": "DashBoard",
"explainer": "Explainer",
"editor": "Visual Editor",
"vizPipe": "interactive Pipeline",
"support": "Support",
"lts": "Explore",
"pattern": "Build Knowledge(beta)",
7 changes: 4 additions & 3 deletions packages/frontend/public/locales/zh-CN.json
Original file line number Diff line number Diff line change
@@ -3,7 +3,8 @@
"dimension": "维度",
"measure": "度量",
"name": "名称",
"home": "主页"
"home": "主页",
"settings": "设置"
},
"menu": {
"dataSource": "数据源",
@@ -273,8 +274,8 @@
"relateFeatures": "特征推荐",
"explainDiff": "差异性分析",
"pointInterests": "拆分",
"pin": "选为主视图",
"compare": "选为比对视图",
"pin": "深入分析",
"compare": "比对分析",
"vizsys": {
"title": "可视化推荐系统",
"lite": "轻量模式(近似算法、更快)",
4 changes: 2 additions & 2 deletions packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ require('intl/locale-data/jsonp/zh.js')

function App() {
const { langStore, commonStore } = useGlobalStore()
const { appKey } = commonStore;
const { appKey, navMode } = commonStore;

useEffect(() => {
initRathWorker(commonStore.computationEngine);
@@ -93,7 +93,7 @@ function App() {
</div>
</div> */}
<div className="main-app-container">
<div className="main-app-nav">
<div className="main-app-nav" style={{ flexBasis: navMode === 'text' ? '220px' : '20px' }}>
<AppNav />
</div>
<div className="main-app-content">
32 changes: 26 additions & 6 deletions packages/frontend/src/components/appNav.tsx
Original file line number Diff line number Diff line change
@@ -31,26 +31,45 @@ const LogoBar = styled.div`
}
`

const IconMap = {
[PIVOT_KEYS.lts]: 'Robot',
[PIVOT_KEYS.pattern]: 'Manufacturing',
[PIVOT_KEYS.editor]: 'LineChart',
[PIVOT_KEYS.support]: 'Telemarketer',
[PIVOT_KEYS.dataSource]: 'DataManagementSettings',
[PIVOT_KEYS.noteBook]: 'Game',
[PIVOT_KEYS.dashBoard]: 'ViewDashboard',
[PIVOT_KEYS.gallery]: 'ReadingMode',
[PIVOT_KEYS.explainer]: 'SiteScan'
} as {
[key: string]: string
}

function getIcon (k: string): string {
return IconMap[k] || 'Settings'
}

interface AppNavProps {}
const AppNav: React.FC<AppNavProps> = props => {
const { commonStore } = useGlobalStore()

const { appKey } = commonStore;
const { appKey, navMode } = commonStore;

const getLinks = useCallback((pivotKeys: string[]) => {
return pivotKeys.map(p => {
return {
url: `#${p}`,
key: p,
name: intl.get(`menu.${p}`),
name: navMode === 'text' ? intl.get(`menu.${p}`) : '',
forceAnchor: true,
iconProps: navMode === 'icon' ? {iconName: getIcon(p) } : undefined,
onClick (e: any) {
e.preventDefault();
commonStore.setAppKey(p)
}
}
})
}, [commonStore])
}, [commonStore, navMode])

const groups: INavLinkGroup[] = [
{
@@ -63,7 +82,7 @@ const AppNav: React.FC<AppNavProps> = props => {
{
url: '#dev-mode',
key: intl.get('menu.devCollection'),
name: intl.get('menu.devCollection'),
name: navMode === 'text' ? intl.get('menu.devCollection') : '',
isExpanded: false,
forceAnchor: true,
onClick (e: any) { e.preventDefault() },
@@ -76,7 +95,8 @@ const AppNav: React.FC<AppNavProps> = props => {
},
{
url: '/',
name: intl.get('common.home')
name: navMode === 'text' ? intl.get('common.home') : '',
iconProps: navMode === 'icon' ? {iconName: 'Home'} : undefined
},
...getLinks([PIVOT_KEYS.support]),
// ...pivotList.map(item => {
@@ -106,7 +126,7 @@ const AppNav: React.FC<AppNavProps> = props => {
alt="rath"
/>
</a>
<h1>RATH</h1>
{navMode === 'text' && <h1>RATH</h1>}
</LogoBar>
<Nav
selectedKey={appKey}
159 changes: 159 additions & 0 deletions packages/frontend/src/components/fieldFilter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useId } from '@uifabric/react-hooks';
import produce from 'immer';
import { toJS } from 'mobx';
import { observer } from 'mobx-react-lite';
import { Callout, ChoiceGroup, DefaultButton, IconButton, PrimaryButton, Stack, Selection, SelectionMode, Toggle } from 'office-ui-fabric-react';
import React, { useCallback, useMemo, useState } from 'react';
import { IFilter } from '../../interfaces';
import { useGlobalStore } from '../../store';
import RangeSelection from './rangeSelection';
import SetSelection from './setSelection';


interface FieldFilterProps {
fid: string;
}
const FieldFilter: React.FC<FieldFilterProps> = props => {
const { fid } = props;
const buttonId = useId('filter-button');
const [showFilterConfig, setShowFilterConfig] = useState<boolean>(false);
const { dataSourceStore } = useGlobalStore();

const meta = dataSourceStore.fieldMetas.find(fm => fm.fid === fid);

const { rawData } = dataSourceStore;

const fieldValues = useMemo(() => rawData.map(r => r[fid]), [rawData, fid]);

const [filter, setFilter] = useState<IFilter>((meta && meta.semanticType === 'quantitative') ? {
fid,
type: 'range',
range: [0, 0],
} : {
fid,
type: 'set',
values: []
})

const selection = useMemo(() => {
return new Selection({
selectionMode: SelectionMode.multiple,
onSelectionChanged: () => {},
// items: meta.distribution,
// getKey: () => ''
})
}, [])

const submitFilter = useCallback(() => {
if (filter.type === 'range') {
dataSourceStore.setFilter(filter);
} else {
if (meta?.distribution) {
const nextFilter: IFilter = {
...filter,
values: selection.getSelectedIndices().map(i => meta.distribution[i].memberName)
}
dataSourceStore.setFilter(nextFilter);
}
}
setShowFilterConfig(false);
}, [filter, meta?.distribution, dataSourceStore, selection])

const toggleShowFilter = useCallback(() => {
setShowFilterConfig(v => !v);
}, [])

const onRangeValueChange = useCallback((v: number, r: [number, number]) => {
setFilter(f => {
const nextF = produce(f, draft => {
if (draft.type === 'range' && r) {
draft.range = r
}
})
return nextF
})
}, [])

return <div>
<IconButton id={buttonId}
text="Filter"
iconProps={{ iconName: 'filter' }}
onClick={toggleShowFilter}
/>
{
showFilterConfig && <Callout target={`#${buttonId}`}
onDismiss={toggleShowFilter}
>
<div style={{ padding: '1em', minWidth: '400px'}}>

<h2>Filter Config</h2>
<Toggle
onText='Apply'
offText='Disable'
label="Filter"
checked={!filter.disable}
onChange={(e, checked) => {
setFilter(f => ({
...f,
disable: !checked
}))
}}
/>
<div>
<ChoiceGroup
label="Filter by"
options={[
{ key: 'range', text: 'range' },
{ key: 'set', text: 'set'}
]}
selectedKey={filter.type}
onChange={(ev, op) => {
if (op) {
setFilter(f => {
const nextF = produce(f, draft => {
draft.type = op.key as any
if (draft.type === 'set') {
draft.values = []
} else {
draft.range = [0, 0]
}
})
return nextF;
})
}
}}
/>
</div>
{
filter.type === 'set' && meta && <SetSelection
dist={toJS(meta.distribution)}
selection={selection}
/>
}
{
filter.type === 'range' && meta && <RangeSelection
values={fieldValues}
left={filter.range[0]}
right={filter.range[1]}
onValueChange={onRangeValueChange}
/>
}
<Stack horizontal>
<PrimaryButton
text="Submit"
onClick={submitFilter}
/>
<DefaultButton
style={{ marginLeft: '1em' }}
text="Cancel"
onClick={toggleShowFilter}
/>
</Stack>
</div>

</Callout>
}
</div>
}

export default observer(FieldFilter);
44 changes: 44 additions & 0 deletions packages/frontend/src/components/fieldFilter/rangeSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Slider } from 'office-ui-fabric-react';
import React, { useEffect, useMemo } from 'react';


interface RangeSelectionProps {
values: number[];
left: number;
right: number;
onValueChange: (value: number, range: [number, number]) => void;
}
const RangeSelection: React.FC<RangeSelectionProps> = props => {
const { values, left, right, onValueChange } = props;

const fieldRange = useMemo<[number, number]>(() => {
if (values.length === 0) return [0, 0]
let _min = Infinity;
let _max = -Infinity;
for (let i = 0; i < values.length; i++) {
if (values[i] > _max) _max = values[i];
if (values[i] < _min) _min = values[i];
}
return [_min, _max]
}, [values])

useEffect(() => {
onValueChange(0 ,fieldRange);
}, [fieldRange, onValueChange])

return <div>
<Slider
label='range'
min={fieldRange[0]}
max={fieldRange[1]}
value={right}
lowerValue={left}
ranged
onChange={(v, r) => {
r && onValueChange(v, r);
}}
/>
</div>
}

export default RangeSelection;
38 changes: 38 additions & 0 deletions packages/frontend/src/components/fieldFilter/setSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DetailsList, SelectionMode, Selection } from 'office-ui-fabric-react';
import React from 'react';
import { IFieldMeta } from '../../interfaces';

interface SetSelectionProps {
dist: IFieldMeta['distribution'];
selection: Selection;
}

const SetSelection: React.FC<SetSelectionProps> = props => {
const { dist, selection } = props;

return <div style={{ maxHeight: '200px', overflow: 'auto' }}>
<DetailsList
selection={selection}
selectionMode={SelectionMode.multiple}
onItemInvoked={(p) => { console.log(p); }}
columns={[
{
key: 'memberName',
name: 'Member Name',
fieldName: 'memberName',
minWidth: 60
},
{
key: 'count',
name: 'Count',
fieldName: 'count',
minWidth: 40
}
]}
compact
items={dist}
/>
</div>
}

export default SetSelection;
Loading