@kanaries/graphic-walker 0.3.15 → 0.3.16

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.
@@ -0,0 +1 @@
1
+ export declare function useDebounceValue<T>(value: T, timeout?: number): T;
@@ -6,6 +6,8 @@ interface UseRendererProps {
6
6
  viewMeasures: IViewField[];
7
7
  filters: readonly DeepReadonly<IFilterField>[];
8
8
  defaultAggregated: boolean;
9
+ sort: 'none' | 'ascending' | 'descending';
10
+ limit: number;
9
11
  }
10
12
  interface UseRendererResult {
11
13
  viewData: IRow[];
@@ -7,3 +7,4 @@ export interface IVisSpace {
7
7
  export declare const applyFilter: (data: IRow[], filters: readonly IFilterField[]) => Promise<IRow[]>;
8
8
  export declare const transformDataService: (data: IRow[], columns: IField[]) => Promise<IRow[]>;
9
9
  export declare const applyViewQuery: (data: IRow[], metas: IField[], query: IViewQuery) => Promise<IRow[]>;
10
+ export declare const applySort: (data: IRow[], viewMeasures: IField[], sort: 'ascending' | 'descending') => Promise<IRow[]>;
@@ -542,5 +542,7 @@ export declare class VizSpecStore {
542
542
  importRaw(raw: string): void;
543
543
  private visSpecEncoder;
544
544
  private visSpecDecoder;
545
+ limit: number;
546
+ get sort(): "none" | "ascending" | "descending";
545
547
  }
546
548
  export {};
@@ -0,0 +1,2 @@
1
+ import { IViewField, IRow } from '../interfaces';
2
+ export declare function sortBy(data: IRow[], viewMeasures: IViewField[], sort: 'ascending' | 'descending'): IRow[];
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanaries/graphic-walker",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "scripts": {
5
5
  "dev:front_end": "vite --host",
6
6
  "dev": "npm run dev:front_end",
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+
4
+ export default function LimitSetting(props: { value: number; setValue: (v: number) => void }) {
5
+ const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' });
6
+
7
+ return (
8
+ <div className=" mt-2">
9
+ <input
10
+ className="w-full h-2 bg-blue-100 appearance-none"
11
+ type="range"
12
+ name="limit"
13
+ value={props.value > 0 ? props.value : 0}
14
+ min="1"
15
+ max="50"
16
+ disabled={props.value < 0}
17
+ step="1"
18
+ onChange={(e) => {
19
+ const v = parseInt(e.target.value);
20
+ if (!isNaN(v)) {
21
+ props.setValue(v);
22
+ }
23
+ }}
24
+ />
25
+ <output className="text-sm ml-1" htmlFor="height">
26
+ <input
27
+ type="checkbox"
28
+ className="h-4 w-4 mr-1 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
29
+ checked={props.value > 0}
30
+ onChange={(e) => {
31
+ props.setValue(e.target.checked ? 30 : -1);
32
+ }}
33
+ ></input>
34
+ {`${t('limit')}${props.value > 0 ? `: ${props.value}` : ''}`}
35
+ </output>
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,10 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ export function useDebounceValue<T>(value: T, timeout = 200): T {
4
+ const [innerValue, setInnerValue] = useState(value);
5
+ useEffect(() => {
6
+ const handler = setTimeout(() => setInnerValue(value), timeout);
7
+ return () => clearTimeout(handler);
8
+ }, [value]);
9
+ return innerValue;
10
+ }
@@ -154,6 +154,7 @@
154
154
  "export_chart_as": "Export as {{type}}",
155
155
  "export_code": "Export Code"
156
156
  },
157
+ "limit": "Limit",
157
158
  "size": "Resize",
158
159
  "size_setting": {
159
160
  "width": "Width",
@@ -153,6 +153,7 @@
153
153
  "export_chart_as": "{{type}}としてエクスポート",
154
154
  "export_code": "コードをエクスポート"
155
155
  },
156
+ "limit": "上限",
156
157
  "size": "サイズ変更",
157
158
  "size_setting": {
158
159
  "width": "幅",
@@ -154,6 +154,7 @@
154
154
  "export_chart_as": "导出 {{type}}",
155
155
  "export_code": "导出代码"
156
156
  },
157
+ "limit": "上限",
157
158
  "size": "调整尺寸",
158
159
  "size_setting": {
159
160
  "width": "宽度",
@@ -1,10 +1,10 @@
1
1
  import { useState, useEffect, useMemo, useRef } from 'react';
2
2
  import { unstable_batchedUpdates } from 'react-dom';
3
3
  import type { DeepReadonly, IFilterField, IRow, IViewField } from '../interfaces';
4
- import { applyFilter, applyViewQuery, transformDataService } from '../services';
4
+ import { applyFilter, applySort, applyViewQuery, transformDataService } from '../services';
5
5
  import { getMeaAggKey } from '../utils';
6
6
  import { useAppRootContext } from '../components/appRoot';
7
-
7
+ import { useDebounceValue } from '../hooks';
8
8
 
9
9
  interface UseRendererProps {
10
10
  data: IRow[];
@@ -13,6 +13,8 @@ interface UseRendererProps {
13
13
  viewMeasures: IViewField[];
14
14
  filters: readonly DeepReadonly<IFilterField>[];
15
15
  defaultAggregated: boolean;
16
+ sort: 'none' | 'ascending' | 'descending';
17
+ limit: number;
16
18
  }
17
19
 
18
20
  interface UseRendererResult {
@@ -21,10 +23,21 @@ interface UseRendererResult {
21
23
  }
22
24
 
23
25
  export const useRenderer = (props: UseRendererProps): UseRendererResult => {
24
- const { data, allFields, viewDimensions, viewMeasures, filters, defaultAggregated } = props;
26
+ const {
27
+ data,
28
+ allFields,
29
+ viewDimensions,
30
+ viewMeasures,
31
+ filters,
32
+ defaultAggregated,
33
+ sort,
34
+ limit: storeLimit,
35
+ } = props;
25
36
  const [computing, setComputing] = useState(false);
26
37
  const taskIdRef = useRef(0);
27
38
 
39
+ const limit = useDebounceValue(storeLimit);
40
+
28
41
  const [viewData, setViewData] = useState<IRow[]>([]);
29
42
 
30
43
  const appRef = useAppRootContext();
@@ -50,10 +63,26 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
50
63
  return applyViewQuery(d, dims.concat(meas), {
51
64
  op: defaultAggregated ? 'aggregate' : 'raw',
52
65
  groupBy: dims.map((f) => f.fid),
53
- measures: meas.map((f) => ({ field: f.fid, agg: f.aggName as any, asFieldKey: getMeaAggKey(f.fid, f.aggName!) })),
66
+ measures: meas.map((f) => ({
67
+ field: f.fid,
68
+ agg: f.aggName as any,
69
+ asFieldKey: getMeaAggKey(f.fid, f.aggName!),
70
+ })),
54
71
  });
55
72
  })
56
- .then(data => {
73
+ .then((data) => {
74
+ if (limit > 0 && sort !== 'none' && viewMeasures.length > 0) {
75
+ return applySort(data, viewMeasures, sort);
76
+ }
77
+ return data;
78
+ })
79
+ .then((data) => {
80
+ if (limit > 0) {
81
+ return data.slice(0, limit);
82
+ }
83
+ return data;
84
+ })
85
+ .then((data) => {
57
86
  if (taskId !== taskIdRef.current) {
58
87
  return;
59
88
  }
@@ -62,7 +91,8 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
62
91
  setComputing(false);
63
92
  setViewData(data);
64
93
  });
65
- }).catch((err) => {
94
+ })
95
+ .catch((err) => {
66
96
  if (taskId !== taskIdRef.current) {
67
97
  return;
68
98
  }
@@ -76,7 +106,7 @@ export const useRenderer = (props: UseRendererProps): UseRendererResult => {
76
106
  return () => {
77
107
  taskIdRef.current++;
78
108
  };
79
- }, [data, filters, viewDimensions, viewMeasures, defaultAggregated]);
109
+ }, [data, filters, viewDimensions, viewMeasures, defaultAggregated, limit]);
80
110
 
81
111
  return useMemo(() => {
82
112
  return {
@@ -21,7 +21,18 @@ interface RendererProps {
21
21
  const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, ref) {
22
22
  const { themeKey, dark } = props;
23
23
  const { vizStore, commonStore } = useGlobalStore();
24
- const { allFields, viewFilters, viewDimensions, viewMeasures, visualConfig, draggableFieldState, visList, visIndex } = vizStore;
24
+ const {
25
+ allFields,
26
+ viewFilters,
27
+ viewDimensions,
28
+ viewMeasures,
29
+ visualConfig,
30
+ draggableFieldState,
31
+ visList,
32
+ visIndex,
33
+ sort,
34
+ limit,
35
+ } = vizStore;
25
36
  const chart = visList[visIndex];
26
37
  const { currentDataset } = commonStore;
27
38
  const { dataSource } = currentDataset;
@@ -37,6 +48,8 @@ const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, r
37
48
  viewMeasures,
38
49
  filters: viewFilters,
39
50
  defaultAggregated: visualConfig.defaultAggregated,
51
+ sort,
52
+ limit: limit,
40
53
  });
41
54
 
42
55
  // Dependencies that should not trigger effect individually
@@ -64,19 +77,16 @@ const Renderer = forwardRef<IReactVegaHandler, RendererProps>(function (props, r
64
77
  useChartIndexControl({
65
78
  count: visList.length,
66
79
  index: visIndex,
67
- onChange: idx => vizStore.selectVisualization(idx),
80
+ onChange: (idx) => vizStore.selectVisualization(idx),
68
81
  });
69
82
 
70
- const handleGeomClick = useCallback(
71
- (values: any, e: any) => {
72
- e.stopPropagation();
73
- runInAction(() => {
74
- commonStore.showEmbededMenu([e.pageX, e.pageY]);
75
- commonStore.setFilters(values);
76
- });
77
- },
78
- []
79
- );
83
+ const handleGeomClick = useCallback((values: any, e: any) => {
84
+ e.stopPropagation();
85
+ runInAction(() => {
86
+ commonStore.showEmbededMenu([e.pageX, e.pageY]);
87
+ commonStore.setFilters(values);
88
+ });
89
+ }, []);
80
90
 
81
91
  const handleChartResize = useCallback(
82
92
  (width: number, height: number) => {
@@ -9,7 +9,6 @@ import type { IReactVegaHandler } from '../vis/react-vega';
9
9
  import SpecRenderer from './specRenderer';
10
10
  import { useRenderer } from './hooks';
11
11
 
12
-
13
12
  interface IPureRendererProps {
14
13
  name?: string;
15
14
  themeKey?: IThemeKey;
@@ -17,25 +16,20 @@ interface IPureRendererProps {
17
16
  rawData?: IRow[];
18
17
  visualState: DraggableFieldState;
19
18
  visualConfig: IVisualConfig;
19
+ sort?: 'none' | 'ascending' | 'descending';
20
+ limit?: number;
20
21
  }
21
22
 
22
23
  /**
23
24
  * Render a readonly chart with given visualization schema.
24
25
  * This is a pure component, which means it will not depend on any global state.
25
26
  */
26
- const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function PureRenderer (props, ref) {
27
- const {
28
- name,
29
- themeKey,
30
- dark,
31
- rawData,
32
- visualState,
33
- visualConfig,
34
- } = props;
27
+ const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function PureRenderer(props, ref) {
28
+ const { name, themeKey, dark, rawData, visualState, visualConfig, sort, limit } = props;
35
29
  const defaultAggregated = visualConfig?.defaultAggregated ?? false;
36
30
 
37
31
  const [viewData, setViewData] = useState<IRow[]>([]);
38
-
32
+
39
33
  const { allFields, viewDimensions, viewMeasures, filters } = useMemo(() => {
40
34
  const viewDimensions: IViewField[] = [];
41
35
  const viewMeasures: IViewField[] = [];
@@ -64,6 +58,8 @@ const PureRenderer = forwardRef<IReactVegaHandler, IPureRendererProps>(function
64
58
  viewMeasures,
65
59
  filters,
66
60
  defaultAggregated,
61
+ sort: sort ?? 'none',
62
+ limit: limit ?? -1,
67
63
  });
68
64
 
69
65
  // Dependencies that should not trigger effect individually
package/src/services.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { toJS } from 'mobx';
2
- import { IRow, IMutField, IField, IFilterField, Specification } from './interfaces';
2
+ import { IRow, IMutField, IField, IFilterField, Specification, IViewField } from './interfaces';
3
3
  /* eslint import/no-webpack-loader-syntax:0 */
4
4
  // @ts-ignore
5
5
  // eslint-disable-next-line
@@ -11,6 +11,8 @@ import { IRow, IMutField, IField, IFilterField, Specification } from './interfac
11
11
  import FilterWorker from './workers/filter.worker?worker&inline';
12
12
  import TransformDataWorker from './workers/transform.worker?worker&inline';
13
13
  import ViewQueryWorker from './workers/viewQuery.worker?worker&inline';
14
+ import SortWorker from './workers/sort.worker?worker&inline';
15
+
14
16
  import { IViewQuery } from './lib/viewQuery';
15
17
 
16
18
  // interface WorkerState {
@@ -154,7 +156,7 @@ export const transformDataService = async (data: IRow[], columns: IField[]): Pro
154
156
  } finally {
155
157
  worker.terminate();
156
158
  }
157
- }
159
+ };
158
160
 
159
161
  export const applyViewQuery = async (data: IRow[], metas: IField[], query: IViewQuery): Promise<IRow[]> => {
160
162
  const worker = new ViewQueryWorker();
@@ -170,4 +172,24 @@ export const applyViewQuery = async (data: IRow[], metas: IField[], query: IView
170
172
  } finally {
171
173
  worker.terminate();
172
174
  }
173
- }
175
+ };
176
+
177
+ export const applySort = async (
178
+ data: IRow[],
179
+ viewMeasures: IField[],
180
+ sort: 'ascending' | 'descending'
181
+ ): Promise<IRow[]> => {
182
+ const worker = new SortWorker();
183
+ try {
184
+ const res: IRow[] = await workerService(worker, {
185
+ data,
186
+ viewMeasures: viewMeasures.map((x) => toJS(x)),
187
+ sort,
188
+ });
189
+ return res;
190
+ } catch (err) {
191
+ throw new Error('Uncaught error in ViewQueryWorker', { cause: err });
192
+ } finally {
193
+ worker.terminate();
194
+ }
195
+ };
@@ -264,7 +264,7 @@ export class VizSpecStore {
264
264
  */
265
265
  public get viewDimensions(): IViewField[] {
266
266
  const { draggableFieldState } = this;
267
- const state = toJS(draggableFieldState);
267
+ const { filters, ...state } = toJS(draggableFieldState);
268
268
  const fields: IViewField[] = [];
269
269
  (Object.keys(state) as (keyof DraggableFieldState)[])
270
270
  .filter((dkey) => !MetaFieldKeys.includes(dkey))
@@ -278,7 +278,7 @@ export class VizSpecStore {
278
278
  */
279
279
  public get viewMeasures(): IViewField[] {
280
280
  const { draggableFieldState } = this;
281
- const state = toJS(draggableFieldState);
281
+ const { filters, ...state } = toJS(draggableFieldState);
282
282
  const fields: IViewField[] = [];
283
283
  (Object.keys(state) as (keyof DraggableFieldState)[])
284
284
  .filter((dkey) => !MetaFieldKeys.includes(dkey))
@@ -771,4 +771,16 @@ export class VizSpecStore {
771
771
  });
772
772
  return updatedVisList;
773
773
  }
774
+ public limit = -1;
775
+
776
+ public get sort() {
777
+ const { rows, columns } = this.draggableFieldState;
778
+ if (rows.length && !rows.find((x) => x.analyticType === 'measure')) {
779
+ return rows[rows.length - 1].sort || 'none';
780
+ }
781
+ if (columns.length && !columns.find((x) => x.analyticType === 'measure')) {
782
+ return columns[columns.length - 1].sort || 'none';
783
+ }
784
+ return 'none';
785
+ }
774
786
  }
@@ -19,6 +19,7 @@ import {
19
19
  LightBulbIcon,
20
20
  CodeBracketSquareIcon,
21
21
  Cog6ToothIcon,
22
+ HashtagIcon,
22
23
  } from '@heroicons/react/24/outline';
23
24
  import { observer } from 'mobx-react-lite';
24
25
  import React, { SVGProps, useCallback, useMemo } from 'react';
@@ -35,6 +36,8 @@ import { useCurrentMediaTheme } from '../utils/media';
35
36
  import throttle from '../utils/throttle';
36
37
  import KanariesLogo from '../assets/kanaries.png';
37
38
  import { ImageWithFallback } from '../components/timeoutImg';
39
+ import LimitSetting from '../components/limitSetting';
40
+ import { runInAction } from 'mobx';
38
41
 
39
42
  const Invisible = styled.div`
40
43
  clip: rect(1px, 1px, 1px, 1px);
@@ -73,7 +76,7 @@ const VisualSettings: React.FC<IVisualSettings> = ({
73
76
  exclude = [],
74
77
  }) => {
75
78
  const { vizStore, commonStore } = useGlobalStore();
76
- const { visualConfig, canUndo, canRedo } = vizStore;
79
+ const { visualConfig, canUndo, canRedo, limit } = vizStore;
77
80
  const { t: tGlobal } = useTranslation();
78
81
  const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' });
79
82
 
@@ -493,6 +496,24 @@ const VisualSettings: React.FC<IVisualSettings> = ({
493
496
  },
494
497
  ...(extra.length === 0 ? [] : ['-', ...extra]),
495
498
  '-',
499
+ {
500
+ key: 'limit_axis',
501
+ label: t('limit'),
502
+ icon: HashtagIcon,
503
+ form: (
504
+ <FormContainer>
505
+ <LimitSetting
506
+ value={limit}
507
+ setValue={(v) => {
508
+ runInAction(() => {
509
+ vizStore.limit = v;
510
+ });
511
+ }}
512
+ />
513
+ </FormContainer>
514
+ ),
515
+ },
516
+ '-',
496
517
  {
497
518
  key: 'kanaries',
498
519
  label: 'kanaries',
@@ -532,6 +553,7 @@ const VisualSettings: React.FC<IVisualSettings> = ({
532
553
  dark,
533
554
  extra,
534
555
  exclude,
556
+ limit,
535
557
  ]);
536
558
 
537
559
  return (
@@ -0,0 +1,23 @@
1
+ import { IViewField, IRow } from '../interfaces';
2
+ import { getMeaAggKey } from '../utils';
3
+
4
+ function compareMulti(a: number[], b: number[]): number {
5
+ if (a.length < b.length) return -compareMulti(b, a);
6
+ for (let i = 0; i < a.length; i++) {
7
+ if (!b[i]) return 1;
8
+ const c = a[i] - b[i];
9
+ if (c !== 0) return c;
10
+ }
11
+ return 0;
12
+ }
13
+
14
+ export function sortBy(data: IRow[], viewMeasures: IViewField[], sort: 'ascending' | 'descending') {
15
+ const sortM = sort === 'ascending' ? 1 : -1;
16
+ return data
17
+ .map((x) => ({
18
+ data: x,
19
+ value: viewMeasures.map((f) => x[getMeaAggKey(f.fid, f.aggName)]),
20
+ }))
21
+ .sort((a, b) => sortM * compareMulti(a.value, b.value))
22
+ .map((x) => x.data);
23
+ }
@@ -0,0 +1,21 @@
1
+ import { IRow, IViewField } from '../interfaces';
2
+ import { sortBy } from './sort';
3
+
4
+ const main = (e: {
5
+ data: {
6
+ data: IRow[];
7
+ viewMeasures: IViewField[];
8
+ sort: 'ascending' | 'descending';
9
+ };
10
+ }) => {
11
+ try {
12
+ const { data, viewMeasures, sort } = e.data;
13
+ const ans = sortBy(data, viewMeasures, sort);
14
+ self.postMessage(ans);
15
+ } catch (err: any) {
16
+ console.error(err.stack);
17
+ self.postMessage(err.stack);
18
+ }
19
+ };
20
+
21
+ self.addEventListener('message', main, false);