@kanaries/graphic-walker 0.3.12 → 0.3.13
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/assets/transform.worker-a12fb3d8.js.map +1 -0
- package/dist/fields/filterField/slider.d.ts +0 -1
- package/dist/graphic-walker.es.js +24331 -24184
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +259 -127
- package/dist/graphic-walker.umd.js.map +1 -1
- package/package.json +2 -1
- package/src/assets/kanaries-logo.svg +1 -0
- package/src/components/modal.tsx +9 -9
- package/src/fields/filterField/filterEditDialog.tsx +33 -8
- package/src/fields/filterField/slider.tsx +127 -85
- package/src/fields/filterField/tabs.tsx +301 -184
- package/src/lib/execExp.ts +1 -1
- package/src/locales/en-US.json +10 -3
- package/src/locales/ja-JP.json +10 -3
- package/src/locales/zh-CN.json +10 -3
- package/src/visualSettings/index.tsx +299 -70
- package/dist/assets/transform.worker-90e4f506.js.map +0 -1
package/src/components/modal.tsx
CHANGED
|
@@ -5,15 +5,15 @@ import { Fragment, useState } from "react";
|
|
|
5
5
|
import { Dialog, Transition } from "@headlessui/react";
|
|
6
6
|
import { ExclamationTriangleIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
7
7
|
|
|
8
|
-
const Background = styled.div
|
|
9
|
-
position:
|
|
10
|
-
left: 0
|
|
11
|
-
top: 0
|
|
12
|
-
width:
|
|
13
|
-
height:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const Background = styled.div`
|
|
9
|
+
position: fixed;
|
|
10
|
+
left: 0;
|
|
11
|
+
top: 0;
|
|
12
|
+
width: 100vw;
|
|
13
|
+
height: 100vh;
|
|
14
|
+
backdrop-filter: blur(1px);
|
|
15
|
+
z-index: 25535;
|
|
16
|
+
`;
|
|
17
17
|
|
|
18
18
|
const Container = styled.div`
|
|
19
19
|
width: 98%;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { CheckCircleIcon } from "@heroicons/react/24/outline";
|
|
2
1
|
import { observer } from "mobx-react-lite";
|
|
3
2
|
import React from "react";
|
|
4
3
|
import { useTranslation } from "react-i18next";
|
|
@@ -9,6 +8,7 @@ import { useGlobalStore } from "../../store";
|
|
|
9
8
|
import Tabs, { RuleFormProps } from "./tabs";
|
|
10
9
|
import DefaultButton from "../../components/button/default";
|
|
11
10
|
import PrimaryButton from "../../components/button/primary";
|
|
11
|
+
import DropdownSelect from "../../components/dropdownSelect";
|
|
12
12
|
|
|
13
13
|
const QuantitativeRuleForm: React.FC<RuleFormProps> = ({ field, onChange }) => {
|
|
14
14
|
return <Tabs field={field} onChange={onChange} tabs={["range", "one of"]} />;
|
|
@@ -23,7 +23,7 @@ const OrdinalRuleForm: React.FC<RuleFormProps> = ({ field, onChange }) => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const TemporalRuleForm: React.FC<RuleFormProps> = ({ field, onChange }) => {
|
|
26
|
-
return <Tabs field={field} onChange={onChange} tabs={["
|
|
26
|
+
return <Tabs field={field} onChange={onChange} tabs={["temporal range", "one of"]} />;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const EmptyForm: React.FC<RuleFormProps> = () => <React.Fragment />;
|
|
@@ -71,6 +71,28 @@ const FilterEditDialog: React.FC = observer(() => {
|
|
|
71
71
|
vizStore.closeFilterEditing();
|
|
72
72
|
}, [editingFilterIdx, uncontrolledField]);
|
|
73
73
|
|
|
74
|
+
const allFieldOptions = React.useMemo(() => {
|
|
75
|
+
return [...draggableFieldState.dimensions, ...draggableFieldState.measures].map((d) => ({
|
|
76
|
+
label: d.name,
|
|
77
|
+
value: d.fid,
|
|
78
|
+
}));
|
|
79
|
+
}, [draggableFieldState]);
|
|
80
|
+
|
|
81
|
+
const handleSelectFilterField = (fieldKey) => {
|
|
82
|
+
const existingFilterIdx = draggableFieldState.filters.findIndex((field) => field.fid === fieldKey)
|
|
83
|
+
if (existingFilterIdx >= 0) {
|
|
84
|
+
vizStore.setFilterEditing(existingFilterIdx);
|
|
85
|
+
} else {
|
|
86
|
+
const sourceKey = draggableFieldState.dimensions.find((field) => field.fid === fieldKey)
|
|
87
|
+
? "dimensions"
|
|
88
|
+
: "measures"
|
|
89
|
+
const sourceIndex = sourceKey === "dimensions"
|
|
90
|
+
? draggableFieldState.dimensions.findIndex((field) => field.fid === fieldKey)
|
|
91
|
+
: draggableFieldState.measures.findIndex((field) => field.fid === fieldKey);
|
|
92
|
+
vizStore.moveField(sourceKey, sourceIndex, "filters", 0);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
74
96
|
const Form = field
|
|
75
97
|
? ({
|
|
76
98
|
quantitative: QuantitativeRuleForm,
|
|
@@ -82,12 +104,15 @@ const FilterEditDialog: React.FC = observer(() => {
|
|
|
82
104
|
|
|
83
105
|
return uncontrolledField ? (
|
|
84
106
|
<Modal show={Boolean(uncontrolledField)} title={t("editing")} onClose={() => vizStore.closeFilterEditing()}>
|
|
85
|
-
<div className="
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
<div className="px-4 py-1">
|
|
108
|
+
<div className="py-1">{t("form.name")}</div>
|
|
109
|
+
<DropdownSelect
|
|
110
|
+
buttonClassName="w-96"
|
|
111
|
+
className="mb-2"
|
|
112
|
+
options={allFieldOptions}
|
|
113
|
+
selectedKey={uncontrolledField.fid}
|
|
114
|
+
onSelect={handleSelectFilterField}
|
|
115
|
+
/>
|
|
91
116
|
<Form field={uncontrolledField} onChange={handleChange} />
|
|
92
117
|
<div className="mt-4">
|
|
93
118
|
<PrimaryButton
|
|
@@ -1,81 +1,75 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
3
|
import { filter, fromEvent, map, throttleTime } from 'rxjs';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
4
5
|
|
|
5
|
-
const SliderContainer = styled.div
|
|
6
|
-
display:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
overflow:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'> .output': {
|
|
14
|
-
display: 'flex',
|
|
15
|
-
flexDirection: 'row',
|
|
16
|
-
flexWrap: 'wrap',
|
|
17
|
-
alignItems: 'stretch',
|
|
18
|
-
justifyContent: 'space-between',
|
|
19
|
-
|
|
20
|
-
'> output': {
|
|
21
|
-
userSelect: 'none',
|
|
22
|
-
minWidth: '4em',
|
|
23
|
-
paddingInline: '0.5em',
|
|
24
|
-
textAlign: 'center',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
});
|
|
6
|
+
const SliderContainer = styled.div`
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: stretch;
|
|
10
|
+
justify-content: stretch;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
padding-block: 1em;
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
flexShrink: 1,
|
|
34
|
-
display: 'flex',
|
|
35
|
-
flexDirection: 'row',
|
|
36
|
-
alignItems: 'center',
|
|
37
|
-
justifyContent: 'stretch',
|
|
38
|
-
});
|
|
14
|
+
> .output {
|
|
15
|
+
display: flex;
|
|
16
|
+
justify-content: space-between;
|
|
17
|
+
margin-top: 1em;
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
backgroundColor: '#ccc',
|
|
44
|
-
border: '1px solid #aaa',
|
|
45
|
-
height: '10px',
|
|
46
|
-
borderRadius: '5px',
|
|
47
|
-
position: 'relative',
|
|
48
|
-
});
|
|
19
|
+
> output {
|
|
20
|
+
width: 100%;
|
|
21
|
+
}
|
|
49
22
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
width: '10px',
|
|
60
|
-
height: '20px',
|
|
61
|
-
borderRadius: '2px',
|
|
62
|
-
outline: 'none',
|
|
63
|
-
|
|
64
|
-
':hover': {
|
|
65
|
-
backgroundColor: '#fff',
|
|
66
|
-
},
|
|
67
|
-
});
|
|
23
|
+
> output:first-child {
|
|
24
|
+
margin-right: 0.5em;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
> output:last-child {
|
|
28
|
+
margin-left: 0.5em;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
68
32
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
33
|
+
const SliderElement = styled.div`
|
|
34
|
+
margin-inline: 0.5em;
|
|
35
|
+
padding: 1em;
|
|
36
|
+
flex-grow: 1;
|
|
37
|
+
flex-shrink: 1;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: row;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: stretch;
|
|
42
|
+
`;
|
|
74
43
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
44
|
+
const SliderTrack = styled.div`
|
|
45
|
+
flex-grow: 1;
|
|
46
|
+
flex-shrink: 1;
|
|
47
|
+
background-color: #ccc;
|
|
48
|
+
height: 5px;
|
|
49
|
+
border-radius: 3px;
|
|
50
|
+
position: relative;
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const SliderThumb = styled.div`
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 50%;
|
|
56
|
+
cursor: ew-resize;
|
|
57
|
+
background-color: #fff;
|
|
58
|
+
width: 2em;
|
|
59
|
+
height: 2em;
|
|
60
|
+
border-radius: 1em;
|
|
61
|
+
outline: none;
|
|
62
|
+
box-shadow: 0 4px 6px 2px rgba(0, 0, 0, 0.1);
|
|
63
|
+
|
|
64
|
+
&:hover {
|
|
65
|
+
background-color: #fff;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const SliderSlice = styled.div`
|
|
70
|
+
position: absolute;
|
|
71
|
+
height: 100%;
|
|
72
|
+
`;
|
|
79
73
|
|
|
80
74
|
|
|
81
75
|
const nicer = (range: readonly [number, number], value: number): string => {
|
|
@@ -84,12 +78,40 @@ const nicer = (range: readonly [number, number], value: number): string => {
|
|
|
84
78
|
return precision === undefined ? `${value}` : value.toFixed(precision).replace(/\.?0+$/, '');
|
|
85
79
|
};
|
|
86
80
|
|
|
81
|
+
interface ValueInputProps {
|
|
82
|
+
min: number;
|
|
83
|
+
max: number;
|
|
84
|
+
value: number;
|
|
85
|
+
resetValue: number;
|
|
86
|
+
onChange: (value: number) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ValueInput: React.FC<ValueInputProps> = props => {
|
|
90
|
+
const { min, max, value, resetValue, onChange } = props;
|
|
91
|
+
const handleSubmitValue = (value) => {
|
|
92
|
+
if (!isNaN(value) && value <= max && value >= min) {
|
|
93
|
+
onChange(value);
|
|
94
|
+
} else {
|
|
95
|
+
onChange(resetValue);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
<input
|
|
100
|
+
type="number"
|
|
101
|
+
min={min}
|
|
102
|
+
max={max}
|
|
103
|
+
className="block w-full rounded-md border-0 py-1 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
|
104
|
+
value={value}
|
|
105
|
+
onChange={(e) => handleSubmitValue(Number(e.target.value))}
|
|
106
|
+
/>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
interface SliderProps {
|
|
88
111
|
min: number;
|
|
89
112
|
max: number;
|
|
90
113
|
value: readonly [number, number];
|
|
91
114
|
onChange: (value: readonly [number, number]) => void;
|
|
92
|
-
isDateTime?: boolean;
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
@@ -97,7 +119,6 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
97
119
|
max,
|
|
98
120
|
value,
|
|
99
121
|
onChange,
|
|
100
|
-
isDateTime = false,
|
|
101
122
|
}) {
|
|
102
123
|
const [dragging, setDragging] = React.useState<'left' | 'right' | null>(null);
|
|
103
124
|
const trackRef = React.useRef<HTMLDivElement | null>(null);
|
|
@@ -110,7 +131,9 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
110
131
|
|
|
111
132
|
const mouseOffsetRef = React.useRef(0);
|
|
112
133
|
|
|
113
|
-
|
|
134
|
+
const { t } = useTranslation();
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
114
137
|
if (dragging) {
|
|
115
138
|
const stop = (ev?: MouseEvent) => {
|
|
116
139
|
setDragging(null);
|
|
@@ -166,14 +189,6 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
166
189
|
|
|
167
190
|
return (
|
|
168
191
|
<SliderContainer>
|
|
169
|
-
<div className="output">
|
|
170
|
-
<output htmlFor="slider:min">
|
|
171
|
-
{isDateTime ? `${new Date(value[0])}` : nicer([min, max], value[0])}
|
|
172
|
-
</output>
|
|
173
|
-
<output htmlFor="slider:max">
|
|
174
|
-
{isDateTime ? `${new Date(value[1])}` : nicer([min, max], value[1])}
|
|
175
|
-
</output>
|
|
176
|
-
</div>
|
|
177
192
|
<SliderElement>
|
|
178
193
|
<SliderTrack
|
|
179
194
|
ref={e => trackRef.current = e}
|
|
@@ -181,6 +196,7 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
181
196
|
<SliderSlice
|
|
182
197
|
role="presentation"
|
|
183
198
|
ref={e => sliceRef.current = e}
|
|
199
|
+
className="bg-indigo-600"
|
|
184
200
|
style={{
|
|
185
201
|
left: `${range[0] * 100}%`,
|
|
186
202
|
width: `${(range[1] - range[0]) * 100}%`,
|
|
@@ -193,7 +209,7 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
193
209
|
aria-valuemin={min}
|
|
194
210
|
aria-valuemax={max}
|
|
195
211
|
aria-valuenow={value[0]}
|
|
196
|
-
aria-valuetext={
|
|
212
|
+
aria-valuetext={nicer([min, max], value[0])}
|
|
197
213
|
tabIndex={-1}
|
|
198
214
|
onMouseDown={ev => {
|
|
199
215
|
if (ev.buttons === 1) {
|
|
@@ -202,7 +218,7 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
202
218
|
}
|
|
203
219
|
}}
|
|
204
220
|
style={{
|
|
205
|
-
left:
|
|
221
|
+
left: `calc(1em + ${range[0] * 100}%)`,
|
|
206
222
|
transform: 'translate(-100%, -50%)',
|
|
207
223
|
}}
|
|
208
224
|
/>
|
|
@@ -213,7 +229,7 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
213
229
|
aria-valuemin={min}
|
|
214
230
|
aria-valuemax={max}
|
|
215
231
|
aria-valuenow={value[1]}
|
|
216
|
-
aria-valuetext={
|
|
232
|
+
aria-valuetext={nicer([min, max], value[1])}
|
|
217
233
|
tabIndex={-1}
|
|
218
234
|
onMouseDown={ev => {
|
|
219
235
|
if (ev.buttons === 1) {
|
|
@@ -222,12 +238,38 @@ const Slider: React.FC<SliderProps> = React.memo(function Slider ({
|
|
|
222
238
|
}
|
|
223
239
|
}}
|
|
224
240
|
style={{
|
|
225
|
-
left:
|
|
241
|
+
left: `calc(${range[1] * 100}% - 1em)`,
|
|
226
242
|
transform: 'translate(0, -50%)',
|
|
227
243
|
}}
|
|
228
244
|
/>
|
|
229
245
|
</SliderTrack>
|
|
230
246
|
</SliderElement>
|
|
247
|
+
<div className="output">
|
|
248
|
+
<output htmlFor="slider:min">
|
|
249
|
+
<div className="my-1">{t('filters.range.start_value')}</div>
|
|
250
|
+
{
|
|
251
|
+
<ValueInput
|
|
252
|
+
min={min}
|
|
253
|
+
max={value[1]}
|
|
254
|
+
value={value[0]}
|
|
255
|
+
resetValue={min}
|
|
256
|
+
onChange={(newValue) => onChange([newValue, value[1]])}
|
|
257
|
+
/>
|
|
258
|
+
}
|
|
259
|
+
</output>
|
|
260
|
+
<output htmlFor="slider:max">
|
|
261
|
+
<div className="my-1">{t('filters.range.end_value')}</div>
|
|
262
|
+
{
|
|
263
|
+
<ValueInput
|
|
264
|
+
min={value[0]}
|
|
265
|
+
max={max}
|
|
266
|
+
value={value[1]}
|
|
267
|
+
resetValue={max}
|
|
268
|
+
onChange={(newValue) => onChange([value[0], newValue])}
|
|
269
|
+
/>
|
|
270
|
+
}
|
|
271
|
+
</output>
|
|
272
|
+
</div>
|
|
231
273
|
</SliderContainer>
|
|
232
274
|
);
|
|
233
275
|
});
|