Skip to content

Commit

Permalink
fix: computed field with filter (#192)
Browse files Browse the repository at this point in the history
* fix: inner field cannot move to filter & meta

* fix: field stat with computed field

* feat: workflow with computed field

* fix: computed filter not in graph
  • Loading branch information
islxyqwe authored Oct 19, 2023
1 parent cd3e6b1 commit 881ccc9
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 93 deletions.
47 changes: 35 additions & 12 deletions packages/graphic-walker/src/computation/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import type { IComputationFunction, IDataQueryPayload, IDataQueryWorkflowStep, IDatasetStats, IFieldStats, IRow } from '../interfaces';
import type {
IComputationFunction,
IDataQueryPayload,
IDataQueryWorkflowStep,
IDatasetStats,
IField,
IFieldStats,
IRow,
ITransformWorkflowStep,
} from '../interfaces';
import { getTimeFormat } from '../lib/inferMeta';

export const datasetStats = async (service: IComputationFunction): Promise<IDatasetStats> => {
Expand Down Expand Up @@ -63,19 +72,33 @@ export const dataQuery = async (service: IComputationFunction, workflow: IDataQu
return res;
};

export const fieldStat = async (service: IComputationFunction, field: string, options: { values?: boolean; range?: boolean }): Promise<IFieldStats> => {
export const fieldStat = async (service: IComputationFunction, field: IField, options: { values?: boolean; range?: boolean }): Promise<IFieldStats> => {
const { values = true, range = true } = options;
const COUNT_ID = `count_${field}`;
const MIN_ID = `min_${field}`;
const MAX_ID = `max_${field}`;
const COUNT_ID = `count_${field.fid}`;
const MIN_ID = `min_${field.fid}`;
const MAX_ID = `max_${field.fid}`;
const transformWork: ITransformWorkflowStep[] = field.computed
? [
{
type: 'transform',
transform: [
{
expression: field.expression!,
key: field.fid,
},
],
},
]
: [];
const valuesQueryPayload: IDataQueryPayload = {
workflow: [
...transformWork,
{
type: 'view',
query: [
{
op: 'aggregate',
groupBy: [field],
groupBy: [field.fid],
measures: [
{
field: '*',
Expand All @@ -91,6 +114,7 @@ export const fieldStat = async (service: IComputationFunction, field: string, op
const valuesRes = values ? await service(valuesQueryPayload) : [];
const rangeQueryPayload: IDataQueryPayload = {
workflow: [
...transformWork,
{
type: 'view',
query: [
Expand All @@ -99,12 +123,12 @@ export const fieldStat = async (service: IComputationFunction, field: string, op
groupBy: [],
measures: [
{
field,
field: field.fid,
agg: 'min',
asFieldKey: MIN_ID,
},
{
field,
field: field.fid,
agg: 'max',
asFieldKey: MAX_ID,
},
Expand Down Expand Up @@ -132,14 +156,13 @@ export const fieldStat = async (service: IComputationFunction, field: string, op
values: valuesRes
.sort((a, b) => b[COUNT_ID] - a[COUNT_ID])
.map((row) => ({
value: row[field],
value: row[field.fid],
count: row[COUNT_ID],
})),
range: [rangeRes[MIN_ID], rangeRes[MAX_ID]],
};
};


export async function getSample(service: IComputationFunction, field: string) {
const res = await service({
workflow: [
Expand Down Expand Up @@ -183,7 +206,7 @@ export async function getTemporalRange(service: IComputationFunction, field: str
field,
agg: 'max',
asFieldKey: MAX_ID,
format
format,
},
],
},
Expand All @@ -196,6 +219,6 @@ export async function getTemporalRange(service: IComputationFunction, field: str
[MIN_ID]: 0,
[MAX_ID]: 0,
},
] = await service(rangeQueryPayload);
] = await service(rangeQueryPayload);
return [new Date(rangeRes[MIN_ID]).getTime(), new Date(rangeRes[MAX_ID]).getTime()] as [number, number];
}
113 changes: 70 additions & 43 deletions packages/graphic-walker/src/fields/datasetFields/utils.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,155 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useCompututaion, useVizStore } from "../../store";
import type { IActionMenuItem } from "../../components/actionMenu/list";
import { COUNT_FIELD_ID, DATE_TIME_DRILL_LEVELS, DATE_TIME_FEATURE_LEVELS } from "../../constants";
import { getSample } from "../../computation";
import { getTimeFormat } from "../../lib/inferMeta";

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompututaion, useVizStore } from '../../store';
import type { IActionMenuItem } from '../../components/actionMenu/list';
import { COUNT_FIELD_ID, DATE_TIME_DRILL_LEVELS, DATE_TIME_FEATURE_LEVELS, MEA_KEY_ID, MEA_VAL_ID } from '../../constants';
import { getSample } from '../../computation';
import { getTimeFormat } from '../../lib/inferMeta';

const keepTrue = <T extends string | number | object | Function | symbol>(array: (T | 0 | null | false | undefined | void)[]): T[] => {
return array.filter(Boolean) as T[];
};

export const useMenuActions = (channel: "dimensions" | "measures"): IActionMenuItem[][] => {
export const useMenuActions = (channel: 'dimensions' | 'measures'): IActionMenuItem[][] => {
const vizStore = useVizStore();
const fields = vizStore.currentVis.encodings[channel];
const { t } = useTranslation('translation', { keyPrefix: "field_menu" });
const { t } = useTranslation('translation', { keyPrefix: 'field_menu' });
const computation = useCompututaion();

return useMemo<IActionMenuItem[][]>(() => {
return fields.map((f, index) => {
const isDateTimeDrilled = f.expression?.op === 'dateTimeDrill';

const isInnerField = [COUNT_FIELD_ID, MEA_KEY_ID, MEA_VAL_ID].includes(f.fid);
return keepTrue<IActionMenuItem>([
channel === 'dimensions' && {
label: t('to_mea'),
disabled: isInnerField,
onPress() {
vizStore.moveField("dimensions", index, "measures", vizStore.viewMeasures.length);
vizStore.moveField('dimensions', index, 'measures', vizStore.viewMeasures.length);
},
},
channel === 'measures' && {
label: t('to_dim'),
disabled: f.fid === COUNT_FIELD_ID,
disabled: isInnerField,
onPress() {
vizStore.moveField("measures", index, "dimensions", vizStore.viewDimensions.length);
vizStore.moveField('measures', index, 'dimensions', vizStore.viewDimensions.length);
},
},
{
label: t('new_calc'),
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal',
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal' || isInnerField,
children: [
{
label: t('bin'),
onPress() {
vizStore.createBinField(channel, index, "bin");
vizStore.createBinField(channel, index, 'bin');
},
},
{
label: t('binCount'),
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal',
onPress() {
vizStore.createBinField(channel, index, "binCount");
vizStore.createBinField(channel, index, 'binCount');
},
},
{
label: t('log', {base: 10}),
label: t('log', { base: 10 }),
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal',
onPress() {
vizStore.createLogField(channel, index, "log", 10);
vizStore.createLogField(channel, index, 'log', 10);
},
},
{
label: t('log', {base: 2}),
label: t('log', { base: 2 }),
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal',
onPress() {
vizStore.createLogField(channel, index, "log", 2);
vizStore.createLogField(channel, index, 'log', 2);
},
},
{
label:t('binCustom'),
label: t('binCustom'),
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal',
onPress(){
onPress() {
vizStore.setShowBinSettingPanel(true);
vizStore.setCreateField({channel:channel,index:index});
}
vizStore.setCreateField({ channel: channel, index: index });
},
},
{
label:t('logCustom'),
label: t('logCustom'),
disabled: f.semanticType === 'nominal' || f.semanticType === 'ordinal',
onPress(){
onPress() {
vizStore.setShowLogSettingPanel(true);
vizStore.setCreateField({channel:channel,index:index})
}
vizStore.setCreateField({ channel: channel, index: index });
},
},

],
},
{
label: t('semantic_type.name'),
children: (["nominal", "ordinal", "quantitative", "temporal"] as const).map(x => ({
disabled: isInnerField,
children: (['nominal', 'ordinal', 'quantitative', 'temporal'] as const).map((x) => ({
label: t(`semantic_type.types.${x}`),
disabled: f.semanticType === x,
onPress() {
const originChannel = f.analyticType === 'dimension' ? 'dimensions' : 'measures';
vizStore.changeSemanticType(originChannel, index, x);
},
}))
})),
},
(f.semanticType === 'temporal' || isDateTimeDrilled) && {
label: t('drill.name'),
children: DATE_TIME_DRILL_LEVELS.map(level => ({
disabled: isInnerField,
children: DATE_TIME_DRILL_LEVELS.map((level) => ({
label: t(`drill.levels.${level}`),
disabled: isDateTimeDrilled && f.expression?.params.find(p => p.type === 'value')?.value === level,
disabled: isDateTimeDrilled && f.expression?.params.find((p) => p.type === 'value')?.value === level,
onPress() {
const originField = (isDateTimeDrilled ? vizStore.allFields.find(field => field.fid === f.expression?.params.find(p => p.type === 'field')?.value) : null) ?? f;
const originField =
(isDateTimeDrilled
? vizStore.allFields.find((field) => field.fid === f.expression?.params.find((p) => p.type === 'field')?.value)
: null) ?? f;
const originChannel = originField.analyticType === 'dimension' ? 'dimensions' : 'measures';
const originIndex = vizStore.allFields.findIndex(x => x.fid === originField.fid);
getSample(computation, originField.fid).then(getTimeFormat).then(format => vizStore.createDateTimeDrilledField(originChannel, originIndex, level, `${t(`drill.levels.${level}`)} (${originField.name || originField.fid})`, format));
const originIndex = vizStore.allFields.findIndex((x) => x.fid === originField.fid);
getSample(computation, originField.fid)
.then(getTimeFormat)
.then((format) =>
vizStore.createDateTimeDrilledField(
originChannel,
originIndex,
level,
`${t(`drill.levels.${level}`)} (${originField.name || originField.fid})`,
format
)
);
},
})),
},
(f.semanticType === 'temporal' || isDateTimeDrilled) && {
label: t('drill.feature_name'),
children: DATE_TIME_FEATURE_LEVELS.map(level => ({
disabled: isInnerField,
children: DATE_TIME_FEATURE_LEVELS.map((level) => ({
label: t(`drill.levels.${level}`),
disabled: isDateTimeDrilled && f.expression?.params.find(p => p.type === 'value')?.value === level,
disabled: isDateTimeDrilled && f.expression?.params.find((p) => p.type === 'value')?.value === level,
onPress() {
const originField = (isDateTimeDrilled ? vizStore.allFields.find(field => field.fid === f.expression?.params.find(p => p.type === 'field')?.value) : null) ?? f;
const originField =
(isDateTimeDrilled
? vizStore.allFields.find((field) => field.fid === f.expression?.params.find((p) => p.type === 'field')?.value)
: null) ?? f;
const originChannel = originField.analyticType === 'dimension' ? 'dimensions' : 'measures';
const originIndex = vizStore.allFields.findIndex(x => x.fid === originField.fid);
getSample(computation, originField.fid).then(getTimeFormat).then(format => vizStore.createDateFeatureField(originChannel, originIndex, level,`${t(`drill.levels.${level}`)} [${originField.name || originField.fid}]`, format));
const originIndex = vizStore.allFields.findIndex((x) => x.fid === originField.fid);
getSample(computation, originField.fid)
.then(getTimeFormat)
.then((format) =>
vizStore.createDateFeatureField(
originChannel,
originIndex,
level,
`${t(`drill.levels.${level}`)} [${originField.name || originField.fid}]`,
format
)
);
},
})),
},

]);
});
}, [channel, fields, vizStore, t, computation]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Tabs, { RuleFormProps } from './tabs';
import DefaultButton from '../../components/button/default';
import PrimaryButton from '../../components/button/primary';
import DropdownSelect from '../../components/dropdownSelect';
import { COUNT_FIELD_ID, MEA_KEY_ID, MEA_VAL_ID } from '../../constants';

const QuantitativeRuleForm: React.FC<RuleFormProps> = ({ rawFields, field, onChange }) => {
return <Tabs field={field} onChange={onChange} tabs={['range', 'one of']} rawFields={rawFields} />;
Expand Down Expand Up @@ -72,7 +73,7 @@ const FilterEditDialog: React.FC = observer(() => {
}, [editingFilterIdx, uncontrolledField?.rule, vizStore]);

const allFieldOptions = React.useMemo(() => {
return allFields.map((d) => ({
return allFields.filter(x => ![COUNT_FIELD_ID, MEA_KEY_ID, MEA_VAL_ID].includes(x.fid)).map((d) => ({
label: d.name,
value: d.fid,
}));
Expand Down
2 changes: 1 addition & 1 deletion packages/graphic-walker/src/fields/filterField/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const useFieldStats = (
React.useEffect(() => {
setLoading(true);
let isCancelled = false;
fieldStat(computation, fid, { values, range })
fieldStat(computation, field, { values, range })
.then((stats) => {
if (isCancelled) {
return;
Expand Down
10 changes: 9 additions & 1 deletion packages/graphic-walker/src/store/visualSpecStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
IChartForExport,
} from '../interfaces';
import { GLOBAL_CONFIG } from '../config';
import { DATE_TIME_DRILL_LEVELS, DATE_TIME_FEATURE_LEVELS } from '../constants';
import { COUNT_FIELD_ID, DATE_TIME_DRILL_LEVELS, DATE_TIME_FEATURE_LEVELS, MEA_KEY_ID, MEA_VAL_ID } from '../constants';

import { toWorkflow } from '../utils/workflow';
import { KVTuple, uniqueId } from '../models/utils';
Expand Down Expand Up @@ -246,6 +246,10 @@ export class VizSpecStore {
}

private appendFilter(index: number, sourceKey: keyof Omit<DraggableFieldState, 'filters'>, sourceIndex: number) {
const oriF = this.currentEncodings[sourceKey][sourceIndex];
if (oriF.fid === MEA_KEY_ID || oriF.fid === MEA_VAL_ID || oriF.fid === COUNT_FIELD_ID) {
return;
}
this.visList[this.visIndex] = performers.appendFilter(this.visList[this.visIndex], index, sourceKey, sourceIndex, uniqueId());
this.editingFilterIdx = index;
}
Expand Down Expand Up @@ -338,8 +342,12 @@ export class VizSpecStore {
} else if (destinationKey === 'filters') {
return this.appendFilter(destinationIndex, sourceKey, sourceIndex);
}
const oriF = this.currentEncodings[sourceKey][sourceIndex];
const sourceMeta = GLOBAL_CONFIG.META_FIELD_KEYS.includes(sourceKey);
const destMeta = GLOBAL_CONFIG.META_FIELD_KEYS.includes(destinationKey);
if (destMeta && (oriF.fid === MEA_KEY_ID || oriF.fid === MEA_VAL_ID || oriF.fid === COUNT_FIELD_ID)) {
return;
}
const limit = GLOBAL_CONFIG.CHANNEL_LIMIT[destinationKey] ?? Infinity;
if (destMeta === sourceMeta) {
this.visList[this.visIndex] = performers.moveField(this.visList[this.visIndex], sourceKey, sourceIndex, destinationKey, destinationIndex, limit);
Expand Down
Loading

1 comment on commit 881ccc9

@vercel
Copy link

@vercel vercel bot commented on 881ccc9 Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.