@kanaries/graphic-walker 0.2.4 → 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/package.json +3 -2
- 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,159 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import styled from "styled-components";
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { COLORS } from "../config";
|
|
5
|
+
|
|
6
|
+
export const AestheticSegment = styled.div`
|
|
7
|
+
border: 1px solid #dfe3e8;
|
|
8
|
+
font-size: 12px;
|
|
9
|
+
margin: 0.2em;
|
|
10
|
+
|
|
11
|
+
.aes-header{
|
|
12
|
+
border-bottom: 1px solid #dfe3e8;
|
|
13
|
+
padding: 0.6em;
|
|
14
|
+
h4 {
|
|
15
|
+
font-weight: 400;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
.aes-container{
|
|
19
|
+
overflow-x: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
export const FieldListContainer: React.FC<{ name: string }> = (props) => {
|
|
25
|
+
const { t } = useTranslation('translation', { keyPrefix: 'constant.draggable_key' });
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<FieldListSegment>
|
|
29
|
+
<div className="fl-header">
|
|
30
|
+
<h4>{t(props.name)}</h4>
|
|
31
|
+
</div>
|
|
32
|
+
<div className="fl-container">{props.children}</div>
|
|
33
|
+
</FieldListSegment>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const AestheticFieldContainer: React.FC<{ name: string }> = props => {
|
|
38
|
+
const { t } = useTranslation('translation', { keyPrefix: 'constant.draggable_key' });
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<AestheticSegment>
|
|
42
|
+
<div className="aes-header cursor-default select-none">
|
|
43
|
+
<h4>{t(props.name)}</h4>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="aes-container">{props.children}</div>
|
|
46
|
+
</AestheticSegment>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const FilterFieldContainer: React.FC = props => {
|
|
51
|
+
const { t } = useTranslation('translation', { keyPrefix: 'constant.draggable_key' });
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<FilterFieldSegment>
|
|
55
|
+
<div className="flt-header cursor-default select-none">
|
|
56
|
+
<h4>{t('filters')}</h4>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flt-container">{props.children}</div>
|
|
59
|
+
</FilterFieldSegment>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const FieldsContainer = styled.div`
|
|
64
|
+
display: flex;
|
|
65
|
+
padding: 0.2em;
|
|
66
|
+
min-height: 2.4em;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
>div{
|
|
69
|
+
margin: 1px;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export const FilterFieldsContainer = styled.div({
|
|
74
|
+
display: 'flex',
|
|
75
|
+
flexDirection: 'column',
|
|
76
|
+
paddingBlock: '0.5em 0.8em',
|
|
77
|
+
paddingInline: '0.2em',
|
|
78
|
+
minHeight: '4em',
|
|
79
|
+
'> div': {
|
|
80
|
+
marginBlock: '0.3em',
|
|
81
|
+
marginInline: '1px',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const FieldListSegment = styled.div`
|
|
86
|
+
display: flex;
|
|
87
|
+
border: 1px solid #dfe3e8;
|
|
88
|
+
margin: 0.2em;
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
div.fl-header {
|
|
91
|
+
/* flex-basis: 100px; */
|
|
92
|
+
width: 100px;
|
|
93
|
+
border-right: 1px solid #dfe3e8;
|
|
94
|
+
flex-shrink: 0;
|
|
95
|
+
h4 {
|
|
96
|
+
margin: 0.6em;
|
|
97
|
+
font-weight: 400;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
div.fl-container {
|
|
101
|
+
flex-grow: 10;
|
|
102
|
+
/* display: flex;
|
|
103
|
+
flex-wrap: wrap; */
|
|
104
|
+
/* overflow-x: auto;
|
|
105
|
+
overflow-y: hidden; */
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
export const FilterFieldSegment = styled.div({
|
|
110
|
+
border: '1px solid #dfe3e8',
|
|
111
|
+
fontSize: '12px',
|
|
112
|
+
margin: '0.2em',
|
|
113
|
+
|
|
114
|
+
'.flt-header': {
|
|
115
|
+
borderBottom: '1px solid #dfe3e8',
|
|
116
|
+
padding: '0.6em',
|
|
117
|
+
|
|
118
|
+
'> h4': {
|
|
119
|
+
fontWeight: 400,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
'.flt-container': {
|
|
124
|
+
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const Pill = styled.div<{colType: 'discrete' | 'continuous'}>`
|
|
129
|
+
background-color: ${props => props.colType === 'continuous' ? COLORS.white : COLORS.black};
|
|
130
|
+
border-color: ${props => props.colType === 'continuous' ? COLORS.black : COLORS.white};
|
|
131
|
+
color: ${props => props.colType === 'continuous' ? COLORS.black : COLORS.white};
|
|
132
|
+
-moz-user-select: none;
|
|
133
|
+
-ms-user-select: none;
|
|
134
|
+
-webkit-align-items: center;
|
|
135
|
+
-webkit-user-select: none;
|
|
136
|
+
align-items: center;
|
|
137
|
+
border-radius: 10px;
|
|
138
|
+
border-style: solid;
|
|
139
|
+
border-width: 1px;
|
|
140
|
+
box-sizing: border-box;
|
|
141
|
+
cursor: default;
|
|
142
|
+
display: -webkit-flex;
|
|
143
|
+
display: flex;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
height: 20px;
|
|
146
|
+
min-width: 150px;
|
|
147
|
+
overflow-y: hidden;
|
|
148
|
+
padding: 0 10px;
|
|
149
|
+
user-select: none;
|
|
150
|
+
text-overflow: ellipsis;
|
|
151
|
+
white-space: nowrap;
|
|
152
|
+
/* --tw-ring-offset-shadow: 0 0 #0000;
|
|
153
|
+
--tw-ring-shadow: 0 0 #0000;
|
|
154
|
+
--tw-shadow-color: rgb(6 182 212/0.5);
|
|
155
|
+
--tw-shadow: var(--tw-shadow-colored);
|
|
156
|
+
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);
|
|
157
|
+
box-shadow: var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow); */
|
|
158
|
+
`
|
|
159
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Draggable, DroppableProvided } from 'react-beautiful-dnd';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { useGlobalStore } from '../../store';
|
|
5
|
+
import DataTypeIcon from '../../components/dataTypeIcon';
|
|
6
|
+
import { FieldPill } from './fieldPill';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
provided: DroppableProvided;
|
|
10
|
+
}
|
|
11
|
+
const DimFields: React.FC<Props> = props => {
|
|
12
|
+
const { provided } = props;
|
|
13
|
+
const { vizStore } = useGlobalStore();
|
|
14
|
+
const dimensions = vizStore.draggableFieldState.dimensions;
|
|
15
|
+
return <div {...provided.droppableProps} ref={provided.innerRef}>
|
|
16
|
+
{dimensions.map((f, index) => (
|
|
17
|
+
<Draggable key={f.dragId} draggableId={f.dragId} index={index}>
|
|
18
|
+
{(provided, snapshot) => {
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<FieldPill
|
|
22
|
+
className="pt-0.5 pb-0.5 pl-2 pr-2 m-1 text-xs hover:bg-blue-100 rounded-full truncate"
|
|
23
|
+
ref={provided.innerRef}
|
|
24
|
+
isDragging={snapshot.isDragging}
|
|
25
|
+
{...provided.draggableProps}
|
|
26
|
+
{...provided.dragHandleProps}
|
|
27
|
+
>
|
|
28
|
+
<DataTypeIcon dataType={f.semanticType} analyticType={f.analyticType} /> {f.name}
|
|
29
|
+
</FieldPill>
|
|
30
|
+
{
|
|
31
|
+
<FieldPill className={`pt-0.5 pb-0.5 pl-2 pr-2 m-1 text-xs hover:bg-blue-100 rounded-full border-blue-400 border truncate ${snapshot.isDragging ? '' : 'hidden'}`}
|
|
32
|
+
isDragging={snapshot.isDragging}
|
|
33
|
+
>
|
|
34
|
+
<DataTypeIcon dataType={f.semanticType} analyticType={f.analyticType} /> {f.name}
|
|
35
|
+
</FieldPill>
|
|
36
|
+
}
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}}
|
|
40
|
+
</Draggable>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default observer(DimFields);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* react-beautiful-dnd v13.1.0 bug
|
|
5
|
+
* https://github.com/atlassian/react-beautiful-dnd/issues/2361
|
|
6
|
+
*/
|
|
7
|
+
export const FieldPill = styled.div<{isDragging: boolean}>`
|
|
8
|
+
transform: ${props => !props.isDragging && 'translate(0px, 0px) !important'};
|
|
9
|
+
user-select: none;
|
|
10
|
+
`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Droppable } from "react-beautiful-dnd";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { NestContainer } from "../../components/container";
|
|
5
|
+
import DimFields from "./dimFields";
|
|
6
|
+
import MeaFields from "./meaFields";
|
|
7
|
+
|
|
8
|
+
const DatasetFields: React.FC = (props) => {
|
|
9
|
+
const { t } = useTranslation("translation", { keyPrefix: "main.tabpanel.DatasetFields" });
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<NestContainer className="flex flex-col" style={{ height: "680px", paddingBlock: 0 }}>
|
|
13
|
+
<h4 className="text-xs mb-2 flex-grow-0 cursor-default select-none mt-2">{t("field_list")}</h4>
|
|
14
|
+
<div className="pd-1 overflow-y-auto" style={{ maxHeight: "380px" }}>
|
|
15
|
+
<Droppable droppableId="dimensions" direction="vertical">
|
|
16
|
+
{(provided, snapshot) => <DimFields provided={provided} />}
|
|
17
|
+
</Droppable>
|
|
18
|
+
</div>
|
|
19
|
+
<div className="border-t flex-grow pd-1 overflow-y-auto">
|
|
20
|
+
<Droppable droppableId="measures" direction="vertical">
|
|
21
|
+
{(provided, snapshot) => <MeaFields provided={provided} />}
|
|
22
|
+
</Droppable>
|
|
23
|
+
</div>
|
|
24
|
+
</NestContainer>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default DatasetFields;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Draggable, DroppableProvided } from 'react-beautiful-dnd';
|
|
3
|
+
import { observer } from 'mobx-react-lite';
|
|
4
|
+
import { useGlobalStore } from '../../store';
|
|
5
|
+
import DataTypeIcon from '../../components/dataTypeIcon';
|
|
6
|
+
import { FieldPill } from './fieldPill';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
provided: DroppableProvided;
|
|
10
|
+
}
|
|
11
|
+
const MeaFields: React.FC<Props> = props => {
|
|
12
|
+
const { provided } = props;
|
|
13
|
+
const { vizStore } = useGlobalStore();
|
|
14
|
+
const measures = vizStore.draggableFieldState.measures;
|
|
15
|
+
return <div {...provided.droppableProps} ref={provided.innerRef}>
|
|
16
|
+
{measures.map((f, index) => (
|
|
17
|
+
<Draggable key={f.dragId} draggableId={f.dragId} index={index}>
|
|
18
|
+
{(provided, snapshot) => {
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<FieldPill
|
|
22
|
+
className="pt-0.5 pb-0.5 pl-2 pr-2 m-1 text-xs hover:bg-blue-100 rounded-full truncate"
|
|
23
|
+
isDragging={snapshot.isDragging}
|
|
24
|
+
ref={provided.innerRef}
|
|
25
|
+
{...provided.draggableProps}
|
|
26
|
+
{...provided.dragHandleProps}
|
|
27
|
+
>
|
|
28
|
+
<DataTypeIcon dataType={f.semanticType} analyticType={f.analyticType} /> {f.name}
|
|
29
|
+
{
|
|
30
|
+
f.fid && !snapshot.isDragging && <select className="bg-transparent text-gray-700 float-right focus:outline-none focus:border-gray-500" value="" onChange={e => {
|
|
31
|
+
if (e.target.value === 'bin') {
|
|
32
|
+
vizStore.createBinField('measures', index)
|
|
33
|
+
} else if (e.target.value === 'log10') {
|
|
34
|
+
vizStore.createLogField('measures', index)
|
|
35
|
+
}
|
|
36
|
+
}}>
|
|
37
|
+
<option value=""></option>
|
|
38
|
+
<option value="bin">bin</option>
|
|
39
|
+
<option value="log10">log10</option>
|
|
40
|
+
</select>
|
|
41
|
+
}
|
|
42
|
+
</FieldPill>
|
|
43
|
+
{
|
|
44
|
+
<FieldPill className={`pt-0.5 pb-0.5 pl-2 pr-2 m-1 text-xs hover:bg-blue-100 rounded-full border-blue-400 border truncate ${snapshot.isDragging ? '' : 'hidden'}`}
|
|
45
|
+
isDragging={snapshot.isDragging}
|
|
46
|
+
>
|
|
47
|
+
<DataTypeIcon dataType={f.semanticType} analyticType={f.analyticType} /> {f.name}
|
|
48
|
+
</FieldPill>
|
|
49
|
+
}
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}}
|
|
53
|
+
</Draggable>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default observer(MeaFields);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { DraggableFieldState, IDraggableStateKey } from '../interfaces';
|
|
3
|
+
import {
|
|
4
|
+
DragDropContext,
|
|
5
|
+
DropResult,
|
|
6
|
+
ResponderProvided,
|
|
7
|
+
DraggableLocation,
|
|
8
|
+
} from "react-beautiful-dnd";
|
|
9
|
+
import { useGlobalStore } from '../store';
|
|
10
|
+
window['__react-beautiful-dnd-disable-dev-warnings'] = true;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export const FieldsContextWrapper: React.FC = props => {
|
|
14
|
+
const { vizStore } = useGlobalStore();
|
|
15
|
+
const onDragEnd = useCallback((result: DropResult, provided: ResponderProvided) => {
|
|
16
|
+
if (!result.destination) {
|
|
17
|
+
vizStore.removeField(result.source.droppableId as keyof DraggableFieldState, result.source.index)
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const destination = result.destination as DraggableLocation;
|
|
21
|
+
if (destination.droppableId === result.source.droppableId) {
|
|
22
|
+
if (destination.index === result.source.index) return;
|
|
23
|
+
vizStore.reorderField(destination.droppableId as keyof DraggableFieldState, result.source.index, destination.index);
|
|
24
|
+
} else {
|
|
25
|
+
let sourceKey = result.source
|
|
26
|
+
.droppableId as keyof DraggableFieldState;
|
|
27
|
+
let targetKey = destination
|
|
28
|
+
.droppableId as keyof DraggableFieldState;
|
|
29
|
+
vizStore.moveField(sourceKey, result.source.index, targetKey, destination.index)
|
|
30
|
+
}
|
|
31
|
+
}, [])
|
|
32
|
+
return <DragDropContext onDragEnd={onDragEnd}
|
|
33
|
+
onDragStart={() => {}}
|
|
34
|
+
onDragUpdate={() => {}}
|
|
35
|
+
>
|
|
36
|
+
{ props.children }
|
|
37
|
+
</DragDropContext>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default FieldsContextWrapper;
|
|
41
|
+
|
|
42
|
+
export const DRAGGABLE_STATE_KEYS: Readonly<IDraggableStateKey[]> = [
|
|
43
|
+
{ id: 'fields', mode: 0 },
|
|
44
|
+
{ id: 'columns', mode: 0 },
|
|
45
|
+
{ id: 'rows', mode: 0 },
|
|
46
|
+
{ id: 'color', mode: 1 },
|
|
47
|
+
{ id: 'opacity', mode: 1 },
|
|
48
|
+
{ id: 'size', mode: 1 },
|
|
49
|
+
{ id: 'shape', mode: 1},
|
|
50
|
+
{ id: 'theta', mode: 1 },
|
|
51
|
+
{ id: 'radius', mode: 1 },
|
|
52
|
+
{ id: 'filters', mode: 1 },
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
export const AGGREGATOR_LIST: Readonly<string[]> = [
|
|
56
|
+
'sum',
|
|
57
|
+
'mean',
|
|
58
|
+
'count',
|
|
59
|
+
] as const;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { CheckCircleIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import { observer } from 'mobx-react-lite';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
import Modal from '../../components/modal';
|
|
7
|
+
import type { IFilterField, IFilterRule } from '../../interfaces';
|
|
8
|
+
import { useGlobalStore } from '../../store';
|
|
9
|
+
import Tabs, { RuleFormProps } from './tabs';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
const QuantitativeRuleForm: React.FC<RuleFormProps> = ({
|
|
13
|
+
field,
|
|
14
|
+
onChange,
|
|
15
|
+
}) => {
|
|
16
|
+
return (
|
|
17
|
+
<Tabs
|
|
18
|
+
field={field}
|
|
19
|
+
onChange={onChange}
|
|
20
|
+
tabs={['range', 'one of']}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const NominalRuleForm: React.FC<RuleFormProps> = ({
|
|
26
|
+
field,
|
|
27
|
+
onChange,
|
|
28
|
+
}) => {
|
|
29
|
+
return (
|
|
30
|
+
<Tabs
|
|
31
|
+
field={field}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
tabs={['one of']}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const OrdinalRuleForm: React.FC<RuleFormProps> = ({
|
|
39
|
+
field,
|
|
40
|
+
onChange,
|
|
41
|
+
}) => {
|
|
42
|
+
return (
|
|
43
|
+
<Tabs
|
|
44
|
+
field={field}
|
|
45
|
+
onChange={onChange}
|
|
46
|
+
tabs={['range', 'one of']}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const TemporalRuleForm: React.FC<RuleFormProps> = ({
|
|
52
|
+
field,
|
|
53
|
+
onChange,
|
|
54
|
+
}) => {
|
|
55
|
+
return (
|
|
56
|
+
<Tabs
|
|
57
|
+
field={field}
|
|
58
|
+
onChange={onChange}
|
|
59
|
+
tabs={['one of', 'temporal range']}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const EmptyForm: React.FC<RuleFormProps> = () => <React.Fragment />;
|
|
65
|
+
|
|
66
|
+
const FilterEditDialog: React.FC = observer(() => {
|
|
67
|
+
const { vizStore } = useGlobalStore();
|
|
68
|
+
const { editingFilterIdx, draggableFieldState } = vizStore;
|
|
69
|
+
|
|
70
|
+
const { t } = useTranslation('translation', { keyPrefix: 'filters' });
|
|
71
|
+
|
|
72
|
+
const field = React.useMemo(() => {
|
|
73
|
+
return editingFilterIdx !== null ? draggableFieldState.filters[editingFilterIdx] : null;
|
|
74
|
+
}, [editingFilterIdx, draggableFieldState]);
|
|
75
|
+
|
|
76
|
+
const [uncontrolledField, setUncontrolledField] = React.useState(field as IFilterField | null);
|
|
77
|
+
const ufRef = React.useRef(uncontrolledField);
|
|
78
|
+
ufRef.current = uncontrolledField;
|
|
79
|
+
|
|
80
|
+
React.useEffect(() => {
|
|
81
|
+
if (field !== ufRef.current) {
|
|
82
|
+
setUncontrolledField(field);
|
|
83
|
+
}
|
|
84
|
+
}, [field]);
|
|
85
|
+
|
|
86
|
+
const handleChange = React.useCallback((r: IFilterRule) => {
|
|
87
|
+
if (editingFilterIdx !== null) {
|
|
88
|
+
setUncontrolledField(uf => ({
|
|
89
|
+
...uf,
|
|
90
|
+
rule: r,
|
|
91
|
+
}) as IFilterField);
|
|
92
|
+
}
|
|
93
|
+
}, [editingFilterIdx]);
|
|
94
|
+
|
|
95
|
+
const handleSubmit = React.useCallback(() => {
|
|
96
|
+
if (editingFilterIdx !== null) {
|
|
97
|
+
vizStore.writeFilter(editingFilterIdx, uncontrolledField?.rule ?? null);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
vizStore.closeFilterEditing();
|
|
101
|
+
}, [editingFilterIdx, uncontrolledField]);
|
|
102
|
+
|
|
103
|
+
const Form = field ? ({
|
|
104
|
+
quantitative: QuantitativeRuleForm,
|
|
105
|
+
nominal: NominalRuleForm,
|
|
106
|
+
ordinal: OrdinalRuleForm,
|
|
107
|
+
temporal: TemporalRuleForm,
|
|
108
|
+
}[field.semanticType] as React.FC<RuleFormProps>) : EmptyForm;
|
|
109
|
+
|
|
110
|
+
return uncontrolledField ? (
|
|
111
|
+
<Modal
|
|
112
|
+
title={t('editing')}
|
|
113
|
+
onClose={() => vizStore.closeFilterEditing()}
|
|
114
|
+
>
|
|
115
|
+
<header className="text-lg font-semibold py-2 outline-none">
|
|
116
|
+
{t('form.name')}
|
|
117
|
+
</header>
|
|
118
|
+
<input className="border py-1 px-4" readOnly value={uncontrolledField.name}/>
|
|
119
|
+
<header className="text-lg font-semibold py-2 outline-none">
|
|
120
|
+
{t('form.rule')}
|
|
121
|
+
</header>
|
|
122
|
+
<Form
|
|
123
|
+
field={uncontrolledField}
|
|
124
|
+
onChange={handleChange}
|
|
125
|
+
/>
|
|
126
|
+
<div className="flex justify-center text-green-500 mt-4">
|
|
127
|
+
<CheckCircleIcon
|
|
128
|
+
width="3em"
|
|
129
|
+
height="3em"
|
|
130
|
+
role="button"
|
|
131
|
+
tabIndex={0}
|
|
132
|
+
aria-label="ok"
|
|
133
|
+
className="cursor-pointer hover:bg-green-50 p-1"
|
|
134
|
+
onClick={handleSubmit}
|
|
135
|
+
strokeWidth="1.5"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
</Modal>
|
|
139
|
+
) : null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
export default FilterEditDialog;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { observer } from 'mobx-react-lite';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { DraggableProvided } from "react-beautiful-dnd";
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import styled from 'styled-components';
|
|
6
|
+
import { PencilSquareIcon } from '@heroicons/react/24/solid';
|
|
7
|
+
import { useGlobalStore } from '../../store';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
interface FilterPillProps {
|
|
11
|
+
provided: DraggableProvided;
|
|
12
|
+
fIndex: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Pill = styled.div({
|
|
16
|
+
userSelect: 'none',
|
|
17
|
+
alignItems: 'stretch',
|
|
18
|
+
borderStyle: 'solid',
|
|
19
|
+
borderWidth: '1px',
|
|
20
|
+
boxSizing: 'border-box',
|
|
21
|
+
cursor: 'default',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
flexDirection: 'column',
|
|
24
|
+
fontSize: '12px',
|
|
25
|
+
minWidth: '150px',
|
|
26
|
+
overflowY: 'hidden',
|
|
27
|
+
padding: 0,
|
|
28
|
+
|
|
29
|
+
'> *': {
|
|
30
|
+
flexGrow: 1,
|
|
31
|
+
paddingBlock: '0.2em',
|
|
32
|
+
paddingInline: '0.5em',
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
'> header': {
|
|
36
|
+
height: '20px',
|
|
37
|
+
borderBottomWidth: '1px',
|
|
38
|
+
},
|
|
39
|
+
'> div.output': {
|
|
40
|
+
minHeight: '20px',
|
|
41
|
+
|
|
42
|
+
'> span': {
|
|
43
|
+
overflowY: 'hidden',
|
|
44
|
+
maxHeight: '4em',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
'> .output .icon': {
|
|
49
|
+
display: 'none',
|
|
50
|
+
},
|
|
51
|
+
'> .output:hover .icon': {
|
|
52
|
+
display: 'unset',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const FilterPill: React.FC<FilterPillProps> = observer(props => {
|
|
57
|
+
const { provided, fIndex } = props;
|
|
58
|
+
const { vizStore } = useGlobalStore();
|
|
59
|
+
const { draggableFieldState } = vizStore;
|
|
60
|
+
|
|
61
|
+
const field = draggableFieldState.filters[fIndex];
|
|
62
|
+
|
|
63
|
+
const { t } = useTranslation('translation', { keyPrefix: 'filters' });
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Pill
|
|
67
|
+
className="text-gray-900"
|
|
68
|
+
ref={provided.innerRef}
|
|
69
|
+
{...provided.draggableProps}
|
|
70
|
+
{...provided.dragHandleProps}
|
|
71
|
+
>
|
|
72
|
+
<header className="bg-indigo-50">
|
|
73
|
+
{field.name}
|
|
74
|
+
</header>
|
|
75
|
+
<div
|
|
76
|
+
className="bg-white text-gray-500 hover:bg-gray-100 flex flex-row output"
|
|
77
|
+
onClick={() => vizStore.setFilterEditing(fIndex)}
|
|
78
|
+
style={{ cursor: 'pointer' }}
|
|
79
|
+
title={t('to_edit')}
|
|
80
|
+
>
|
|
81
|
+
{
|
|
82
|
+
field.rule ? (
|
|
83
|
+
<span className="flex-1">
|
|
84
|
+
{
|
|
85
|
+
field.rule.type === 'one of' ? (
|
|
86
|
+
`oneOf: [${[...field.rule.value].map(d => JSON.stringify(d)).join(', ')}]`
|
|
87
|
+
) : field.rule.type === 'range' ? (
|
|
88
|
+
`range: [${field.rule.value[0]}, ${field.rule.value[1]}]`
|
|
89
|
+
) : field.rule.type === 'temporal range' ? (
|
|
90
|
+
`range: [${new Date(field.rule.value[0])}, ${new Date(field.rule.value[1])}]`
|
|
91
|
+
) : null
|
|
92
|
+
}
|
|
93
|
+
</span>
|
|
94
|
+
) : (
|
|
95
|
+
<span className="text-gray-600 flex-1">
|
|
96
|
+
{t('empty_rule')}
|
|
97
|
+
</span>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
<PencilSquareIcon
|
|
101
|
+
className="icon flex-grow-0 flex-shrink-0 pointer-events-none text-gray-500"
|
|
102
|
+
role="presentation"
|
|
103
|
+
aria-hidden
|
|
104
|
+
width="1.4em"
|
|
105
|
+
height="1.4em"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</Pill>
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
export default FilterPill;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { observer } from 'mobx-react-lite';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Draggable,
|
|
5
|
+
Droppable, DroppableProvided,
|
|
6
|
+
} from "react-beautiful-dnd";
|
|
7
|
+
|
|
8
|
+
import { useGlobalStore } from '../../store';
|
|
9
|
+
import { FilterFieldContainer, FilterFieldsContainer } from '../components';
|
|
10
|
+
import FilterPill from './filterPill';
|
|
11
|
+
import FilterEditDialog from './filterEditDialog';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
interface FieldContainerProps {
|
|
15
|
+
provided: DroppableProvided;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FilterItemContainer: React.FC<FieldContainerProps> = observer(({ provided }) => {
|
|
19
|
+
const { vizStore } = useGlobalStore();
|
|
20
|
+
const { draggableFieldState: { filters } } = vizStore;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<FilterFieldsContainer
|
|
24
|
+
{...provided.droppableProps}
|
|
25
|
+
ref={provided.innerRef}
|
|
26
|
+
>
|
|
27
|
+
{filters.map((f, index) => (
|
|
28
|
+
<Draggable key={f.dragId} draggableId={f.dragId} index={index}>
|
|
29
|
+
{(provided, snapshot) => {
|
|
30
|
+
return (
|
|
31
|
+
<FilterPill
|
|
32
|
+
fIndex={index}
|
|
33
|
+
provided={provided}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}}
|
|
37
|
+
</Draggable>
|
|
38
|
+
))}
|
|
39
|
+
{provided.placeholder}
|
|
40
|
+
<FilterEditDialog />
|
|
41
|
+
</FilterFieldsContainer>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const FilterField: React.FC = () => {
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<FilterFieldContainer>
|
|
49
|
+
<Droppable droppableId="filters" direction="vertical">
|
|
50
|
+
{(provided, snapshot) => (
|
|
51
|
+
<FilterItemContainer
|
|
52
|
+
provided={provided}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
</Droppable>
|
|
56
|
+
</FilterFieldContainer>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default FilterField;
|