@kanaries/graphic-walker 0.3.15 → 0.4.0
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/App.d.ts +9 -2
- package/dist/assets/filter.worker-f09fcd6f.js.map +1 -1
- package/dist/assets/sort.worker-f77540ac.js.map +1 -0
- package/dist/assets/transform.worker-bae8e910.js.map +1 -0
- package/dist/assets/{viewQuery.worker-03404216.js.map → viewQuery.worker-bdb6477c.js.map} +1 -1
- package/dist/components/askViz/index.d.ts +6 -0
- package/dist/components/askViz/schemaTransform.d.ts +2 -0
- package/dist/components/dataTable/index.d.ts +8 -5
- package/dist/components/limitSetting.d.ts +5 -0
- package/dist/components/pivotTable/store.d.ts +0 -2
- package/dist/components/spinner.d.ts +2 -0
- package/dist/computation/clientComputation.d.ts +3 -0
- package/dist/computation/serverComputation.d.ts +8 -0
- package/dist/config.d.ts +3 -1
- package/dist/fields/filterField/tabs.d.ts +2 -1
- package/dist/graphic-walker.es.js +16181 -15523
- package/dist/graphic-walker.es.js.map +1 -1
- package/dist/graphic-walker.umd.js +144 -144
- package/dist/graphic-walker.umd.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/interfaces.d.ts +93 -4
- package/dist/lib/execExp.d.ts +4 -4
- package/dist/lib/interfaces.d.ts +1 -0
- package/dist/lib/viewQuery.d.ts +2 -2
- package/dist/renderer/hooks.d.ts +10 -4
- package/dist/renderer/index.d.ts +2 -1
- package/dist/renderer/pureRenderer.d.ts +17 -1
- package/dist/renderer/specRenderer.d.ts +1 -0
- package/dist/services.d.ts +8 -4
- package/dist/store/commonStore.d.ts +2 -2
- package/dist/store/visualSpecStore.d.ts +58 -40
- package/dist/utils/save.d.ts +10 -2
- package/dist/utils/workflow.d.ts +3 -0
- package/dist/vis/react-vega.d.ts +3 -1
- package/dist/workers/sort.d.ts +2 -0
- package/dist/workers/sort.worker.d.ts +1 -0
- package/dist/workers/transform.d.ts +5 -2
- package/package.json +2 -2
- package/src/App.tsx +46 -7
- package/src/components/askViz/index.tsx +92 -0
- package/src/components/askViz/schemaTransform.ts +38 -0
- package/src/components/dataTable/index.tsx +51 -11
- package/src/components/limitSetting.tsx +38 -0
- package/src/components/pivotTable/index.tsx +0 -1
- package/src/components/pivotTable/store.tsx +0 -16
- package/src/components/spinner.tsx +14 -0
- package/src/components/toggle.tsx +2 -2
- package/src/components/visualConfig/index.tsx +78 -8
- package/src/computation/clientComputation.ts +55 -0
- package/src/computation/serverComputation.ts +153 -0
- package/src/config.ts +15 -2
- package/src/dataSource/datasetConfig/index.tsx +38 -6
- package/src/dataSource/table.tsx +11 -2
- package/src/fields/filterField/filterEditDialog.tsx +11 -10
- package/src/fields/filterField/tabs.tsx +178 -77
- package/src/hooks/index.ts +10 -0
- package/src/index.tsx +2 -0
- package/src/interfaces.ts +108 -5
- package/src/lib/execExp.ts +20 -11
- package/src/lib/interfaces.ts +1 -0
- package/src/lib/op/aggregate.ts +1 -1
- package/src/lib/viewQuery.ts +2 -2
- package/src/locales/en-US.json +12 -2
- package/src/locales/ja-JP.json +12 -2
- package/src/locales/zh-CN.json +12 -2
- package/src/main.tsx +1 -1
- package/src/renderer/hooks.ts +113 -49
- package/src/renderer/index.tsx +32 -18
- package/src/renderer/pureRenderer.tsx +44 -22
- package/src/renderer/specRenderer.tsx +24 -7
- package/src/services.ts +30 -9
- package/src/store/commonStore.ts +7 -7
- package/src/store/visualSpecStore.ts +300 -193
- package/src/utils/save.ts +81 -3
- package/src/utils/workflow.ts +148 -0
- package/src/vis/react-vega.tsx +21 -6
- package/src/vis/spec/aggregate.ts +3 -2
- package/src/vis/spec/stack.ts +7 -6
- package/src/visualSettings/index.tsx +22 -1
- package/src/workers/filter.worker.js +1 -1
- package/src/workers/sort.ts +22 -0
- package/src/workers/sort.worker.ts +21 -0
- package/src/workers/transform.ts +7 -8
- package/src/workers/transform.worker.js +2 -2
- package/src/workers/viewQuery.worker.js +2 -2
- package/dist/assets/transform.worker-a12fb3d8.js.map +0 -1
package/src/utils/save.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { IDataSet, IDataSource, IVisSpec, IVisSpecForExport } from "../interfaces";
|
|
1
|
+
import { DraggableFieldState, IDataSet, IDataSource, IVisSpec, IVisSpecForExport, IVisualConfig } from "../interfaces";
|
|
2
2
|
import { VisSpecWithHistory } from "../models/visSpecHistory";
|
|
3
|
+
import { toJS } from 'mobx';
|
|
4
|
+
import { GEMO_TYPES } from '../config';
|
|
3
5
|
|
|
4
6
|
export function dumpsGWPureSpec(list: VisSpecWithHistory[]): IVisSpec[] {
|
|
5
7
|
return list.map((l) => l.exportGW());
|
|
@@ -9,10 +11,86 @@ export function parseGWPureSpec(list: IVisSpec[]): VisSpecWithHistory[] {
|
|
|
9
11
|
return list.map((l) => new VisSpecWithHistory(l));
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
export function initVisualConfig(): IVisualConfig {
|
|
15
|
+
return {
|
|
16
|
+
defaultAggregated: true,
|
|
17
|
+
geoms: [GEMO_TYPES[0]!],
|
|
18
|
+
stack: 'stack',
|
|
19
|
+
showActions: false,
|
|
20
|
+
interactiveScale: false,
|
|
21
|
+
sorted: 'none',
|
|
22
|
+
zeroScale: true,
|
|
23
|
+
background: undefined,
|
|
24
|
+
size: {
|
|
25
|
+
mode: "auto",
|
|
26
|
+
width: 320,
|
|
27
|
+
height: 200,
|
|
28
|
+
},
|
|
29
|
+
format: {
|
|
30
|
+
numberFormat: undefined,
|
|
31
|
+
timeFormat: undefined,
|
|
32
|
+
normalizedNumberFormat: undefined,
|
|
33
|
+
},
|
|
34
|
+
resolve: {
|
|
35
|
+
x: false,
|
|
36
|
+
y: false,
|
|
37
|
+
color: false,
|
|
38
|
+
opacity: false,
|
|
39
|
+
shape: false,
|
|
40
|
+
size: false,
|
|
41
|
+
},
|
|
42
|
+
limit: -1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function visSpecDecoder(visList: IVisSpecForExport[]): IVisSpec[] {
|
|
47
|
+
const updatedVisList = visList.map((visSpec) => {
|
|
48
|
+
const updatedFilters = visSpec.encodings.filters.map((filter) => {
|
|
49
|
+
if (filter.rule?.type === 'one of' && Array.isArray(filter.rule.value)) {
|
|
50
|
+
return {
|
|
51
|
+
...filter,
|
|
52
|
+
rule: {
|
|
53
|
+
...filter.rule,
|
|
54
|
+
value: new Set(filter.rule.value),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return filter;
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
...visSpec,
|
|
62
|
+
encodings: {
|
|
63
|
+
...visSpec.encodings,
|
|
64
|
+
filters: updatedFilters,
|
|
65
|
+
},
|
|
66
|
+
} as IVisSpec;
|
|
67
|
+
});
|
|
68
|
+
return updatedVisList;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const forwardVisualConfigs = (backwards: ReturnType<typeof parseGWContent>['specList']): IVisSpecForExport[] => {
|
|
72
|
+
return backwards.map((content) => ({
|
|
73
|
+
...content,
|
|
74
|
+
config: {
|
|
75
|
+
...initVisualConfig(),
|
|
76
|
+
...content.config,
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export function resolveSpecFromStoInfo(info: IStoInfo) {
|
|
82
|
+
const spec = parseGWPureSpec(visSpecDecoder(forwardVisualConfigs(info.specList)))[0];
|
|
83
|
+
return {
|
|
84
|
+
config: toJS(spec.config) as IVisualConfig,
|
|
85
|
+
encodings: toJS(spec.encodings) as DraggableFieldState,
|
|
86
|
+
name: spec.name,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
12
90
|
export interface IStoInfo {
|
|
13
91
|
datasets: IDataSet[];
|
|
14
92
|
specList: {
|
|
15
|
-
[K in keyof IVisSpecForExport]: K extends
|
|
93
|
+
[K in keyof IVisSpecForExport]: K extends 'config' ? Partial<IVisSpecForExport[K]> : IVisSpecForExport[K];
|
|
16
94
|
}[];
|
|
17
95
|
dataSources: IDataSource[];
|
|
18
96
|
}
|
|
@@ -34,7 +112,7 @@ export function download(data: string, filename: string, type: string) {
|
|
|
34
112
|
window.navigator.msSaveOrOpenBlob(file, filename);
|
|
35
113
|
else {
|
|
36
114
|
// Others
|
|
37
|
-
var a = document.createElement(
|
|
115
|
+
var a = document.createElement('a'),
|
|
38
116
|
url = URL.createObjectURL(file);
|
|
39
117
|
a.href = url;
|
|
40
118
|
a.download = filename;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { IDataQueryWorkflowStep, IExpression, IFilterWorkflowStep, ITransformWorkflowStep, IViewField, IViewWorkflowStep, IVisFilter, ISortWorkflowStep } from "../interfaces";
|
|
2
|
+
import type { VizSpecStore } from "../store/visualSpecStore";
|
|
3
|
+
import { getMeaAggKey } from ".";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const walkExpression = (expression: IExpression, each: (field: string) => void): void => {
|
|
7
|
+
for (const param of expression.params) {
|
|
8
|
+
if (param.type === 'field') {
|
|
9
|
+
each(param.value);
|
|
10
|
+
} else if (param.type === 'expression') {
|
|
11
|
+
walkExpression(param.value, each);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const treeShake = (computedFields: readonly { key: string; expression: IExpression }[], viewKeys: readonly string[]): { key: string; expression: IExpression }[] => {
|
|
17
|
+
const usedFields = new Set(viewKeys);
|
|
18
|
+
const result = computedFields.filter(f => usedFields.has(f.key));
|
|
19
|
+
let currentFields = result.slice();
|
|
20
|
+
let rest = computedFields.filter(f => !usedFields.has(f.key));
|
|
21
|
+
while (currentFields.length && rest.length) {
|
|
22
|
+
const dependencies = new Set<string>();
|
|
23
|
+
for (const f of currentFields) {
|
|
24
|
+
walkExpression(f.expression, field => dependencies.add(field));
|
|
25
|
+
}
|
|
26
|
+
const nextFields = rest.filter(f => dependencies.has(f.key));
|
|
27
|
+
currentFields = nextFields;
|
|
28
|
+
rest = rest.filter(f => !dependencies.has(f.key));
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const toWorkflow = (
|
|
34
|
+
viewFilters: VizSpecStore['viewFilters'],
|
|
35
|
+
allFields: Omit<IViewField, 'dragId'>[],
|
|
36
|
+
viewDimensions: Omit<IViewField, 'dragId'>[],
|
|
37
|
+
viewMeasures: Omit<IViewField, 'dragId'>[],
|
|
38
|
+
defaultAggregated: VizSpecStore['visualConfig']['defaultAggregated'],
|
|
39
|
+
sort: 'none' | 'ascending' | 'descending',
|
|
40
|
+
limit?: number,
|
|
41
|
+
): IDataQueryWorkflowStep[] => {
|
|
42
|
+
const viewKeys = new Set<string>([...viewDimensions, ...viewMeasures].map(f => f.fid));
|
|
43
|
+
|
|
44
|
+
let filterWorkflow: IFilterWorkflowStep | null = null;
|
|
45
|
+
let transformWorkflow: ITransformWorkflowStep | null = null;
|
|
46
|
+
let viewQueryWorkflow: IViewWorkflowStep | null = null;
|
|
47
|
+
let sortWorkflow: ISortWorkflowStep | null = null;
|
|
48
|
+
|
|
49
|
+
// TODO: apply **fold** before filter
|
|
50
|
+
|
|
51
|
+
// First, to apply filters on the detailed data
|
|
52
|
+
const filters = viewFilters.filter(f => f.rule).map<IVisFilter>(f => {
|
|
53
|
+
viewKeys.add(f.fid);
|
|
54
|
+
const rule = f.rule!;
|
|
55
|
+
if (rule.type === 'one of') {
|
|
56
|
+
return {
|
|
57
|
+
fid: f.fid,
|
|
58
|
+
rule: {
|
|
59
|
+
type: 'one of',
|
|
60
|
+
value: [...rule.value],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
} else if (rule.type === 'temporal range') {
|
|
64
|
+
const range = [new Date(rule.value[0]).getTime(), new Date(rule.value[1]).getTime()] as const;
|
|
65
|
+
return {
|
|
66
|
+
fid: f.fid,
|
|
67
|
+
rule: {
|
|
68
|
+
type: 'temporal range',
|
|
69
|
+
value: range,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
const range = [Number(rule.value[0]), Number(rule.value[1])] as const;
|
|
74
|
+
return {
|
|
75
|
+
fid: f.fid,
|
|
76
|
+
rule: {
|
|
77
|
+
type: 'range',
|
|
78
|
+
value: range,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (filters.length) {
|
|
84
|
+
filterWorkflow = {
|
|
85
|
+
type: 'filter',
|
|
86
|
+
filters,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Second, to transform the data by rows 1 by 1
|
|
91
|
+
const computedFields = treeShake(allFields.filter(f => f.computed && f.expression).map(f => ({
|
|
92
|
+
key: f.fid,
|
|
93
|
+
expression: f.expression!,
|
|
94
|
+
})), [...viewKeys]);
|
|
95
|
+
if (computedFields.length) {
|
|
96
|
+
transformWorkflow = {
|
|
97
|
+
type: 'transform',
|
|
98
|
+
transform: computedFields,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Finally, to apply the aggregation
|
|
103
|
+
// When aggregation is enabled, there're 2 cases:
|
|
104
|
+
// 1. If any of the measures is aggregated, then we apply the aggregation
|
|
105
|
+
// 2. If there's no measure in the view, then we apply the aggregation
|
|
106
|
+
const aggregateOn = viewMeasures.filter(f => f.aggName).map(f => [f.fid, f.aggName as string]);
|
|
107
|
+
const aggergated = defaultAggregated && (aggregateOn.length || (viewMeasures.length === 0 && viewDimensions.length > 0));
|
|
108
|
+
if (aggergated) {
|
|
109
|
+
viewQueryWorkflow = {
|
|
110
|
+
type: 'view',
|
|
111
|
+
query: [{
|
|
112
|
+
op: 'aggregate',
|
|
113
|
+
groupBy: viewDimensions.map(f => f.fid),
|
|
114
|
+
measures: viewMeasures.map((f) => ({
|
|
115
|
+
field: f.fid,
|
|
116
|
+
agg: f.aggName as any,
|
|
117
|
+
asFieldKey: getMeaAggKey(f.fid, f.aggName!),
|
|
118
|
+
})),
|
|
119
|
+
}],
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
viewQueryWorkflow = {
|
|
123
|
+
type: 'view',
|
|
124
|
+
query: [{
|
|
125
|
+
op: 'raw',
|
|
126
|
+
fields: [...new Set([...viewDimensions, ...viewMeasures])].map(f => f.fid),
|
|
127
|
+
}],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (sort !== "none" && limit) {
|
|
132
|
+
sortWorkflow = {
|
|
133
|
+
type: 'sort',
|
|
134
|
+
by: viewMeasures.map(f => aggergated ? getMeaAggKey(f.fid, f.aggName) : f.fid),
|
|
135
|
+
sort,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
const steps: IDataQueryWorkflowStep[] = [
|
|
141
|
+
filterWorkflow!,
|
|
142
|
+
transformWorkflow!,
|
|
143
|
+
viewQueryWorkflow!,
|
|
144
|
+
sortWorkflow!,
|
|
145
|
+
].filter(Boolean);
|
|
146
|
+
|
|
147
|
+
return steps;
|
|
148
|
+
};
|
package/src/vis/react-vega.tsx
CHANGED
|
@@ -4,10 +4,9 @@ import { Subject, Subscription } from 'rxjs'
|
|
|
4
4
|
import * as op from 'rxjs/operators';
|
|
5
5
|
import type { ScenegraphEvent } from 'vega';
|
|
6
6
|
import styled from 'styled-components';
|
|
7
|
-
|
|
7
|
+
import { NonPositionChannelConfigList, PositionChannelConfigList } from '../config';
|
|
8
8
|
import { useVegaExportApi } from '../utils/vegaApiExport';
|
|
9
9
|
import { IViewField, IRow, IStackMode, VegaGlobalConfig, IVegaChartRef } from '../interfaces';
|
|
10
|
-
import { useTranslation } from 'react-i18next';
|
|
11
10
|
import { getVegaTimeFormatRules } from './temporalFormat';
|
|
12
11
|
import { getSingleView } from './spec/view';
|
|
13
12
|
import { NULL_FIELD } from './spec/field';
|
|
@@ -29,7 +28,7 @@ interface ReactVegaProps {
|
|
|
29
28
|
name?: string;
|
|
30
29
|
rows: Readonly<IViewField[]>;
|
|
31
30
|
columns: Readonly<IViewField[]>;
|
|
32
|
-
dataSource: IRow[];
|
|
31
|
+
dataSource: readonly IRow[];
|
|
33
32
|
defaultAggregate?: boolean;
|
|
34
33
|
stack: IStackMode;
|
|
35
34
|
interactiveScale: boolean;
|
|
@@ -48,6 +47,8 @@ interface ReactVegaProps {
|
|
|
48
47
|
height: number;
|
|
49
48
|
onGeomClick?: (values: any, e: any) => void
|
|
50
49
|
vegaConfig: VegaGlobalConfig;
|
|
50
|
+
/** @default "en-US" */
|
|
51
|
+
locale?: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const click$ = new Subject<ScenegraphEvent>();
|
|
@@ -100,9 +101,9 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
100
101
|
// dark = 'media',
|
|
101
102
|
vegaConfig,
|
|
102
103
|
// format
|
|
104
|
+
locale = 'en-US',
|
|
103
105
|
} = props;
|
|
104
106
|
const [viewPlaceholders, setViewPlaceholders] = useState<React.MutableRefObject<HTMLDivElement>[]>([]);
|
|
105
|
-
const { i18n } = useTranslation();
|
|
106
107
|
// const mediaTheme = useCurrentMediaTheme(dark);
|
|
107
108
|
// const themeConfig = builtInThemes[themeKey]?.[mediaTheme];
|
|
108
109
|
|
|
@@ -221,8 +222,22 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
221
222
|
spec.encoding = singleView.encoding;
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
spec.resolve ||= {};
|
|
226
|
+
// @ts-ignore
|
|
227
|
+
let resolve = vegaConfig.resolve;
|
|
228
|
+
for (let v in resolve) {
|
|
229
|
+
let value = resolve[v] ? 'independent' : 'shared';
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
spec.resolve.scale = { ...spec.resolve.scale, [v]: value };
|
|
232
|
+
if((PositionChannelConfigList as string[]).includes(v)) {
|
|
233
|
+
spec.resolve.axis = { ...spec.resolve.axis, [v]: value };
|
|
234
|
+
}else if((NonPositionChannelConfigList as string[]).includes(v)){
|
|
235
|
+
spec.resolve.legend = { ...spec.resolve.legend, [v]: value };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
224
239
|
if (viewPlaceholders.length > 0 && viewPlaceholders[0].current) {
|
|
225
|
-
const task = embed(viewPlaceholders[0].current, spec, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(
|
|
240
|
+
const task = embed(viewPlaceholders[0].current, spec, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(locale), config: vegaConfig }).then(res => {
|
|
226
241
|
const container = res.view.container();
|
|
227
242
|
const canvas = container?.querySelector('canvas') ?? null;
|
|
228
243
|
vegaRefs.current = [{
|
|
@@ -301,7 +316,7 @@ const ReactVega = forwardRef<IReactVegaHandler, ReactVegaProps>(function ReactVe
|
|
|
301
316
|
}
|
|
302
317
|
if (node) {
|
|
303
318
|
const id = index;
|
|
304
|
-
const task = embed(node, ans, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(
|
|
319
|
+
const task = embed(node, ans, { mode: 'vega-lite', actions: showActions, timeFormatLocale: getVegaTimeFormatRules(locale), config: vegaConfig }).then(res => {
|
|
305
320
|
const container = res.view.container();
|
|
306
321
|
const canvas = container?.querySelector('canvas') ?? null;
|
|
307
322
|
vegaRefs.current[id] = {
|
|
@@ -4,11 +4,12 @@ import { getMeaAggKey } from '../../utils';
|
|
|
4
4
|
|
|
5
5
|
export function channelAggregate(encoding: { [key: string]: any }, fields: IViewField[]) {
|
|
6
6
|
Object.values(encoding).forEach((c) => {
|
|
7
|
-
|
|
7
|
+
if (c.aggregate === null) return;
|
|
8
|
+
const targetField = fields.find((f) => f.fid === c.field && (f.analyticType === 'measure' || f.fid === COUNT_FIELD_ID));
|
|
8
9
|
if (targetField && targetField.fid === COUNT_FIELD_ID) {
|
|
9
10
|
c.title = 'Count';
|
|
10
11
|
c.field = getMeaAggKey(targetField.fid, targetField.aggName)
|
|
11
|
-
} else if (targetField
|
|
12
|
+
} else if (targetField) {
|
|
12
13
|
c.title = `${targetField.aggName}(${targetField.name})`;
|
|
13
14
|
c.field = getMeaAggKey(targetField.fid, targetField.aggName)
|
|
14
15
|
}
|
package/src/vis/spec/stack.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { IStackMode } from "../../interfaces";
|
|
2
2
|
|
|
3
3
|
export function channelStack(encoding: { [key: string]: any }, stackMode: IStackMode) {
|
|
4
|
-
if (stackMode === 'stack') return;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
if (stackMode === 'stack' || stackMode === 'zero') return;
|
|
5
|
+
let stackValue = stackMode === 'none' ? null : stackMode;
|
|
6
|
+
const stackableChannels = ['x', 'y', 'theta', 'radius'];
|
|
7
|
+
for (let ch of stackableChannels) {
|
|
8
|
+
if (encoding[ch] && encoding[ch].type === 'quantitative') {
|
|
9
|
+
encoding[ch].stack = stackValue
|
|
10
|
+
}
|
|
10
11
|
}
|
|
11
12
|
}
|
|
@@ -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
|
|
|
@@ -364,6 +367,7 @@ const VisualSettings: React.FC<IVisualSettings> = ({
|
|
|
364
367
|
none: XMarkIcon,
|
|
365
368
|
stack: ChevronDoubleUpIcon,
|
|
366
369
|
normalize: ArrowsUpDownIcon,
|
|
370
|
+
center: ChevronUpDownIcon, // TODO: fix unsafe extends
|
|
367
371
|
}[g],
|
|
368
372
|
})),
|
|
369
373
|
value: stack,
|
|
@@ -493,6 +497,22 @@ const VisualSettings: React.FC<IVisualSettings> = ({
|
|
|
493
497
|
},
|
|
494
498
|
...(extra.length === 0 ? [] : ['-', ...extra]),
|
|
495
499
|
'-',
|
|
500
|
+
{
|
|
501
|
+
key: 'limit_axis',
|
|
502
|
+
label: t('limit'),
|
|
503
|
+
icon: HashtagIcon,
|
|
504
|
+
form: (
|
|
505
|
+
<FormContainer>
|
|
506
|
+
<LimitSetting
|
|
507
|
+
value={limit}
|
|
508
|
+
setValue={(v) => {
|
|
509
|
+
vizStore.setLimit(v);
|
|
510
|
+
}}
|
|
511
|
+
/>
|
|
512
|
+
</FormContainer>
|
|
513
|
+
),
|
|
514
|
+
},
|
|
515
|
+
'-',
|
|
496
516
|
{
|
|
497
517
|
key: 'kanaries',
|
|
498
518
|
label: 'kanaries',
|
|
@@ -532,6 +552,7 @@ const VisualSettings: React.FC<IVisualSettings> = ({
|
|
|
532
552
|
dark,
|
|
533
553
|
extra,
|
|
534
554
|
exclude,
|
|
555
|
+
limit,
|
|
535
556
|
]);
|
|
536
557
|
|
|
537
558
|
return (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {import('../interfaces').IRow[]} dataSource
|
|
6
|
-
* @param {import('../interfaces').
|
|
6
|
+
* @param {import('../interfaces').IFilterFiledSimple[]} filters
|
|
7
7
|
* @return {import('../interfaces').IRow[]}
|
|
8
8
|
*/
|
|
9
9
|
const filter = (dataSource, filters) => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IRow } from '../interfaces';
|
|
2
|
+
|
|
3
|
+
function compareMulti(a: number[], b: number[]): number {
|
|
4
|
+
if (a.length < b.length) return -compareMulti(b, a);
|
|
5
|
+
for (let i = 0; i < a.length; i++) {
|
|
6
|
+
if (!b[i]) return 1;
|
|
7
|
+
const c = a[i] - b[i];
|
|
8
|
+
if (c !== 0) return c;
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sortBy(data: IRow[], viewMeasures: string[], sort: 'ascending' | 'descending') {
|
|
14
|
+
const sortM = sort === 'ascending' ? 1 : -1;
|
|
15
|
+
return data
|
|
16
|
+
.map((x) => ({
|
|
17
|
+
data: x,
|
|
18
|
+
value: viewMeasures.map((f) => x[f]),
|
|
19
|
+
}))
|
|
20
|
+
.sort((a, b) => sortM * compareMulti(a.value, b.value))
|
|
21
|
+
.map((x) => x.data);
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IRow } from '../interfaces';
|
|
2
|
+
import { sortBy } from './sort';
|
|
3
|
+
|
|
4
|
+
const main = (e: {
|
|
5
|
+
data: {
|
|
6
|
+
data: IRow[];
|
|
7
|
+
viewMeasures: string[];
|
|
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);
|
package/src/workers/transform.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ITransformWorkflowStep, IRow, IExpression } from "../interfaces";
|
|
2
2
|
import { dataframe2Dataset, dataset2DataFrame, execExpression } from "../lib/execExp";
|
|
3
3
|
|
|
4
|
-
export function transformData(data: IRow[],
|
|
5
|
-
|
|
6
|
-
let
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
df = execExpression(field.expression!, df, columns);
|
|
4
|
+
export function transformData(data: IRow[], trans: { key: string, expression: IExpression }[]) {
|
|
5
|
+
let df = dataset2DataFrame(data);
|
|
6
|
+
for (let i = 0; i < trans.length; i++) {
|
|
7
|
+
const field = trans[i];
|
|
8
|
+
df = execExpression(field.expression, df);
|
|
10
9
|
}
|
|
11
|
-
return dataframe2Dataset(df
|
|
10
|
+
return dataframe2Dataset(df);
|
|
12
11
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { transformData } from './transform'
|
|
2
2
|
const main = e => {
|
|
3
|
-
const { dataSource,
|
|
3
|
+
const { dataSource, trans } = e.data;
|
|
4
4
|
|
|
5
5
|
try {
|
|
6
|
-
const ans = transformData(dataSource,
|
|
6
|
+
const ans = transformData(dataSource, trans);
|
|
7
7
|
self.postMessage(ans);
|
|
8
8
|
} catch (error) {
|
|
9
9
|
self.postMessage({ error: error.message });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { queryView } from '../lib/viewQuery'
|
|
2
2
|
const main = e => {
|
|
3
3
|
try {
|
|
4
|
-
const { dataSource,
|
|
5
|
-
const ans = queryView(dataSource,
|
|
4
|
+
const { dataSource, query } = e.data;
|
|
5
|
+
const ans = queryView(dataSource, query);
|
|
6
6
|
self.postMessage(ans);
|
|
7
7
|
|
|
8
8
|
} catch (err) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transform.worker-a12fb3d8.js","sources":["../src/lib/execExp.ts","../src/workers/transform.ts","../src/workers/transform.worker.js"],"sourcesContent":["import { IExpParamter, IExpression, IField, IRow } from \"../interfaces\";\n\ninterface IDataFrame {\n [key: string]: any[];\n}\n\nexport function execExpression (exp: IExpression, dataFrame: IDataFrame, columns: IField[]): IDataFrame {\n const { op, params } = exp;\n const subFrame: IDataFrame = { ...dataFrame };\n const len = dataFrame[Object.keys(dataFrame)[0]].length;\n for (let param of params) {\n switch (param.type) {\n case 'field':\n subFrame[param.value] = dataFrame[param.value];\n break;\n case 'constant':\n subFrame[param.value] = new Array(len).fill(param.value);\n break;\n case 'expression':\n let f = execExpression(param.value, dataFrame, columns);\n Object.keys(f).forEach(key => {\n subFrame[key] = f[key];\n })\n break;\n case 'value':\n default:\n break;\n }\n }\n switch (op) {\n case 'one':\n return one(exp.as, params, subFrame);\n case 'bin':\n return bin(exp.as, params, subFrame);\n case 'log2':\n return log2(exp.as, params, subFrame);\n case 'log10':\n return log10(exp.as, params, subFrame);\n case 'binCount':\n return binCount(exp.as, params, subFrame);\n default:\n return subFrame;\n }\n}\n\nfunction bin(resKey: string, params: IExpParamter[], data: IDataFrame, binSize: number | undefined = 10): IDataFrame {\n const { value: fieldKey } = params[0];\n const fieldValues = data[fieldKey] as number[];\n let _min = Infinity;\n let _max = -Infinity;\n for (let i = 0; i < fieldValues.length; i++) {\n let val = fieldValues[i];\n if (val > _max) _max = val;\n if (val < _min) _min = val;\n }\n const step = (_max - _min) / binSize;\n const beaStep = Math.max(-Math.round(Math.log10(_max - _min)) + 2, 0)\n const newValues = fieldValues.map((v: number) => {\n let bIndex = Math.floor((v - _min) / step);\n if (bIndex === binSize) bIndex = binSize - 1;\n return Number(((bIndex * step + _min)).toFixed(beaStep))\n });\n return {\n ...data,\n [resKey]: newValues,\n }\n}\n\nfunction binCount(resKey: string, params: IExpParamter[], data: IDataFrame, binSize: number | undefined = 10): IDataFrame {\n const { value: fieldKey } = params[0];\n const fieldValues = data[fieldKey] as number[];\n\n const valueWithIndices: {val: number; index: number; orderIndex: number }[] = fieldValues.map((v, i) => ({\n val: v,\n index: i\n })).sort((a, b) => a.val - b.val)\n .map((item, i) => ({\n val: item.val,\n index: item.index,\n orderIndex: i\n }))\n\n const groupSize = valueWithIndices.length / binSize;\n\n const newValues = valueWithIndices.sort((a, b) => a.index - b.index).map(item => {\n let bIndex = Math.floor(item.orderIndex / groupSize);\n if (bIndex === binSize) bIndex = binSize - 1;\n return bIndex + 1\n })\n return {\n ...data,\n [resKey]: newValues,\n }\n}\n\nfunction log2(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFrame {\n const { value } = params[0];\n const field = data[value];\n const newField = field.map((v: number) => Math.log2(v));\n return {\n ...data,\n [resKey]: newField,\n }\n}\n\nfunction log10(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFrame {\n const { value: fieldKey } = params[0];\n const fieldValues = data[fieldKey];\n const newField = fieldValues.map((v: number) => Math.log10(v));\n return {\n ...data,\n [resKey]: newField,\n }\n}\n\nfunction one(resKey: string, params: IExpParamter[], data: IDataFrame): IDataFrame {\n // const { value: fieldKey } = params[0];\n if (Object.keys(data).length === 0) return data;\n const len = data[Object.keys(data)[0]].length;\n const newField = new Array(len).fill(1);\n return {\n ...data,\n [resKey]: newField,\n }\n}\n\nexport function dataset2DataFrame(dataset: IRow[], columns: IField[]): IDataFrame {\n const dataFrame: IDataFrame = {};\n columns.forEach((col) => {\n dataFrame[col.fid] = dataset.map((row) => row[col.fid]);\n });\n return dataFrame;\n}\n\nexport function dataframe2Dataset(dataFrame: IDataFrame, columns: IField[]): IRow[] {\n if (columns.length === 0) return [];\n const dataset: IRow[] = [];\n const len = dataFrame[Object.keys(dataFrame)[0]].length;\n for (let i = 0; i < len; i++) {\n const row: IRow = {};\n columns.forEach((col) => {\n row[col.fid] = dataFrame[col.fid][i];\n });\n dataset.push(row);\n }\n return dataset;\n}\n","import { IField, IRow } from \"../interfaces\";\nimport { dataframe2Dataset, dataset2DataFrame, execExpression } from \"../lib/execExp\";\n\nexport function transformData(data: IRow[], columns: IField[]) {\n const computedFields = columns.filter((f) => f.computed);\n let df = dataset2DataFrame(data, columns);\n for (let i = 0; i < computedFields.length; i++) {\n const field = computedFields[i];\n df = execExpression(field.expression!, df, columns);\n }\n return dataframe2Dataset(df, columns);\n}\n","import { transformData } from './transform'\nconst main = e => {\n const { dataSource, columns } = e.data;\n\n try {\n const ans = transformData(dataSource, columns);\n self.postMessage(ans);\n } catch (error) {\n self.postMessage({ error: error.message });\n }\n};\n\nself.addEventListener('message', main, false);"],"names":["execExpression","exp","dataFrame","columns","op","params","subFrame","len","param","f","key","one","bin","log2","log10","binCount","resKey","data","binSize","fieldKey","fieldValues","_min","_max","i","val","step","beaStep","newValues","v","bIndex","valueWithIndices","b","item","groupSize","value","newField","dataset2DataFrame","dataset","col","row","dataframe2Dataset","transformData","computedFields","df","field","main","e","dataSource","ans","error"],"mappings":"yBAMgB,SAAAA,EAAgBC,EAAkBC,EAAuBC,EAA+B,CAC9F,KAAA,CAAE,GAAAC,EAAI,OAAAC,CAAW,EAAAJ,EACjBK,EAAuB,CAAE,GAAGJ,GAC5BK,EAAML,EAAU,OAAO,KAAKA,CAAS,EAAE,CAAC,CAAC,EAAE,OACjD,QAASM,KAASH,EACd,OAAQG,EAAM,KAAM,CAChB,IAAK,QACDF,EAASE,EAAM,KAAK,EAAIN,EAAUM,EAAM,KAAK,EAC7C,MACJ,IAAK,WACQF,EAAAE,EAAM,KAAK,EAAI,IAAI,MAAMD,CAAG,EAAE,KAAKC,EAAM,KAAK,EACvD,MACJ,IAAK,aACD,IAAIC,EAAIT,EAAeQ,EAAM,MAAON,CAAkB,EACtD,OAAO,KAAKO,CAAC,EAAE,QAAeC,GAAA,CACjBJ,EAAAI,CAAG,EAAID,EAAEC,CAAG,CAAA,CACxB,EACD,KAIR,CAEJ,OAAQN,EAAI,CACR,IAAK,MACD,OAAOO,EAAIV,EAAI,GAAII,EAAQC,CAAQ,EACvC,IAAK,MACD,OAAOM,EAAIX,EAAI,GAAII,EAAQC,CAAQ,EACvC,IAAK,OACD,OAAOO,EAAKZ,EAAI,GAAII,EAAQC,CAAQ,EACxC,IAAK,QACD,OAAOQ,EAAMb,EAAI,GAAII,EAAQC,CAAQ,EACzC,IAAK,WACD,OAAOS,EAASd,EAAI,GAAII,EAAQC,CAAQ,EAC5C,QACW,OAAAA,CACf,CACJ,CAEA,SAASM,EAAII,EAAgBX,EAAwBY,EAAkBC,EAA8B,GAAgB,CACjH,KAAM,CAAE,MAAOC,CAAS,EAAId,EAAO,CAAC,EAC9Be,EAAcH,EAAKE,CAAQ,EACjC,IAAIE,EAAO,IACPC,EAAO,KACX,QAASC,EAAI,EAAGA,EAAIH,EAAY,OAAQG,IAAK,CACrC,IAAAC,EAAMJ,EAAYG,CAAC,EACnBC,EAAMF,IAAaA,EAAAE,GACnBA,EAAMH,IAAaA,EAAAG,EAC3B,CACM,MAAAC,GAAQH,EAAOD,GAAQH,EACvBQ,EAAU,KAAK,IAAI,CAAC,KAAK,MAAM,KAAK,MAAMJ,EAAOD,CAAI,CAAC,EAAI,EAAG,CAAC,EAC9DM,EAAYP,EAAY,IAAKQ,GAAc,CAC7C,IAAIC,EAAS,KAAK,OAAOD,EAAIP,GAAQI,CAAI,EACzC,OAAII,IAAWX,IAASW,EAASX,EAAU,GACpC,QAASW,EAASJ,EAAOJ,GAAO,QAAQK,CAAO,CAAC,CAAA,CAC1D,EACM,MAAA,CACH,GAAGT,EACH,CAACD,CAAM,EAAGW,CAAA,CAElB,CAEA,SAASZ,EAASC,EAAgBX,EAAwBY,EAAkBC,EAA8B,GAAgB,CACtH,KAAM,CAAE,MAAOC,CAAS,EAAId,EAAO,CAAC,EAG9ByB,EAFcb,EAAKE,CAAQ,EAEyD,IAAI,CAACS,EAAG,KAAO,CACrG,IAAKA,EACL,MAAO,CACT,EAAA,EAAE,KAAK,CAAC,EAAGG,IAAM,EAAE,IAAMA,EAAE,GAAG,EAC3B,IAAI,CAACC,EAAM,KAAO,CACf,IAAKA,EAAK,IACV,MAAOA,EAAK,MACZ,WAAY,CACd,EAAA,EAEAC,EAAYH,EAAiB,OAASZ,EAEtCS,EAAYG,EAAiB,KAAK,CAAC,EAAGC,IAAM,EAAE,MAAQA,EAAE,KAAK,EAAE,IAAYC,GAAA,CAC7E,IAAIH,EAAS,KAAK,MAAMG,EAAK,WAAaC,CAAS,EACnD,OAAIJ,IAAWX,IAASW,EAASX,EAAU,GACpCW,EAAS,CAAA,CACnB,EACM,MAAA,CACH,GAAGZ,EACH,CAACD,CAAM,EAAGW,CAAA,CAElB,CAEA,SAASd,EAAKG,EAAgBX,EAAwBY,EAA8B,CAChF,KAAM,CAAE,MAAAiB,CAAA,EAAU7B,EAAO,CAAC,EAEpB8B,EADQlB,EAAKiB,CAAK,EACD,IAAKN,GAAc,KAAK,KAAKA,CAAC,CAAC,EAC/C,MAAA,CACH,GAAGX,EACH,CAACD,CAAM,EAAGmB,CAAA,CAElB,CAEA,SAASrB,EAAME,EAAgBX,EAAwBY,EAA8B,CACjF,KAAM,CAAE,MAAOE,CAAS,EAAId,EAAO,CAAC,EAE9B8B,EADclB,EAAKE,CAAQ,EACJ,IAAKS,GAAc,KAAK,MAAMA,CAAC,CAAC,EACtD,MAAA,CACH,GAAGX,EACH,CAACD,CAAM,EAAGmB,CAAA,CAElB,CAEA,SAASxB,EAAIK,EAAgBX,EAAwBY,EAA8B,CAE/E,GAAI,OAAO,KAAKA,CAAI,EAAE,SAAW,EAAU,OAAAA,EACrC,MAAAV,EAAMU,EAAK,OAAO,KAAKA,CAAI,EAAE,CAAC,CAAC,EAAE,OACjCkB,EAAW,IAAI,MAAM5B,CAAG,EAAE,KAAK,CAAC,EAC/B,MAAA,CACH,GAAGU,EACH,CAACD,CAAM,EAAGmB,CAAA,CAElB,CAEgB,SAAAC,EAAkBC,EAAiBlC,EAA+B,CAC9E,MAAMD,EAAwB,CAAA,EACtB,OAAAC,EAAA,QAASmC,GAAQ,CACXpC,EAAAoC,EAAI,GAAG,EAAID,EAAQ,IAAKE,GAAQA,EAAID,EAAI,GAAG,CAAC,CAAA,CACzD,EACMpC,CACX,CAEgB,SAAAsC,EAAkBtC,EAAuBC,EAA2B,CAChF,GAAIA,EAAQ,SAAW,EAAG,MAAO,GACjC,MAAMkC,EAAkB,CAAA,EAClB9B,EAAML,EAAU,OAAO,KAAKA,CAAS,EAAE,CAAC,CAAC,EAAE,OACjD,QAASqB,EAAI,EAAGA,EAAIhB,EAAKgB,IAAK,CAC1B,MAAMgB,EAAY,CAAA,EACVpC,EAAA,QAASmC,GAAQ,CACrBC,EAAID,EAAI,GAAG,EAAIpC,EAAUoC,EAAI,GAAG,EAAEf,CAAC,CAAA,CACtC,EACDc,EAAQ,KAAKE,CAAG,CACpB,CACO,OAAAF,CACX,CC/IgB,SAAAI,EAAcxB,EAAcd,EAAmB,CAC3D,MAAMuC,EAAiBvC,EAAQ,OAAQM,GAAMA,EAAE,QAAQ,EACnD,IAAAkC,EAAKP,EAAkBnB,EAAMd,CAAO,EACxC,QAASoB,EAAI,EAAGA,EAAImB,EAAe,OAAQnB,IAAK,CACtC,MAAAqB,EAAQF,EAAenB,CAAC,EAC9BoB,EAAK3C,EAAe4C,EAAM,WAAaD,CAAW,CACtD,CACO,OAAAH,EAAkBG,EAAIxC,CAAO,CACxC,CCVA,MAAM0C,EAAOC,GAAK,CACd,KAAM,CAAE,WAAAC,EAAY,QAAA5C,GAAY2C,EAAE,KAElC,GAAI,CACA,MAAME,EAAMP,EAAcM,EAAY5C,CAAO,EAC7C,KAAK,YAAY6C,CAAG,CACvB,OAAQC,EAAP,CACE,KAAK,YAAY,CAAE,MAAOA,EAAM,OAAS,CAAA,CAC5C,CACL,EAEA,KAAK,iBAAiB,UAAWJ,EAAM,EAAK"}
|