@kanaries/graphic-walker 0.3.11 → 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.
@@ -1,24 +1,72 @@
1
- import React, { createContext, forwardRef, useImperativeHandle, type ForwardedRef, useContext } from "react";
2
- import type { IGWHandler } from "../interfaces";
1
+ import React, { createContext, forwardRef, useImperativeHandle, type ForwardedRef, useContext, type ComponentType, type RefObject } from "react";
2
+ import type { IChartExportResult, IGWHandler, IGWHandlerInsider, IRenderStatus } from "../interfaces";
3
3
 
4
- const AppRootContext = createContext<ForwardedRef<IGWHandler>>(null!);
4
+ const AppRootContext = createContext<ForwardedRef<IGWHandlerInsider>>(null);
5
5
 
6
- export const useAppRootContext = () => {
7
- return useContext(AppRootContext);
6
+ export const useAppRootContext = (): RefObject<IGWHandlerInsider> => {
7
+ const context = useContext(AppRootContext);
8
+ if (context && 'current' in context) {
9
+ return context;
10
+ }
11
+ return {
12
+ current: null,
13
+ };
8
14
  };
9
15
 
10
- const AppRoot = forwardRef<IGWHandler, { children: any }>(({ children }, ref) => {
16
+ const AppRoot = forwardRef<IGWHandlerInsider, { children: any }>(({ children }, ref) => {
11
17
  useImperativeHandle(ref, () => {
18
+ let renderStatus: IRenderStatus = 'idle';
19
+ let onRenderStatusChangeHandlers: ((status: IRenderStatus) => void)[] = [];
20
+ const addRenderStatusChangeListener = (cb: typeof onRenderStatusChangeHandlers[number]): (() => void) => {
21
+ onRenderStatusChangeHandlers.push(cb);
22
+ const dispose = () => {
23
+ onRenderStatusChangeHandlers = onRenderStatusChangeHandlers.filter(which => which !== cb);
24
+ };
25
+ return dispose;
26
+ };
27
+ const updateRenderStatus = (status: IRenderStatus) => {
28
+ if (renderStatus === status) {
29
+ return;
30
+ }
31
+ renderStatus = status;
32
+ onRenderStatusChangeHandlers.forEach(cb => cb(renderStatus));
33
+ };
12
34
  return {
13
- exportChart: (async mode => {
35
+ get renderStatus() {
36
+ return renderStatus;
37
+ },
38
+ onRenderStatusChange: addRenderStatusChangeListener,
39
+ updateRenderStatus,
40
+ chartCount: 1,
41
+ chartIndex: 0,
42
+ openChart() {},
43
+ exportChart: (async (mode: IChartExportResult['mode'] = 'svg') => {
14
44
  return {
15
45
  mode,
16
46
  title: '',
17
47
  nCols: 0,
18
48
  nRows: 0,
19
49
  charts: [],
50
+ container: () => null,
20
51
  };
21
52
  }) as IGWHandler['exportChart'],
53
+ exportChartList: (async function * exportChartList (mode: IChartExportResult['mode'] = 'svg') {
54
+ yield {
55
+ mode,
56
+ total: 1,
57
+ completed: 0,
58
+ index: 0,
59
+ data: {
60
+ mode,
61
+ title: '',
62
+ nCols: 0,
63
+ nRows: 0,
64
+ charts: [],
65
+ container: () => null,
66
+ },
67
+ hasNext: false,
68
+ };
69
+ }) as IGWHandler['exportChartList'],
22
70
  };
23
71
  }, []);
24
72
 
@@ -29,4 +77,14 @@ const AppRoot = forwardRef<IGWHandler, { children: any }>(({ children }, ref) =>
29
77
  );
30
78
  });
31
79
 
80
+ export const withAppRoot = <P extends object>(Component: ComponentType<any>) => {
81
+ return (props: P) => {
82
+ return (
83
+ <AppRoot>
84
+ <Component {...props} />
85
+ </AppRoot>
86
+ );
87
+ };
88
+ };
89
+
32
90
  export default AppRoot;
@@ -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: "fixed",
10
- left: 0,
11
- top: 0,
12
- width: "100vw",
13
- height: "100vh",
14
- backdropFilter: "blur(1px)",
15
- zIndex: 25535,
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={["one of", "temporal range"]} />;
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="p-4">
86
- <h2 className="text-base font-semibold py-2 outline-none">{t("form.name")}</h2>
87
- <span className="inline-flex items-center rounded-full bg-indigo-100 px-3 py-0.5 text-sm font-medium text-indigo-800">
88
- {uncontrolledField.name}
89
- </span>
90
- <h3 className="text-base font-semibold py-2 outline-none">{t("form.rule")}</h3>
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: 'flex',
7
- flexDirection: 'column',
8
- alignItems: 'stretch',
9
- justifyContent: 'stretch',
10
- overflow: 'hidden',
11
- paddingBlock: '1em',
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
- const SliderElement = styled.div({
30
- marginInline: '1em',
31
- paddingBlock: '10px',
32
- flexGrow: 1,
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
- const SliderTrack = styled.div({
41
- flexGrow: 1,
42
- flexShrink: 1,
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
- const SliderThumb = styled.div({
51
- position: 'absolute',
52
- top: '50%',
53
- cursor: 'ew-resize',
54
- backgroundColor: '#e2e2e2',
55
- backgroundImage: `
56
- linear-gradient(#666, #666 4%, transparent 4%, transparent 96%, #666 95%),
57
- linear-gradient(90deg, #666, #666 10%, transparent 10%, transparent 90%, #666 90%)
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 SliderSlice = styled.div({
70
- backgroundColor: '#fff',
71
- position: 'absolute',
72
- height: '100%',
73
- borderRadius: '5px',
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
- ':hover': {
76
- backgroundColor: '#fff',
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
- React.useEffect(() => {
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={isDateTime ? `${new Date(value[0])}` : nicer([min, max], value[0])}
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: `${range[0] * 100}%`,
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={isDateTime ? `${new Date(value[1])}` : nicer([min, max], value[1])}
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: `${range[1] * 100}%`,
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
  });