diff --git a/packages/graphic-walker/.gitignore b/packages/graphic-walker/.gitignore new file mode 100644 index 00000000..e985853e --- /dev/null +++ b/packages/graphic-walker/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/packages/graphic-walker/package.json b/packages/graphic-walker/package.json index dd4bc17d..a6798d21 100644 --- a/packages/graphic-walker/package.json +++ b/packages/graphic-walker/package.json @@ -1,6 +1,6 @@ { "name": "@kanaries/graphic-walker", - "version": "0.3.16", + "version": "0.4.0-a", "scripts": { "dev:front_end": "vite --host", "dev": "npm run dev:front_end", @@ -29,7 +29,7 @@ }, "prettier": { "tabWidth": 4, - "printWidth": 120, + "printWidth": 160, "singleQuote": true }, "types": "./dist/index.d.ts", diff --git a/packages/graphic-walker/src/App.tsx b/packages/graphic-walker/src/App.tsx index 39d6deb4..8115495b 100644 --- a/packages/graphic-walker/src/App.tsx +++ b/packages/graphic-walker/src/App.tsx @@ -20,6 +20,7 @@ import { useCurrentMediaTheme } from './utils/media'; import CodeExport from './components/codeExport'; import VisualConfig from './components/visualConfig'; import type { ToolbarItemProps } from './components/toolbar'; +import AskViz from './components/askViz'; import { getComputation } from './computation/clientComputation'; export interface IGWProps { @@ -43,6 +44,12 @@ export interface IGWProps { extra?: ToolbarItemProps[]; exclude?: string[]; }; + enhanceAPI?: { + header?: Record; + features?: { + askviz?: string | boolean; + } + }; } const App = observer(function App(props) { @@ -58,6 +65,7 @@ const App = observer(function App(props) { dark = 'media', computation, toolbar, + enhanceAPI, } = props; const { commonStore, vizStore } = useGlobalStore(); @@ -158,6 +166,9 @@ const App = observer(function App(props) { style={{ marginTop: '0em', borderTop: 'none' }} className="m-4 p-4 border border-gray-200 dark:border-gray-700" > + {enhanceAPI?.features?.askviz && ( + + )} diff --git a/packages/graphic-walker/src/components/askViz/index.tsx b/packages/graphic-walker/src/components/askViz/index.tsx new file mode 100644 index 00000000..c5650234 --- /dev/null +++ b/packages/graphic-walker/src/components/askViz/index.tsx @@ -0,0 +1,92 @@ +import { observer } from 'mobx-react-lite'; +import React, { useCallback, useState } from 'react'; +import { useGlobalStore } from '../../store'; +import { PaperAirplaneIcon } from '@heroicons/react/24/outline'; +import Spinner from '../spinner'; +import { IViewField } from '../../interfaces'; +import { VisSpecWithHistory } from '../../models/visSpecHistory'; + +type VEGALite = any; + +const api = import.meta.env.DEV ? 'http://localhost:2023/api/vis/text2gw' : 'https://enhanceai.kanaries.net/api/vis/text2gw' + +async function vizQuery(api: string, metas: IViewField[], query: string, headers: Record) { + const res = await fetch(api, { + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + credentials: 'include', + method: 'POST', + body: JSON.stringify({ + metas, + messages: [ + { + role: 'user', + content: query, + }, + ], + }), + }); + const result: { + success: boolean; + data: VEGALite; + message?: string; + } = await res.json(); + if (result.success) { + return result.data; + } else { + throw new Error(result.message); + } +} + +const AskViz: React.FC<{api?: string; headers?: Record}> = (props) => { + const [query, setQuery] = useState(''); + const [loading, setLoading] = useState(false); + const { vizStore } = useGlobalStore(); + + const allFields = vizStore.allFields; + + const startQuery = useCallback(() => { + setLoading(true); + vizQuery(props.api ?? api, allFields, query, props.headers ?? {}) + .then((data) => { + vizStore.visList.push(new VisSpecWithHistory(data)); + vizStore.selectVisualization(vizStore.visList.length - 1); + // const liteGW = parseGW(spec); + // vizStore.renderSpec(liteGW); + }) + .finally(() => { + setLoading(false); + }); + }, [query, allFields]); + return ( +
+ setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && loading === false && query.length > 0) { + startQuery(); + } + }} + disabled={loading || allFields.length === 0} + /> + +
+ ); +}; + +export default observer(AskViz); diff --git a/packages/graphic-walker/src/components/askViz/schemaTransform.ts b/packages/graphic-walker/src/components/askViz/schemaTransform.ts new file mode 100644 index 00000000..904382f5 --- /dev/null +++ b/packages/graphic-walker/src/components/askViz/schemaTransform.ts @@ -0,0 +1,38 @@ +import { Specification } from "../../interfaces"; + +export function parseGW(vlSpec: any): Specification { + const spec: Specification = {}; + if (vlSpec.encoding && vlSpec.mark) { + spec.geomType = [vlSpec.mark]; + spec.position = []; + if (vlSpec.encoding.x && vlSpec.encoding.x.field) { + spec.position.push(vlSpec.encoding.x.field); + if (vlSpec.encoding.x.aggregate) { + spec.aggregate = true; + } + } + if (vlSpec.encoding.y && vlSpec.encoding.y.field) { + spec.position.push(vlSpec.encoding.y.field); + if (vlSpec.encoding.y.aggregate) { + spec.aggregate = true; + } + } + spec.facets = []; + if (vlSpec.encoding.row && vlSpec.encoding.row) { + spec.facets.push(vlSpec.encoding.row); + } + if (vlSpec.encoding.column && vlSpec.encoding.column) { + spec.facets.push(vlSpec.encoding.column); + } + + ['color', 'opacity', 'shape', 'size'].forEach((ch) => { + if (vlSpec.encoding[ch] && vlSpec.encoding[ch].field) { + spec[ch] = [vlSpec.encoding[ch].field]; + if (vlSpec.encoding[ch].aggregate) { + spec.aggregate = true; + } + } + }); + } + return spec; +} \ No newline at end of file diff --git a/packages/graphic-walker/src/components/pivotTable/index.tsx b/packages/graphic-walker/src/components/pivotTable/index.tsx index 31d00972..4fc40f58 100644 --- a/packages/graphic-walker/src/components/pivotTable/index.tsx +++ b/packages/graphic-walker/src/components/pivotTable/index.tsx @@ -78,7 +78,6 @@ const PivotTable: React.FC = (props) => { data ); const metric = buildMetricTableFromNestTree(lt, tt, data); - // debugger unstable_batchedUpdates(() => { setLeftTree(lt); setTopTree(tt); diff --git a/packages/graphic-walker/src/components/spinner.tsx b/packages/graphic-walker/src/components/spinner.tsx new file mode 100644 index 00000000..1bef081b --- /dev/null +++ b/packages/graphic-walker/src/components/spinner.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export default function Spinner() { + return ( + + + + + ); +} \ No newline at end of file diff --git a/packages/graphic-walker/src/config.ts b/packages/graphic-walker/src/config.ts index 4335d356..bab858dd 100644 --- a/packages/graphic-walker/src/config.ts +++ b/packages/graphic-walker/src/config.ts @@ -19,7 +19,8 @@ export const GEMO_TYPES: Readonly = [ export const STACK_MODE: Readonly = [ 'none', 'stack', - 'normalize' + 'normalize', + 'center' ] export const CHART_LAYOUT_TYPE: Readonly = [ diff --git a/packages/graphic-walker/src/interfaces.ts b/packages/graphic-walker/src/interfaces.ts index 0b3aa5d9..4626a6a0 100644 --- a/packages/graphic-walker/src/interfaces.ts +++ b/packages/graphic-walker/src/interfaces.ts @@ -107,10 +107,10 @@ export interface IField { basename?: string; path?: string[], } - +export type ISortMode = 'none' | 'ascending' | 'descending'; export interface IViewField extends IField { dragId: string; - sort?: 'none' | 'ascending' | 'descending'; + sort?: ISortMode; } export interface DataSet { @@ -207,7 +207,7 @@ export type IFilterRule = value: Set; }; -export type IStackMode = 'none' | 'stack' | 'normalize'; +export type IStackMode = 'none' | 'stack' | 'normalize' | 'zero' | 'center'; export interface IVisualConfig { defaultAggregated: boolean; @@ -215,7 +215,7 @@ export interface IVisualConfig { stack: IStackMode; showActions: boolean; interactiveScale: boolean; - sorted: 'none' | 'ascending' | 'descending'; + sorted: ISortMode; zeroScale: boolean; background?: string; format: { diff --git a/packages/graphic-walker/src/locales/en-US.json b/packages/graphic-walker/src/locales/en-US.json index 17d0ee97..8330453e 100644 --- a/packages/graphic-walker/src/locales/en-US.json +++ b/packages/graphic-walker/src/locales/en-US.json @@ -45,7 +45,8 @@ "__enum__": "Stack Mode", "none": "None", "stack": "Stack", - "normalize": "Normalize" + "normalize": "Normalize", + "center": "Center" }, "layout_type": { "__enum__": "Layout Mode", diff --git a/packages/graphic-walker/src/locales/ja-JP.json b/packages/graphic-walker/src/locales/ja-JP.json index 753274a5..99256942 100644 --- a/packages/graphic-walker/src/locales/ja-JP.json +++ b/packages/graphic-walker/src/locales/ja-JP.json @@ -45,7 +45,8 @@ "__enum__": "スタックモード", "none": "なし", "stack": "スタック", - "normalize": "正規化" + "normalize": "正規化", + "center": "中央" }, "layout_type": { "__enum__": "レイアウトタイプ", diff --git a/packages/graphic-walker/src/locales/zh-CN.json b/packages/graphic-walker/src/locales/zh-CN.json index 7e4bda08..b95133af 100644 --- a/packages/graphic-walker/src/locales/zh-CN.json +++ b/packages/graphic-walker/src/locales/zh-CN.json @@ -62,7 +62,8 @@ "__enum__": "堆叠模式", "none": "关闭", "stack": "堆叠", - "normalize": "归一化" + "normalize": "归一化", + "center": "中央" }, "draggable_key": { "fields": "字段", diff --git a/packages/graphic-walker/src/main.tsx b/packages/graphic-walker/src/main.tsx index 03c0cbe3..cff68018 100644 --- a/packages/graphic-walker/src/main.tsx +++ b/packages/graphic-walker/src/main.tsx @@ -5,4 +5,4 @@ if (!import.meta.env.DEV) { inject(); } -embedGraphicWalker(document.getElementById('root') as HTMLElement) \ No newline at end of file +embedGraphicWalker(document.getElementById('root') as HTMLElement, {}) \ No newline at end of file diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index 60d53237..abb38fa5 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -1,47 +1,70 @@ -import { IReactionDisposer, makeAutoObservable, observable, reaction, toJS } from "mobx"; -import produce from "immer"; -import { DataSet, DraggableFieldState, IFilterRule, IViewField, IVisSpec, IVisSpecForExport, IFilterFieldForExport, IVisualConfig, Specification, IComputationFunction } from "../interfaces"; -import { CHANNEL_LIMIT, GEMO_TYPES, MetaFieldKeys } from "../config"; -import { VisSpecWithHistory } from "../models/visSpecHistory"; -import { IStoInfo, dumpsGWPureSpec, parseGWContent, parseGWPureSpec, stringifyGWContent, initVisualConfig, forwardVisualConfigs, visSpecDecoder } from "../utils/save"; -import { CommonStore } from "./commonStore"; -import { createCountField } from "../utils"; -import { nanoid } from "nanoid"; -import { toWorkflow } from "../utils/workflow"; +import { IReactionDisposer, makeAutoObservable, observable, reaction, toJS } from 'mobx'; +import produce from 'immer'; +import { + DataSet, + DraggableFieldState, + IFilterRule, + ISortMode, + IStackMode, + IViewField, + IVisSpec, + IVisSpecForExport, + IFilterFieldForExport, + IVisualConfig, + Specification, + IComputationFunction, +} from '../interfaces'; +import { CHANNEL_LIMIT, GEMO_TYPES, MetaFieldKeys } from '../config'; +import { VisSpecWithHistory } from '../models/visSpecHistory'; +import { + IStoInfo, + dumpsGWPureSpec, + parseGWContent, + parseGWPureSpec, + stringifyGWContent, + initVisualConfig, + forwardVisualConfigs, + visSpecDecoder, +} from '../utils/save'; +import { CommonStore } from './commonStore'; +import { createCountField } from '../utils'; +import { COUNT_FIELD_ID } from '../constants'; +import { nanoid } from 'nanoid'; +import { toWorkflow } from '../utils/workflow'; function getChannelSizeLimit(channel: string): number { - if (typeof CHANNEL_LIMIT[channel] === "undefined") return Infinity; + if (typeof CHANNEL_LIMIT[channel] === 'undefined') return Infinity; return CHANNEL_LIMIT[channel]; } function uniqueId(): string { - return "gw_" + nanoid(4); + return 'gw_' + nanoid(4); } function geomAdapter(geom: string) { switch (geom) { - case "interval": - case "bar": - return "bar"; - case "line": - return "line"; - case "boxplot": - return "boxplot"; - case "area": - return "area"; - case "point": - return "point"; - case "arc": - return "arc"; - case "circle": - return "circle"; - case "heatmap": - return "circle"; - case "rect": - return "rect"; - case "tick": + case 'interval': + case 'bar': + return 'bar'; + case 'line': + return 'line'; + case 'boxplot': + return 'boxplot'; + case 'area': + return 'area'; + case 'point': + return 'point'; + case 'arc': + return 'arc'; + case 'circle': + return 'circle'; + case 'heatmap': + return 'circle'; + case 'rect': + return 'rect'; + case 'tick': default: - return "tick"; + return 'tick'; } } @@ -63,6 +86,28 @@ export function initEncoding(): DraggableFieldState { }; } +function stackValueTransform(vlValue: string | undefined | null): IStackMode { + if (vlValue === 'center') return 'center'; + if (vlValue === 'normalize') return 'normalize'; + if (vlValue === 'zero') return 'zero'; + return 'none'; +} + +function sortValueTransform(vlValue: object | string | null): ISortMode { + let order: string = 'none'; + if (typeof vlValue === 'string') { + order = vlValue; + } else if (vlValue && vlValue instanceof Object) { + order = vlValue['order'] ?? 'ascending'; + } + if (order !== 'none') { + const channels: string[] = ['x', 'y', 'color', 'size', 'opacity']; + // TODO: support all sorting config in vl + if (order.startsWith('-') || order === 'descending') return 'descending'; + if (channels.indexOf(order) > -1 || order === 'ascending') return 'ascending'; + } + return 'none'; +} type DeepReadonly> = { readonly [K in keyof T]: T[K] extends Record ? DeepReadonly : T[K]; @@ -113,7 +158,7 @@ export class VizSpecStore { public canUndo = false; public canRedo = false; public editingFilterIdx: number | null = null; - // TODO + // TODO public computationFuction: IComputationFunction = async () => []; constructor(commonStore: CommonStore) { this.commonStore = commonStore; @@ -177,8 +222,7 @@ export class VizSpecStore { private useMutable(cb: (tab: { encodings: DraggableFieldState; config: IVisualConfig }) => void) { if (this.__dangerous_is_inside_useMutable__) { throw new Error( - "A recursive call of useMutable() is detected, " + - "this is prevented because update will be overwritten by parent execution context." + 'A recursive call of useMutable() is detected, ' + 'this is prevented because update will be overwritten by parent execution context.' ); } @@ -242,7 +286,7 @@ export class VizSpecStore { (Object.keys(state) as (keyof DraggableFieldState)[]) .filter((dkey) => !MetaFieldKeys.includes(dkey)) .forEach((dkey) => { - fields.push(...state[dkey].filter((f) => f.analyticType === "dimension")); + fields.push(...state[dkey].filter((f) => f.analyticType === 'dimension')); }); return fields; } @@ -256,7 +300,7 @@ export class VizSpecStore { (Object.keys(state) as (keyof DraggableFieldState)[]) .filter((dkey) => !MetaFieldKeys.includes(dkey)) .forEach((dkey) => { - fields.push(...state[dkey].filter((f) => f.analyticType === "measure")); + fields.push(...state[dkey].filter((f) => f.analyticType === 'measure')); }); return fields; } @@ -272,7 +316,6 @@ export class VizSpecStore { return state.filters; } - public addVisualization(defaultName?: string) { const name = defaultName || 'Chart ' + (this.visList.length + 1); this.visList.push( @@ -291,8 +334,8 @@ export class VizSpecStore { public setVisName(visIndex: number, name: string) { this.visList[visIndex] = this.visList[visIndex].clone(); this.visList[visIndex].updateLatest({ - name - }) + name, + }); } public initState() { this.useMutable((tab) => { @@ -304,7 +347,7 @@ export class VizSpecStore { const countField = createCountField(); this.useMutable(({ encodings }) => { encodings.dimensions = dataset.rawFields - .filter((f) => f.analyticType === "dimension") + .filter((f) => f.analyticType === 'dimension') .map((f) => ({ dragId: uniqueId(), fid: f.fid, @@ -314,7 +357,7 @@ export class VizSpecStore { analyticType: f.analyticType, })); encodings.measures = dataset.rawFields - .filter((f) => f.analyticType === "measure") + .filter((f) => f.analyticType === 'measure') .map((f) => ({ dragId: uniqueId(), fid: f.fid, @@ -322,13 +365,16 @@ export class VizSpecStore { basename: f.basename || f.name || f.fid, analyticType: f.analyticType, semanticType: f.semanticType, - aggName: "sum", + aggName: 'sum', })); encodings.measures.push(countField); }); this.freezeHistory(); } + /** + * clear all config in draggable state + */ public clearState() { this.useMutable(({ encodings }) => { for (let key in encodings) { @@ -341,34 +387,33 @@ export class VizSpecStore { public setVisualConfig(configKey: K, value: IVisualConfig[K]) { this.useMutable(({ config }) => { switch (true) { - case ["defaultAggregated", "defaultStack", "showActions", "interactiveScale"].includes(configKey): { + case ['defaultAggregated', 'defaultStack', 'showActions', 'interactiveScale'].includes(configKey): { return ((config as unknown as { [k: string]: boolean })[configKey] = Boolean(value)); } - case configKey === "geoms" && Array.isArray(value): - case configKey === "size" && typeof value === "object": - case configKey === "sorted": - case configKey === "zeroScale": - case configKey === "background": - case configKey === "limit": - case configKey === "stack": { + case configKey === 'geoms' && Array.isArray(value): + case configKey === 'size' && typeof value === 'object': + case configKey === 'sorted': + case configKey === 'zeroScale': + case configKey === 'background': + case configKey === 'limit': + case configKey === 'stack': { return (config[configKey] = value); } - case configKey === "format" && typeof value === "object": - case configKey === "resolve" && typeof value === "object": { + case configKey === 'format' && typeof value === 'object': { return (config[configKey] = value); } default: { - console.error("[unknown key] " + configKey + " You should registered visualConfig at setVisualConfig"); + console.error('[unknown key] ' + configKey + ' You should registered visualConfig at setVisualConfig'); } } }); } - public transformCoord(coord: "cartesian" | "polar") { - if (coord === "polar") { + public transformCoord(coord: 'cartesian' | 'polar') { + if (coord === 'polar') { } } - public setChartLayout(props: { mode: IVisualConfig["size"]["mode"]; width?: number; height?: number }) { + public setChartLayout(props: { mode: IVisualConfig['size']['mode']; width?: number; height?: number }) { this.useMutable(({ config }) => { const { mode = config.size.mode, width = config.size.width, height = config.size.height } = props; @@ -387,15 +432,10 @@ export class VizSpecStore { fields.splice(destinationIndex, 0, field); }); } - public moveField( - sourceKey: keyof DraggableFieldState, - sourceIndex: number, - destinationKey: keyof DraggableFieldState, - destinationIndex: number - ) { - if (sourceKey === "filters") { + public moveField(sourceKey: keyof DraggableFieldState, sourceIndex: number, destinationKey: keyof DraggableFieldState, destinationIndex: number) { + if (sourceKey === 'filters') { return this.removeField(sourceKey, sourceIndex); - } else if (destinationKey === "filters") { + } else if (destinationKey === 'filters') { return this.appendFilter(destinationIndex, this.draggableFieldState[sourceKey][sourceIndex]); } @@ -415,7 +455,7 @@ export class VizSpecStore { if (MetaFieldKeys.includes(destinationKey)) { if (!MetaFieldKeys.includes(sourceKey)) return; encodings[sourceKey].splice(sourceIndex, 1); - movingField.analyticType = destinationKey === "dimensions" ? "dimension" : "measure"; + movingField.analyticType = destinationKey === 'dimensions' ? 'dimension' : 'measure'; } const limitSize = getChannelSizeLimit(destinationKey); const fixedDestinationIndex = Math.min(destinationIndex, limitSize - 1); @@ -433,10 +473,7 @@ export class VizSpecStore { } public replaceField(sourceKey: keyof DraggableFieldState, sourceIndex: number, fid: string) { if (MetaFieldKeys.includes(sourceKey)) return; - const enteringField = [ - ...this.draggableFieldState.dimensions, - ...this.draggableFieldState.measures - ].find(which => which.fid === fid); + const enteringField = [...this.draggableFieldState.dimensions, ...this.draggableFieldState.measures].find((which) => which.fid === fid); if (!enteringField) { return; } @@ -475,16 +512,23 @@ export class VizSpecStore { encodings.rows = fieldsInCup as typeof encodings.rows; // assume this as writable }); } - public createBinField(stateKey: keyof DraggableFieldState, index: number, binType: 'bin' | 'binCount') { + public createBinField(stateKey: keyof DraggableFieldState, index: number, binType: 'bin' | 'binCount'): string { + const newVarKey = uniqueId(); + const state = this.draggableFieldState; + const existedRelatedBinField = state.dimensions.find( + (f) => f.computed && f.expression && f.expression.op === binType && f.expression.params[0].value === state[stateKey][index].fid + ); + if (existedRelatedBinField) { + return existedRelatedBinField.fid; + } this.useMutable(({ encodings }) => { const originField = encodings[stateKey][index]; - const newVarKey = uniqueId(); const binField: IViewField = { fid: newVarKey, dragId: newVarKey, name: `${binType}(${originField.name})`, - semanticType: "ordinal", - analyticType: "dimension", + semanticType: 'ordinal', + analyticType: 'dimension', computed: true, expression: { op: binType, @@ -492,16 +536,17 @@ export class VizSpecStore { params: [ { type: 'field', - value: originField.fid - } - ] - } + value: originField.fid, + }, + ], + }, }; encodings.dimensions.push(binField); }); + return newVarKey; } public createLogField(stateKey: keyof DraggableFieldState, index: number, scaleType: 'log10' | 'log2') { - if (stateKey === "filters") { + if (stateKey === 'filters') { return; } @@ -512,7 +557,7 @@ export class VizSpecStore { fid: newVarKey, dragId: newVarKey, name: `${scaleType}(${originField.name})`, - semanticType: "quantitative", + semanticType: 'quantitative', analyticType: originField.analyticType, aggName: 'sum', computed: true, @@ -522,10 +567,10 @@ export class VizSpecStore { params: [ { type: 'field', - value: originField.fid - } - ] - } + value: originField.fid, + }, + ], + }, }; encodings[stateKey].push(logField); }); @@ -543,74 +588,150 @@ export class VizSpecStore { const { rows, columns } = this.draggableFieldState; const yField = rows.length > 0 ? rows[rows.length - 1] : null; const xField = columns.length > 0 ? columns[columns.length - 1] : null; - if ( - xField !== null && - xField.analyticType === "dimension" && - yField !== null && - yField.analyticType === "measure" - ) { + if (xField !== null && xField.analyticType === 'dimension' && yField !== null && yField.analyticType === 'measure') { return true; } - if ( - xField !== null && - xField.analyticType === "measure" && - yField !== null && - yField.analyticType === "dimension" - ) { + if (xField !== null && xField.analyticType === 'measure' && yField !== null && yField.analyticType === 'dimension') { return true; } return false; } - public setFieldSort( - stateKey: keyof DraggableFieldState, - index: number, - sortType: "none" | "ascending" | "descending" - ) { + public setFieldSort(stateKey: keyof DraggableFieldState, index: number, sortType: ISortMode) { this.useMutable(({ encodings }) => { encodings[stateKey][index].sort = sortType; }); } - public applyDefaultSort(sortType: "none" | "ascending" | "descending" = "ascending") { + public applyDefaultSort(sortType: ISortMode = 'ascending') { this.useMutable(({ encodings }) => { const { rows, columns } = encodings; const yField = rows.length > 0 ? rows[rows.length - 1] : null; const xField = columns.length > 0 ? columns[columns.length - 1] : null; - if ( - xField !== null && - xField.analyticType === "dimension" && - yField !== null && - yField.analyticType === "measure" - ) { + if (xField !== null && xField.analyticType === 'dimension' && yField !== null && yField.analyticType === 'measure') { encodings.columns[columns.length - 1].sort = sortType; return; } - if ( - xField !== null && - xField.analyticType === "measure" && - yField !== null && - yField.analyticType === "dimension" - ) { + if (xField !== null && xField.analyticType === 'measure' && yField !== null && yField.analyticType === 'dimension') { encodings.rows[rows.length - 1].sort = sortType; return; } }); } - public appendField(destinationKey: keyof DraggableFieldState, field: IViewField | undefined) { + public appendField(destinationKey: keyof DraggableFieldState, field: IViewField | undefined, overrideAttr?: Record) { if (MetaFieldKeys.includes(destinationKey)) return; - if (typeof field === "undefined") return; - if (destinationKey === "filters") { + if (typeof field === 'undefined') return; + if (destinationKey === 'filters') { return; } this.useMutable(({ encodings }) => { - const cloneField = { ...toJS(field) }; + const cloneField = { ...toJS(field), ...overrideAttr }; cloneField.dragId = uniqueId(); encodings[destinationKey].push(cloneField); }); } - public setVizFormatConfig (formatKey: keyof IVisualConfig['format'], value?: string) { - this.visualConfig[formatKey] = value + public setVizFormatConfig(formatKey: keyof IVisualConfig['format'], value?: string) { + this.visualConfig[formatKey] = value; + } + public renderVLSubset(vlStruct: any) { + const tab = this.visList[this.visIndex]; + this.clearState(); + this.setVisualConfig('defaultAggregated', false); + this.setVisualConfig('stack', 'stack'); + // this.setVisualConfig('sorted', 'none') + this.applyDefaultSort('none'); + + if (!tab) return; + const fields = tab.encodings.dimensions.concat(tab.encodings.measures); + const countField = fields.find((f) => f.fid === COUNT_FIELD_ID); + const renderVLFacet = (vlFacet) => { + if (vlFacet.facet) { + this.appendField('rows', fields.find((f) => f.fid === vlFacet.facet.field) || countField, { analyticType: 'dimension' }); + } + if (vlFacet.row) { + this.appendField('rows', fields.find((f) => f.fid === vlFacet.row.field) || countField, { analyticType: 'dimension' }); + } + if (vlFacet.column) { + this.appendField('columns', fields.find((f) => f.fid === vlFacet.column.field) || countField, { analyticType: 'dimension' }); + } + }; + const isValidAggregate = (aggName) => aggName && ['sum', 'count', 'max', 'min', 'mean', 'median', 'variance', 'stdev'].includes(aggName); + const renderVLSpec = (vlSpec) => { + if (typeof vlSpec.mark === 'string') { + this.setVisualConfig('geoms', [geomAdapter(vlSpec.mark)]); + } else { + this.setVisualConfig('geoms', [geomAdapter(vlSpec.mark.type)]); + } + if (vlSpec.encoding.x) { + const field = fields.find((f) => f.fid === vlSpec.encoding.x.field) || countField; + this.appendField('columns', field, { analyticType: 'dimension' }); + if (isValidAggregate(vlSpec.encoding.x.aggregate) || field === countField) { + this.setVisualConfig('defaultAggregated', true); + this.setFieldAggregator('columns', this.draggableFieldState.columns.length - 1, vlSpec.encoding.x.aggregate); + } + if (vlSpec.encoding.x.bin) { + const binFid = this.createBinField('columns', this.draggableFieldState.columns.length - 1, 'bin'); + this.replaceField('columns', this.draggableFieldState.columns.length - 1, binFid); + } + if (vlSpec.encoding.x.stack) { + this.setVisualConfig('stack', stackValueTransform(vlSpec.encoding.x.stack)); + } + } + if (vlSpec.encoding.y) { + const field = fields.find((f) => f.fid === vlSpec.encoding.y.field) || countField; + this.appendField('rows', field, { analyticType: 'measure' }); + if (isValidAggregate(vlSpec.encoding.y.aggregate) || field === countField) { + this.setVisualConfig('defaultAggregated', true); + this.setFieldAggregator('rows', this.draggableFieldState.rows.length - 1, vlSpec.encoding.y.aggregate); + } + if (vlSpec.encoding.y.bin) { + const binFid = this.createBinField('rows', this.draggableFieldState.rows.length - 1, 'bin'); + this.replaceField('rows', this.draggableFieldState.rows.length - 1, binFid); + } + if (vlSpec.encoding.y.stack) { + this.setVisualConfig('stack', stackValueTransform(vlSpec.encoding.y.stack)); + } + } + + (['color', 'opacity', 'shape', 'size', 'details', 'theta', 'text', 'radius'] as (keyof DraggableFieldState)[]).forEach((ch) => { + if (vlSpec.encoding[ch]) { + const field = fields.find((f) => f.fid === vlSpec.encoding[ch].field) || countField; + this.appendField( + ch, + field, + field !== countField && ['color', 'opacity', 'size', 'radius'].includes(ch) + ? { analyticType: 'dimension' } + : field === countField || ['theta'].includes(ch) + ? { analyticType: 'measure' } + : {} + ); + const aggregate = isValidAggregate(vlSpec.encoding[ch].aggregate); + if ((['theta', 'radius'].includes(ch) && aggregate) || field === countField) { + this.setVisualConfig('defaultAggregated', true); + if (aggregate) { + this.setFieldAggregator(ch, this.draggableFieldState[ch].length - 1, vlSpec.encoding[ch].aggregate); + } + } + } + }); + ['x', 'y', 'facet'].forEach((ch) => { + if (vlSpec.encoding[ch] && vlSpec.encoding[ch].sort) { + this.applyDefaultSort(sortValueTransform(vlSpec.encoding[ch].sort)); + } + }); + if (vlSpec.encoding.order && vlSpec.encoding.order.sort) { + this.applyDefaultSort(sortValueTransform(vlSpec.encoding.order.sort)); + } + }; + if (vlStruct.encoding && vlStruct.mark) { + renderVLFacet(vlStruct.encoding); + renderVLSpec(vlStruct); + } else if (vlStruct.spec) { + if (vlStruct.facet) { + renderVLFacet(vlStruct.facet); + } + renderVLSpec(vlStruct.spec); + } } public renderSpec(spec: Specification) { const tab = this.visList[this.visIndex]; @@ -623,7 +744,7 @@ export class VizSpecStore { this.setVisualConfig('defaultAggregated', Boolean(spec.aggregate)); if ((spec.geomType?.length ?? 0) > 0) { this.setVisualConfig( - "geoms", + 'geoms', spec.geomType!.map((g) => geomAdapter(g)) ); } @@ -631,7 +752,7 @@ export class VizSpecStore { const facets = (spec.facets || []).concat(spec.highFacets || []); for (let facet of facets) { this.appendField( - "rows", + 'rows', fields.find((f) => f.fid === facet) ); } @@ -640,30 +761,30 @@ export class VizSpecStore { const [cols, rows] = spec.position; if (cols) this.appendField( - "columns", + 'columns', fields.find((f) => f.fid === cols) ); if (rows) this.appendField( - "rows", + 'rows', fields.find((f) => f.fid === rows) ); } if ((spec.color?.length ?? 0) > 0) { this.appendField( - "color", + 'color', fields.find((f) => f.fid === spec.color![0]) ); } if ((spec.size?.length ?? 0) > 0) { this.appendField( - "size", + 'size', fields.find((f) => f.fid === spec.size![0]) ); } if ((spec.opacity?.length ?? 0) > 0) { this.appendField( - "opacity", + 'opacity', fields.find((f) => f.fid === spec.opacity![0]) ); } @@ -686,7 +807,7 @@ export class VizSpecStore { const pureVisList = dumpsGWPureSpec(this.visList); return this.visSpecEncoder(pureVisList); } - public importStoInfo (stoInfo: IStoInfo) { + public importStoInfo(stoInfo: IStoInfo) { this.visList = parseGWPureSpec(visSpecDecoder(forwardVisualConfigs(stoInfo.specList))); this.visIndex = 0; this.commonStore.datasets = stoInfo.datasets; @@ -701,25 +822,25 @@ export class VizSpecStore { private visSpecEncoder(visList: IVisSpec[]): IVisSpecForExport[] { const updatedVisList = visList.map((visSpec) => { const updatedFilters = visSpec.encodings.filters.map((filter) => { - if (filter.rule?.type === "one of") { - const rule = { - ...filter.rule, - value: Array.from(filter.rule.value) - } + if (filter.rule?.type === 'one of') { + const rule = { + ...filter.rule, + value: Array.from(filter.rule.value), + }; return { - ...filter, - rule - } - } + ...filter, + rule, + }; + } return filter as IFilterFieldForExport; }); return { ...visSpec, encodings: { ...visSpec.encodings, - filters: updatedFilters - } - } + filters: updatedFilters, + }, + }; }); return updatedVisList; } @@ -753,7 +874,7 @@ export class VizSpecStore { this.limit > 0 ? this.limit : undefined ); } - + public setComputationFunction(f: IComputationFunction) { this.computationFuction = f; } diff --git a/packages/graphic-walker/src/vis/spec/aggregate.ts b/packages/graphic-walker/src/vis/spec/aggregate.ts index 2fa131f9..76a6d9a3 100644 --- a/packages/graphic-walker/src/vis/spec/aggregate.ts +++ b/packages/graphic-walker/src/vis/spec/aggregate.ts @@ -4,11 +4,12 @@ import { getMeaAggKey } from '../../utils'; export function channelAggregate(encoding: { [key: string]: any }, fields: IViewField[]) { Object.values(encoding).forEach((c) => { - const targetField = fields.find((f) => f.fid === c.field && !('aggregate' in c)); + if (c.aggregate === null) return; + const targetField = fields.find((f) => f.fid === c.field && (f.analyticType === 'measure' || f.fid === COUNT_FIELD_ID)); if (targetField && targetField.fid === COUNT_FIELD_ID) { c.title = 'Count'; c.field = getMeaAggKey(targetField.fid, targetField.aggName) - } else if (targetField && targetField.analyticType === 'measure') { + } else if (targetField) { c.title = `${targetField.aggName}(${targetField.name})`; c.field = getMeaAggKey(targetField.fid, targetField.aggName) } diff --git a/packages/graphic-walker/src/vis/spec/stack.ts b/packages/graphic-walker/src/vis/spec/stack.ts index f45526b0..b585319d 100644 --- a/packages/graphic-walker/src/vis/spec/stack.ts +++ b/packages/graphic-walker/src/vis/spec/stack.ts @@ -1,11 +1,12 @@ import { IStackMode } from "../../interfaces"; export function channelStack(encoding: { [key: string]: any }, stackMode: IStackMode) { - if (stackMode === 'stack') return; - if (encoding.x && encoding.x.type === 'quantitative') { - encoding.x.stack = stackMode === 'none' ? null : 'normalize'; - } - if (encoding.y && encoding.y.type === 'quantitative') { - encoding.y.stack = stackMode === 'none' ? null : 'normalize'; + if (stackMode === 'stack' || stackMode === 'zero') return; + let stackValue = stackMode === 'none' ? null : stackMode; + const stackableChannels = ['x', 'y', 'theta', 'radius']; + for (let ch of stackableChannels) { + if (encoding[ch] && encoding[ch].type === 'quantitative') { + encoding[ch].stack = stackValue + } } } diff --git a/packages/graphic-walker/src/visualSettings/index.tsx b/packages/graphic-walker/src/visualSettings/index.tsx index eb9584e0..f4286757 100644 --- a/packages/graphic-walker/src/visualSettings/index.tsx +++ b/packages/graphic-walker/src/visualSettings/index.tsx @@ -367,6 +367,7 @@ const VisualSettings: React.FC = ({ none: XMarkIcon, stack: ChevronDoubleUpIcon, normalize: ArrowsUpDownIcon, + center: ChevronUpDownIcon, // TODO: fix unsafe extends }[g], })), value: stack,