@kanaries/graphic-walker 0.2.4 → 0.2.6

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.
Files changed (76) hide show
  1. package/dist/App.d.ts +1 -0
  2. package/dist/graphic-walker.es.js +1 -1
  3. package/dist/graphic-walker.es.js.map +1 -1
  4. package/dist/graphic-walker.umd.js +1 -1
  5. package/dist/graphic-walker.umd.js.map +1 -1
  6. package/package.json +3 -2
  7. package/src/App.tsx +141 -0
  8. package/src/assets/kanaries.ico +0 -0
  9. package/src/components/clickMenu.tsx +29 -0
  10. package/src/components/container.tsx +16 -0
  11. package/src/components/dataTypeIcon.tsx +20 -0
  12. package/src/components/liteForm.tsx +16 -0
  13. package/src/components/modal.tsx +85 -0
  14. package/src/components/sizeSetting.tsx +95 -0
  15. package/src/components/tabs/pureTab.tsx +70 -0
  16. package/src/config.ts +57 -0
  17. package/src/constants.ts +1 -0
  18. package/src/dataSource/config.ts +62 -0
  19. package/src/dataSource/dataSelection/csvData.tsx +77 -0
  20. package/src/dataSource/dataSelection/gwFile.tsx +38 -0
  21. package/src/dataSource/dataSelection/index.tsx +57 -0
  22. package/src/dataSource/dataSelection/publicData.tsx +57 -0
  23. package/src/dataSource/index.tsx +78 -0
  24. package/src/dataSource/pannel.tsx +71 -0
  25. package/src/dataSource/table.tsx +125 -0
  26. package/src/dataSource/utils.ts +47 -0
  27. package/src/fields/aestheticFields.tsx +23 -0
  28. package/src/fields/components.tsx +159 -0
  29. package/src/fields/datasetFields/dimFields.tsx +45 -0
  30. package/src/fields/datasetFields/fieldPill.tsx +10 -0
  31. package/src/fields/datasetFields/index.tsx +28 -0
  32. package/src/fields/datasetFields/meaFields.tsx +58 -0
  33. package/src/fields/fieldsContext.tsx +59 -0
  34. package/src/fields/filterField/filterEditDialog.tsx +143 -0
  35. package/src/fields/filterField/filterPill.tsx +113 -0
  36. package/src/fields/filterField/index.tsx +61 -0
  37. package/src/fields/filterField/slider.tsx +236 -0
  38. package/src/fields/filterField/tabs.tsx +421 -0
  39. package/src/fields/obComponents/obFContainer.tsx +40 -0
  40. package/src/fields/obComponents/obPill.tsx +48 -0
  41. package/src/fields/posFields/index.tsx +33 -0
  42. package/src/fields/select.tsx +31 -0
  43. package/src/fields/utils.ts +31 -0
  44. package/src/index.css +13 -0
  45. package/src/index.tsx +12 -0
  46. package/src/insightBoard/index.tsx +30 -0
  47. package/src/insightBoard/mainBoard.tsx +203 -0
  48. package/src/insightBoard/radioGroupButtons.tsx +50 -0
  49. package/src/insightBoard/selectionSpec.ts +113 -0
  50. package/src/insightBoard/std2vegaSpec.ts +184 -0
  51. package/src/insightBoard/utils.ts +32 -0
  52. package/src/insights.ts +408 -0
  53. package/src/interfaces.ts +154 -0
  54. package/src/locales/en-US.json +140 -0
  55. package/src/locales/i18n.ts +50 -0
  56. package/src/locales/zh-CN.json +140 -0
  57. package/src/main.tsx +10 -0
  58. package/src/models/visSpecHistory.ts +129 -0
  59. package/src/renderer/index.tsx +104 -0
  60. package/src/segments/visNav.tsx +48 -0
  61. package/src/services.ts +139 -0
  62. package/src/store/commonStore.ts +158 -0
  63. package/src/store/index.tsx +53 -0
  64. package/src/store/visualSpecStore.ts +586 -0
  65. package/src/utils/autoMark.ts +34 -0
  66. package/src/utils/index.ts +251 -0
  67. package/src/utils/normalization.ts +158 -0
  68. package/src/utils/save.ts +46 -0
  69. package/src/vis/future-react-vega.tsx +193 -0
  70. package/src/vis/gen-vega.tsx +52 -0
  71. package/src/vis/react-vega.tsx +398 -0
  72. package/src/visualSettings/index.tsx +252 -0
  73. package/src/visualSettings/menubar.tsx +109 -0
  74. package/src/vite-env.d.ts +1 -0
  75. package/src/workers/explainer.worker.js +78 -0
  76. 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}&nbsp;
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}&nbsp;
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}&nbsp;
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}&nbsp;
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;