@kanaries/graphic-walker 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/datasetFields/index.d.ts +3 -3
- package/dist/graphic-walker.es.js +7167 -7152
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +96 -96
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/store/index.d.ts +11 -1
- package/package.json +5 -3
- package/src/App.tsx +140 -0
- package/src/assets/kanaries.ico +0 -0
- package/src/components/clickMenu.tsx +29 -0
- package/src/components/container.tsx +16 -0
- package/src/components/dataTypeIcon.tsx +20 -0
- package/src/components/liteForm.tsx +16 -0
- package/src/components/modal.tsx +85 -0
- package/src/components/sizeSetting.tsx +95 -0
- package/src/components/tabs/pureTab.tsx +70 -0
- package/src/config.ts +57 -0
- package/src/constants.ts +1 -0
- package/src/dataSource/config.ts +62 -0
- package/src/dataSource/dataSelection/csvData.tsx +77 -0
- package/src/dataSource/dataSelection/gwFile.tsx +38 -0
- package/src/dataSource/dataSelection/index.tsx +57 -0
- package/src/dataSource/dataSelection/publicData.tsx +57 -0
- package/src/dataSource/index.tsx +78 -0
- package/src/dataSource/pannel.tsx +71 -0
- package/src/dataSource/table.tsx +125 -0
- package/src/dataSource/utils.ts +47 -0
- package/src/fields/aestheticFields.tsx +23 -0
- package/src/fields/components.tsx +159 -0
- package/src/fields/datasetFields/dimFields.tsx +45 -0
- package/src/fields/datasetFields/fieldPill.tsx +10 -0
- package/src/fields/datasetFields/index.tsx +28 -0
- package/src/fields/datasetFields/meaFields.tsx +58 -0
- package/src/fields/fieldsContext.tsx +59 -0
- package/src/fields/filterField/filterEditDialog.tsx +143 -0
- package/src/fields/filterField/filterPill.tsx +113 -0
- package/src/fields/filterField/index.tsx +61 -0
- package/src/fields/filterField/slider.tsx +236 -0
- package/src/fields/filterField/tabs.tsx +421 -0
- package/src/fields/obComponents/obFContainer.tsx +40 -0
- package/src/fields/obComponents/obPill.tsx +48 -0
- package/src/fields/posFields/index.tsx +33 -0
- package/src/fields/select.tsx +31 -0
- package/src/fields/utils.ts +31 -0
- package/src/index.css +13 -0
- package/src/index.tsx +12 -0
- package/src/insightBoard/index.tsx +30 -0
- package/src/insightBoard/mainBoard.tsx +203 -0
- package/src/insightBoard/radioGroupButtons.tsx +50 -0
- package/src/insightBoard/selectionSpec.ts +113 -0
- package/src/insightBoard/std2vegaSpec.ts +184 -0
- package/src/insightBoard/utils.ts +32 -0
- package/src/insights.ts +408 -0
- package/src/interfaces.ts +154 -0
- package/src/locales/en-US.json +140 -0
- package/src/locales/i18n.ts +50 -0
- package/src/locales/zh-CN.json +140 -0
- package/src/main.tsx +10 -0
- package/src/models/visSpecHistory.ts +129 -0
- package/src/renderer/index.tsx +104 -0
- package/src/segments/visNav.tsx +48 -0
- package/src/services.ts +139 -0
- package/src/store/commonStore.ts +158 -0
- package/src/store/index.tsx +53 -0
- package/src/store/visualSpecStore.ts +586 -0
- package/src/utils/autoMark.ts +34 -0
- package/src/utils/index.ts +251 -0
- package/src/utils/normalization.ts +158 -0
- package/src/utils/save.ts +46 -0
- package/src/vis/future-react-vega.tsx +193 -0
- package/src/vis/gen-vega.tsx +52 -0
- package/src/vis/react-vega.tsx +398 -0
- package/src/visualSettings/index.tsx +252 -0
- package/src/visualSettings/menubar.tsx +109 -0
- package/src/vite-env.d.ts +1 -0
- package/src/workers/explainer.worker.js +78 -0
- package/src/workers/filter.worker.js +70 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { DraggableProvided } from 'react-beautiful-dnd';
|
|
6
|
+
import { COUNT_FIELD_ID } from '../../constants';
|
|
7
|
+
import { IDraggableStateKey } from '../../interfaces';
|
|
8
|
+
import { useGlobalStore } from '../../store';
|
|
9
|
+
import { Pill } from '../components';
|
|
10
|
+
import { AGGREGATOR_LIST } from '../fieldsContext';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
interface PillProps {
|
|
14
|
+
provided: DraggableProvided;
|
|
15
|
+
fIndex: number;
|
|
16
|
+
dkey: IDraggableStateKey;
|
|
17
|
+
}
|
|
18
|
+
const OBPill: React.FC<PillProps> = props => {
|
|
19
|
+
const { provided, dkey, fIndex } = props;
|
|
20
|
+
const { vizStore } = useGlobalStore();
|
|
21
|
+
const { visualConfig } = vizStore;
|
|
22
|
+
const field = vizStore.draggableFieldState[dkey.id][fIndex];
|
|
23
|
+
const { t } = useTranslation('translation', { keyPrefix: 'constant.aggregator' });
|
|
24
|
+
|
|
25
|
+
return <Pill
|
|
26
|
+
ref={provided.innerRef}
|
|
27
|
+
colType={field.analyticType === 'dimension' ? 'discrete' : 'continuous'}
|
|
28
|
+
{...provided.draggableProps}
|
|
29
|
+
{...provided.dragHandleProps}
|
|
30
|
+
>
|
|
31
|
+
{field.name}
|
|
32
|
+
{field.analyticType === 'measure' && field.fid !== COUNT_FIELD_ID && visualConfig.defaultAggregated && (
|
|
33
|
+
<select
|
|
34
|
+
className="bg-transparent text-gray-700 float-right focus:outline-none focus:border-gray-500"
|
|
35
|
+
value={field.aggName || ''}
|
|
36
|
+
onChange={(e) => { vizStore.setFieldAggregator(dkey.id, fIndex, e.target.value) }}
|
|
37
|
+
>
|
|
38
|
+
{
|
|
39
|
+
AGGREGATOR_LIST.map(op => <option value={op} key={op}>{t(op)}</option>)
|
|
40
|
+
}
|
|
41
|
+
</select>
|
|
42
|
+
)}
|
|
43
|
+
{field.analyticType === 'dimension' && field.sort === 'ascending' && <BarsArrowUpIcon className='float-right w-3' role="status" aria-label="Sorted in ascending order" />}
|
|
44
|
+
{field.analyticType === 'dimension' && field.sort === 'descending' && <BarsArrowDownIcon className='float-right w-3' role="status" aria-label="Sorted in descending order" />}
|
|
45
|
+
</Pill>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default observer(OBPill);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import { Droppable } from "react-beautiful-dnd";
|
|
4
|
+
import { useGlobalStore } from '../../store';
|
|
5
|
+
import { FieldListContainer } from "../components";
|
|
6
|
+
import { DRAGGABLE_STATE_KEYS } from '../fieldsContext';
|
|
7
|
+
import OBFieldContainer from '../obComponents/obFContainer';
|
|
8
|
+
|
|
9
|
+
const PosFields: React.FC = props => {
|
|
10
|
+
const { vizStore } = useGlobalStore();
|
|
11
|
+
const { visualConfig } = vizStore;
|
|
12
|
+
const { geoms } = visualConfig;
|
|
13
|
+
|
|
14
|
+
const channels = useMemo(() => {
|
|
15
|
+
if (geoms[0] === 'arc') {
|
|
16
|
+
return DRAGGABLE_STATE_KEYS.filter(f => f.id === 'radius' || f.id === 'theta');
|
|
17
|
+
}
|
|
18
|
+
return DRAGGABLE_STATE_KEYS.filter(f => f.id === 'columns' || f.id === 'rows');
|
|
19
|
+
}, [geoms[0]])
|
|
20
|
+
return <div>
|
|
21
|
+
{
|
|
22
|
+
channels.map(dkey => <FieldListContainer name={dkey.id} key={dkey.id}>
|
|
23
|
+
<Droppable droppableId={dkey.id} direction="horizontal">
|
|
24
|
+
{(provided, snapshot) => (
|
|
25
|
+
<OBFieldContainer dkey={dkey} provided={provided} />
|
|
26
|
+
)}
|
|
27
|
+
</Droppable>
|
|
28
|
+
</FieldListContainer>)
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default observer(PosFields);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface Option {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
interface SelectProps {
|
|
7
|
+
options: Option[];
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Select: React.FC<SelectProps> = (props) => {
|
|
13
|
+
const { options = [], onChange, value } = props;
|
|
14
|
+
return (
|
|
15
|
+
<select
|
|
16
|
+
name="select-com"
|
|
17
|
+
value={value}
|
|
18
|
+
onChange={(e) => {
|
|
19
|
+
onChange(e.target.value);
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
{options.map((option) => (
|
|
23
|
+
<option key={option.id} value={option.id}>
|
|
24
|
+
{option.name}
|
|
25
|
+
</option>
|
|
26
|
+
))}
|
|
27
|
+
</select>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default Select;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function reorder(
|
|
2
|
+
list: any[],
|
|
3
|
+
originalIndex: number,
|
|
4
|
+
targetIndex: number
|
|
5
|
+
): any[] {
|
|
6
|
+
const nextList = [...list];
|
|
7
|
+
nextList.splice(originalIndex, 1);
|
|
8
|
+
nextList.splice(targetIndex, 0, list[originalIndex]);
|
|
9
|
+
return nextList;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface movedLists {
|
|
13
|
+
originList: any[];
|
|
14
|
+
targetList: any[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function move(
|
|
18
|
+
originalList: any[],
|
|
19
|
+
originIndex: number,
|
|
20
|
+
targetList: any[],
|
|
21
|
+
targetIndex: number
|
|
22
|
+
): movedLists {
|
|
23
|
+
let newOriginalList = [...originalList];
|
|
24
|
+
let [removed] = newOriginalList.splice(originIndex, 1);
|
|
25
|
+
let newTargetList = [...targetList];
|
|
26
|
+
newTargetList.splice(targetIndex, 0, removed);
|
|
27
|
+
return {
|
|
28
|
+
originList: newOriginalList,
|
|
29
|
+
targetList: newTargetList,
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
4
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
5
|
+
sans-serif;
|
|
6
|
+
-webkit-font-smoothing: antialiased;
|
|
7
|
+
-moz-osx-font-smoothing: grayscale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
code {
|
|
11
|
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
12
|
+
monospace;
|
|
13
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import App, { EditorProps } from './App';
|
|
3
|
+
import { StoreWrapper } from './store/index';
|
|
4
|
+
import { FieldsContextWrapper } from './fields/fieldsContext';
|
|
5
|
+
|
|
6
|
+
export const GraphicWalker: React.FC<EditorProps> = props => {
|
|
7
|
+
return <StoreWrapper>
|
|
8
|
+
<FieldsContextWrapper>
|
|
9
|
+
<App {...props} />
|
|
10
|
+
</FieldsContextWrapper>
|
|
11
|
+
</StoreWrapper>
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toJS } from 'mobx';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import React, { useCallback } from 'react';
|
|
4
|
+
import Modal from '../components/modal';
|
|
5
|
+
import { useGlobalStore } from '../store';
|
|
6
|
+
import InsightMainBoard from './mainBoard';
|
|
7
|
+
|
|
8
|
+
const InsightBoard: React.FC = props => {
|
|
9
|
+
const { commonStore, vizStore } = useGlobalStore();
|
|
10
|
+
const { showInsightBoard, currentDataset, filters } = commonStore;
|
|
11
|
+
const { viewDimensions, viewMeasures, draggableFieldState } = vizStore;
|
|
12
|
+
const onCloseModal = useCallback(() => {
|
|
13
|
+
commonStore.setShowInsightBoard(false);
|
|
14
|
+
}, [])
|
|
15
|
+
return <div>
|
|
16
|
+
{
|
|
17
|
+
showInsightBoard && <Modal onClose={onCloseModal}>
|
|
18
|
+
<InsightMainBoard
|
|
19
|
+
dataSource={currentDataset.dataSource}
|
|
20
|
+
fields={toJS(draggableFieldState.fields)}
|
|
21
|
+
viewDs={viewDimensions}
|
|
22
|
+
viewMs={viewMeasures}
|
|
23
|
+
filters={toJS(filters)}
|
|
24
|
+
/>
|
|
25
|
+
</Modal>
|
|
26
|
+
}
|
|
27
|
+
</div>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default observer(InsightBoard);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
|
2
|
+
import embed from 'vega-embed';
|
|
3
|
+
import { Insight, Utils, UnivariateSummary } from 'visual-insights';
|
|
4
|
+
import ReactJson from 'react-json-view';
|
|
5
|
+
import { IField, Filters, IMeasure, IRow } from '../interfaces';
|
|
6
|
+
import { IExplaination, IMeasureWithStat } from '../insights';
|
|
7
|
+
import { getExplaination, IVisSpace } from '../services';
|
|
8
|
+
import { baseVis, IReasonType } from './std2vegaSpec';
|
|
9
|
+
import RadioGroupButtons from './radioGroupButtons';
|
|
10
|
+
import { formatFieldName, mergeMeasures } from './utils'
|
|
11
|
+
|
|
12
|
+
const collection = Insight.IntentionWorkerCollection.init();
|
|
13
|
+
|
|
14
|
+
const ReasonTypeNames: { [key: string]: string} = {
|
|
15
|
+
'selection_dim_distribution': '维度线索',
|
|
16
|
+
'selection_mea_distribution': '度量线索',
|
|
17
|
+
'children_major_factor': '子节点主因',
|
|
18
|
+
'children_outlier': '子节点异常'
|
|
19
|
+
}
|
|
20
|
+
collection.enable(Insight.DefaultIWorker.cluster, false);
|
|
21
|
+
interface SubSpace {
|
|
22
|
+
dimensions: string[];
|
|
23
|
+
measures: IMeasure[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface InsightMainBoardProps {
|
|
27
|
+
dataSource: IRow[];
|
|
28
|
+
fields: Readonly<IField[]>;
|
|
29
|
+
filters?: Filters;
|
|
30
|
+
viewDs: IField[];
|
|
31
|
+
viewMs: IField[];
|
|
32
|
+
}
|
|
33
|
+
const InsightMainBoard: React.FC<InsightMainBoardProps> = props => {
|
|
34
|
+
const { dataSource, fields, viewDs, viewMs, filters } = props;
|
|
35
|
+
const [recSpaces, setRecSpaces] = useState<IExplaination[]>([]);
|
|
36
|
+
const [visSpaces, setVisSpaces] = useState<IVisSpace[]>([]);
|
|
37
|
+
const [visIndex, setVisIndex] = useState<number>(0);
|
|
38
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
39
|
+
const [valueExp, setValueExp] = useState<IMeasureWithStat[]>([]);
|
|
40
|
+
const container = useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
const dimsWithTypes = useMemo(() => {
|
|
43
|
+
const dimensions = fields
|
|
44
|
+
.filter((f) => f.analyticType === 'dimension')
|
|
45
|
+
.map((f) => f.fid)
|
|
46
|
+
.filter((f) => !Utils.isFieldUnique(dataSource, f));
|
|
47
|
+
return UnivariateSummary.getAllFieldTypes(dataSource, dimensions);
|
|
48
|
+
}, [fields, dataSource])
|
|
49
|
+
|
|
50
|
+
const measWithTypes = useMemo(() => {
|
|
51
|
+
const measures = fields.filter((f) => f.analyticType === 'measure').map((f) => f.fid);
|
|
52
|
+
return measures.map((m) => ({
|
|
53
|
+
name: m,
|
|
54
|
+
type: 'quantitative',
|
|
55
|
+
}));
|
|
56
|
+
}, [fields]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (dimsWithTypes.length > 0 && measWithTypes.length > 0 && dataSource.length > 0) {
|
|
60
|
+
const measures = fields.filter((f) => f.analyticType === 'measure').map((f) => f.fid);
|
|
61
|
+
const dimensions = dimsWithTypes.map(d => d.name);
|
|
62
|
+
const currentSpace: SubSpace = {
|
|
63
|
+
dimensions: viewDs.map((f) => f.fid),
|
|
64
|
+
measures: viewMs.map((f) => ({
|
|
65
|
+
key: f.fid,
|
|
66
|
+
op: f.aggName as any
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
setLoading(true);
|
|
70
|
+
|
|
71
|
+
getExplaination({
|
|
72
|
+
dimensions,
|
|
73
|
+
measures,
|
|
74
|
+
dataSource,
|
|
75
|
+
currentSpace,
|
|
76
|
+
filters
|
|
77
|
+
}).then(({ visSpaces, explainations, valueExp }) => {
|
|
78
|
+
setRecSpaces(explainations);
|
|
79
|
+
setVisSpaces(visSpaces);
|
|
80
|
+
setValueExp(valueExp);
|
|
81
|
+
setLoading(false);
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}, [fields, viewDs, viewMs, measWithTypes, filters, dimsWithTypes, measWithTypes, dataSource])
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const RecSpace = recSpaces[visIndex];
|
|
88
|
+
const visSpec = visSpaces[visIndex];
|
|
89
|
+
if (container.current && RecSpace && visSpec) {
|
|
90
|
+
const usePredicates: boolean =
|
|
91
|
+
RecSpace.type === 'selection_dim_distribution' ||
|
|
92
|
+
RecSpace.type === 'selection_mea_distribution';
|
|
93
|
+
const mergedMeasures = mergeMeasures(RecSpace.measures, RecSpace.extendMs);
|
|
94
|
+
const _vegaSpec = baseVis(
|
|
95
|
+
visSpec.schema,
|
|
96
|
+
visSpec.schema.geomType && visSpec.schema.geomType[0] === 'point'
|
|
97
|
+
? dataSource
|
|
98
|
+
: visSpec.dataView,
|
|
99
|
+
// result.aggData,
|
|
100
|
+
[...RecSpace.dimensions, ...RecSpace.extendDs],
|
|
101
|
+
[...RecSpace.measures, ...RecSpace.extendMs].map(m => m.key),
|
|
102
|
+
usePredicates ? RecSpace.predicates : null,
|
|
103
|
+
mergedMeasures.map((m) => ({
|
|
104
|
+
op: m.op,
|
|
105
|
+
field: m.key,
|
|
106
|
+
as: m.key,
|
|
107
|
+
})),
|
|
108
|
+
fields,
|
|
109
|
+
RecSpace.type as IReasonType,
|
|
110
|
+
true,
|
|
111
|
+
true
|
|
112
|
+
);
|
|
113
|
+
if (container.current) {
|
|
114
|
+
embed(container.current, _vegaSpec);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}, [visIndex, recSpaces, visSpaces, fields, dataSource])
|
|
118
|
+
|
|
119
|
+
const FilterDesc = useMemo<React.ReactElement[]>(() => {
|
|
120
|
+
if (filters) {
|
|
121
|
+
const dimValues = Object.keys(filters)
|
|
122
|
+
.filter(k => filters[k].length > 0)
|
|
123
|
+
.map((k, ki) => {
|
|
124
|
+
return <div key={`dim-${ki}`}>
|
|
125
|
+
<div className="inline bg-gray-400 p-1 rounded underline text-white">{formatFieldName(k, fields)}</div>
|
|
126
|
+
<div className="inline text-lg ml-1 mr-1">=</div>
|
|
127
|
+
<div className="inline bg-blue-600 p-1 rounded text-white">{filters[k]}</div>
|
|
128
|
+
</div>
|
|
129
|
+
});
|
|
130
|
+
return dimValues
|
|
131
|
+
}
|
|
132
|
+
return []
|
|
133
|
+
}, [filters])
|
|
134
|
+
|
|
135
|
+
const valueDesc = useMemo<React.ReactElement[]>(() => {
|
|
136
|
+
const meaStatus = valueExp.map((mea, mi) =>
|
|
137
|
+
<div key={`mea-${mi}`}>
|
|
138
|
+
<span className="bg-gray-400 p-1 rounded underline text-white">{formatFieldName(mea.key, fields)}({mea.op})</span>的取值<span className="bg-red-500 p-1 rounded text-white">{mea.score === 1 ? '大于' : '小于'}预期 </span>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
return meaStatus
|
|
142
|
+
}, [valueExp])
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div style={{ maxHeight: '80vh', minHeight: '200px', overflowY: 'auto', maxWidth: '880px' }}>
|
|
146
|
+
<div className="text-xs">
|
|
147
|
+
{FilterDesc}
|
|
148
|
+
</div>
|
|
149
|
+
<div className="text-xs mt-2 mb-2">
|
|
150
|
+
{valueDesc}
|
|
151
|
+
</div>
|
|
152
|
+
{loading && (
|
|
153
|
+
<div className="animate-spin inline-block mr-2 ml-2 w-16 h-16 rounded-full border-t-2 border-l-2 border-blue-500"></div>
|
|
154
|
+
)}
|
|
155
|
+
<div style={{ display: 'flex' }}>
|
|
156
|
+
<div style={{ flexBasis: '200px', flexShrink: 0, maxHeight: '800px', overflowY: 'auto' }}>
|
|
157
|
+
<RadioGroupButtons
|
|
158
|
+
choosenIndex={visIndex}
|
|
159
|
+
options={recSpaces.map((s, i) => ({
|
|
160
|
+
value: s.type || '' + i,
|
|
161
|
+
label: `${s.type ? ReasonTypeNames[s.type] : '未识别'}: ${
|
|
162
|
+
s.score.toFixed(2)
|
|
163
|
+
}`,
|
|
164
|
+
}))}
|
|
165
|
+
onChange={(v, i) => {
|
|
166
|
+
setVisIndex(i);
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="p-4 text-sm">
|
|
171
|
+
<div ref={container}></div>
|
|
172
|
+
{recSpaces[visIndex] && (
|
|
173
|
+
<div>
|
|
174
|
+
维度是{recSpaces[visIndex].dimensions.map(f => formatFieldName(f, fields)).join(', ')}。<br />
|
|
175
|
+
度量是{recSpaces[visIndex].measures.map((m) => m.key).map(f => formatFieldName(f, fields)).join(', ')}。<br />
|
|
176
|
+
此时具有
|
|
177
|
+
{recSpaces[visIndex].type
|
|
178
|
+
? ReasonTypeNames[recSpaces[visIndex].type!]
|
|
179
|
+
: '未识别'}{' '}
|
|
180
|
+
,评分为{recSpaces[visIndex].score}。
|
|
181
|
+
<br />
|
|
182
|
+
{recSpaces[visIndex].description &&
|
|
183
|
+
recSpaces[visIndex].description.intMeasures &&
|
|
184
|
+
FilterDesc +
|
|
185
|
+
recSpaces[visIndex].description.intMeasures
|
|
186
|
+
.map(
|
|
187
|
+
(mea: any) =>
|
|
188
|
+
`${formatFieldName(mea.key, fields)}(${mea.op})}的取值${
|
|
189
|
+
mea.score === 1 ? '大于' : '小于'
|
|
190
|
+
}预期`
|
|
191
|
+
)
|
|
192
|
+
.join(', ')}
|
|
193
|
+
<br />
|
|
194
|
+
<ReactJson src={recSpaces[visIndex].description} />
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default InsightMainBoard;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
const RGBContainer = styled.div`
|
|
5
|
+
font-size: 14px;
|
|
6
|
+
.option {
|
|
7
|
+
padding: 0.8em;
|
|
8
|
+
margin: 4px;
|
|
9
|
+
border: 1px solid #f0f0f0;
|
|
10
|
+
background-color: #f0f0f0;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
}
|
|
13
|
+
.choosen.option {
|
|
14
|
+
background-color: #fff;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export interface RGBOption {
|
|
19
|
+
label: string;
|
|
20
|
+
value: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RadioGroupButtonsProps {
|
|
24
|
+
options: RGBOption[];
|
|
25
|
+
onChange?: (value: any, index: number) => void;
|
|
26
|
+
choosenIndex: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const RadioGroupButtons: React.FC<RadioGroupButtonsProps> = (props) => {
|
|
30
|
+
const { options, onChange, choosenIndex } = props;
|
|
31
|
+
return (
|
|
32
|
+
<RGBContainer>
|
|
33
|
+
{options.map((op, i) => (
|
|
34
|
+
<div
|
|
35
|
+
key={i}
|
|
36
|
+
className={`${choosenIndex === i ? 'choosen' : ''} option`}
|
|
37
|
+
onClick={() => {
|
|
38
|
+
if (onChange) {
|
|
39
|
+
onChange(op.value, i);
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{op.label}
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</RGBContainer>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default RadioGroupButtons;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Specification } from 'visual-insights';
|
|
2
|
+
import { IRow, SemanticType } from '../interfaces';
|
|
3
|
+
export const geomTypeMap: { [key: string]: any } = {
|
|
4
|
+
interval: 'bar',
|
|
5
|
+
line: 'line',
|
|
6
|
+
point: 'point',
|
|
7
|
+
// density: 'rect'
|
|
8
|
+
density: 'point',
|
|
9
|
+
};
|
|
10
|
+
export function selectionVis(
|
|
11
|
+
query: Specification,
|
|
12
|
+
dataSource: IRow[],
|
|
13
|
+
dimensions: string[],
|
|
14
|
+
measures: string[],
|
|
15
|
+
aggregatedMeasures: Array<{ op: string; field: string; as: string }>,
|
|
16
|
+
fieldFeatures: Array<{ name: string; type: SemanticType }>,
|
|
17
|
+
defaultAggregated?: boolean,
|
|
18
|
+
defaultStack?: boolean
|
|
19
|
+
) {
|
|
20
|
+
const {
|
|
21
|
+
position = [],
|
|
22
|
+
color = [],
|
|
23
|
+
size = [],
|
|
24
|
+
facets = [],
|
|
25
|
+
opacity = [],
|
|
26
|
+
geomType = [],
|
|
27
|
+
page = [],
|
|
28
|
+
} = query;
|
|
29
|
+
|
|
30
|
+
function adjustField(fieldName: string): string {
|
|
31
|
+
if (defaultAggregated && measures.includes(fieldName)) {
|
|
32
|
+
let aggField = aggregatedMeasures.find((mea) => {
|
|
33
|
+
return mea.field === fieldName;
|
|
34
|
+
});
|
|
35
|
+
return aggField ? aggField.as : fieldName;
|
|
36
|
+
}
|
|
37
|
+
return fieldName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getFieldType(field: string): SemanticType {
|
|
41
|
+
let targetField = fieldFeatures.find((f) => f.name === field);
|
|
42
|
+
return targetField ? targetField.type : 'nominal';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let chartWidth = 500; //container.current ? container.current.offsetWidth * 0.8 : 600;
|
|
46
|
+
const fieldMap: any = {
|
|
47
|
+
x: position[0],
|
|
48
|
+
y: position[1],
|
|
49
|
+
color: color[0],
|
|
50
|
+
size: size[0],
|
|
51
|
+
opacity: opacity[0],
|
|
52
|
+
row: facets[0],
|
|
53
|
+
column: facets[1],
|
|
54
|
+
};
|
|
55
|
+
let spec: any = {
|
|
56
|
+
width: chartWidth,
|
|
57
|
+
data: {
|
|
58
|
+
values: dataSource,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
let basicSpec: any = {
|
|
62
|
+
width: chartWidth,
|
|
63
|
+
mark: {
|
|
64
|
+
type: geomType[0] && geomTypeMap[geomType[0]] ? geomTypeMap[geomType[0]] : geomType[0],
|
|
65
|
+
tooltip: true,
|
|
66
|
+
},
|
|
67
|
+
encoding: {},
|
|
68
|
+
};
|
|
69
|
+
for (let channel in fieldMap) {
|
|
70
|
+
if (fieldMap[channel]) {
|
|
71
|
+
basicSpec.encoding[channel] = {
|
|
72
|
+
field: adjustField(fieldMap[channel]),
|
|
73
|
+
type: getFieldType(fieldMap[channel]),
|
|
74
|
+
};
|
|
75
|
+
if (
|
|
76
|
+
['x', 'y'].includes(channel) &&
|
|
77
|
+
getFieldType(fieldMap[channel]) === 'quantitative' &&
|
|
78
|
+
!defaultStack
|
|
79
|
+
) {
|
|
80
|
+
basicSpec.encoding[channel].stack = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!defaultStack && opacity.length === 0) {
|
|
85
|
+
basicSpec.encoding.opacity = { value: 0.7 };
|
|
86
|
+
}
|
|
87
|
+
if (page.length === 0) {
|
|
88
|
+
spec = {
|
|
89
|
+
...spec,
|
|
90
|
+
...basicSpec,
|
|
91
|
+
};
|
|
92
|
+
} else if (page.length > 0) {
|
|
93
|
+
basicSpec.transform = [
|
|
94
|
+
{ filter: { selection: 'brush' } },
|
|
95
|
+
defaultAggregated
|
|
96
|
+
? {
|
|
97
|
+
aggregate: aggregatedMeasures,
|
|
98
|
+
groupby: dimensions.filter((dim) => dim !== page[0]),
|
|
99
|
+
}
|
|
100
|
+
: null,
|
|
101
|
+
].filter(Boolean);
|
|
102
|
+
let sliderSpec = {
|
|
103
|
+
width: chartWidth,
|
|
104
|
+
mark: 'tick',
|
|
105
|
+
selection: { brush: { encodings: ['x'], type: 'interval' } },
|
|
106
|
+
encoding: {
|
|
107
|
+
x: { field: page[0], type: getFieldType(page[0]) },
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
spec.vconcat = [basicSpec, sliderSpec];
|
|
111
|
+
}
|
|
112
|
+
return spec;
|
|
113
|
+
}
|