From 382c7d1cddd9fee299d1980f7631240133265bb6 Mon Sep 17 00:00:00 2001 From: islxyqwe Date: Thu, 19 Oct 2023 06:08:30 +0800 Subject: [PATCH] fix: facet issue (#187) * fix: enter visSegment after import csv * fix: react-vega render deps * fix: properities * fix: default segment * chore: tabs behaivor * fix: pivottable reactive * fix: chart name idx * fix: props * fix: ux * fix: fold menu z-index * fix: add channel limit * chore: change getTemporalRange * fix: hook deps * fix: menu in dark mode --- packages/graphic-walker/package.json | 3 +- packages/graphic-walker/src/App.tsx | 74 +++++++++---- packages/graphic-walker/src/FullApp.tsx | 10 +- .../src/components/askViz/index.tsx | 2 +- .../src/components/codeExport/index.tsx | 2 +- .../src/components/pivotTable/index.tsx | 7 +- .../src/components/selectContext/index.tsx | 5 +- .../src/components/tabs/editableTab.tsx | 103 +++++++++--------- .../src/components/visualConfig/index.tsx | 15 +-- .../graphic-walker/src/computation/index.ts | 2 +- .../src/dataSource/dataSelection/csvData.tsx | 4 +- .../src/fields/aestheticFields.tsx | 2 +- .../graphic-walker/src/fields/components.tsx | 1 - .../encodeFields/singleEncodeEditor.tsx | 8 +- .../fields/filterField/filterEditDialog.tsx | 2 +- .../src/fields/posFields/index.tsx | 5 +- packages/graphic-walker/src/index.tsx | 24 ++-- .../src/models/visSpecHistory.ts | 12 +- packages/graphic-walker/src/renderer/hooks.ts | 2 +- .../graphic-walker/src/renderer/index.tsx | 43 ++++---- .../src/renderer/specRenderer.tsx | 10 +- .../src/segments/segmentNav.tsx | 2 +- .../graphic-walker/src/segments/visNav.tsx | 8 +- .../graphic-walker/src/store/dataStore.ts | 5 +- packages/graphic-walker/src/store/index.tsx | 6 +- .../src/store/visualSpecStore.ts | 25 +++-- packages/graphic-walker/src/vanilla.tsx | 6 +- .../graphic-walker/src/vis/react-vega.tsx | 32 +++--- packages/graphic-walker/src/vis/theme.ts | 2 +- yarn.lock | 67 +++++++++++- 30 files changed, 293 insertions(+), 196 deletions(-) diff --git a/packages/graphic-walker/package.json b/packages/graphic-walker/package.json index d0f5cec3..267912dc 100644 --- a/packages/graphic-walker/package.json +++ b/packages/graphic-walker/package.json @@ -35,6 +35,7 @@ }, "types": "./dist/index.d.ts", "dependencies": { + "@headlessui-float/react": "^0.11.4", "@headlessui/react": "^1.7.12", "@heroicons/react": "^2.0.8", "@kanaries/react-beautiful-dnd": "^0.0.3", @@ -65,8 +66,8 @@ "react-shadow": "^20.0.0", "rxjs": "^7.3.0", "tailwindcss": "^3.2.4", - "ts-jest": "^29.1.1", "topojson-client": "^3.1.0", + "ts-jest": "^29.1.1", "vega": "^5.22.1", "vega-embed": "^6.21.0", "vega-lite": "^5.6.0" diff --git a/packages/graphic-walker/src/App.tsx b/packages/graphic-walker/src/App.tsx index 015d3c24..28caf60d 100644 --- a/packages/graphic-walker/src/App.tsx +++ b/packages/graphic-walker/src/App.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { useTranslation } from 'react-i18next'; import { LightBulbIcon } from "@heroicons/react/24/outline"; -import { IGeographicData, IComputationFunction, ISegmentKey, IThemeKey, IMutField, IGeoDataItem, VegaGlobalConfig, IChannelScales } from './interfaces'; +import { IGeographicData, IComputationFunction, ISegmentKey, IThemeKey, IMutField, IGeoDataItem, VegaGlobalConfig, IChannelScales, Specification, IDarkMode } from './interfaces'; import type { IReactVegaHandler } from './vis/react-vega'; import VisualSettings from './visualSettings'; import PosFields from './fields/posFields'; @@ -22,7 +22,7 @@ import GeoConfigPanel from './components/leafletRenderer/geoConfigPanel'; import type { ToolbarItemProps } from './components/toolbar'; import ClickMenu from './components/clickMenu'; import AskViz from './components/askViz'; -import { VizSpecStore } from './store/visualSpecStore'; +import { VizSpecStore, renderSpec } from './store/visualSpecStore'; import FieldsContextWrapper from './fields/fieldsContext'; import { guardDataKeys } from './utils/dataPrep'; import { getComputation } from './computation/clientComputation'; @@ -32,6 +32,7 @@ import { ErrorContext } from './utils/reportError'; import { ErrorBoundary } from 'react-error-boundary'; import Errorpanel from './components/errorpanel'; import { GWGlobalConfig } from './vis/theme'; +import { useCurrentMediaTheme } from './utils/media'; export interface BaseVizProps { i18nLang?: string; @@ -58,6 +59,7 @@ export interface BaseVizProps { onError?: (err: Error) => void; geoList?: IGeoDataItem[]; channelScales?: IChannelScales; + spec?: Specification; } export const VizApp = observer(function VizApp(props: BaseVizProps) { @@ -72,6 +74,8 @@ export const VizApp = observer(function VizApp(props: BaseVizProps) { toolbar, geographicData, computationTimeout = 60000, + spec, + onError } = props; const { t, i18n } = useTranslation(); @@ -92,10 +96,16 @@ export const VizApp = observer(function VizApp(props: BaseVizProps) { const vizStore = useVizStore(); useEffect(() => { - if (props.geographicData) { - vizStore.setGeographicData(props.geographicData, props.geographicData.key); + if (geographicData) { + vizStore.setGeographicData(geographicData, geographicData.key); } - }, [geographicData]); + }, [vizStore, geographicData]); + + useEffect(() => { + if (spec) { + vizStore.replaceNow(renderSpec(spec, vizStore.meta, vizStore.currentVis.name ?? 'Chart 1', vizStore.currentVis.visId)); + } + }, [spec, vizStore]); const rendererRef = useRef(null); @@ -105,12 +115,12 @@ export const VizApp = observer(function VizApp(props: BaseVizProps) { (msg: string, code?: number) => { const err = new Error(`Error${code ? `(${code})` : ''}: ${msg}`); console.error(err); - props.onError?.(err); + onError?.(err); if (code) { vizStore.updateShowErrorResolutionPanel(code); } }, - [vizStore, props.onError] + [vizStore, onError] ); const { segmentKey, vizEmbededMenu } = vizStore; @@ -215,12 +225,16 @@ export const VizApp = observer(function VizApp(props: BaseVizProps) { export type VizProps = { i18nLang?: string; i18nResources?: { [lang: string]: Record }; + themeConfig?: GWGlobalConfig; themeKey?: IThemeKey; - darkMode?: 'light' | 'dark'; + dark?: IDarkMode; toolbar?: { extra?: ToolbarItemProps[]; exclude?: string[]; }; + geographicData?: IGeographicData & { + key: string; + }; enhanceAPI?: { header?: Record; features?: { @@ -232,6 +246,11 @@ export type VizProps = { rawFields: IMutField[]; onMetaChange?: (fid: string, meta: Partial) => void; computationTimeout?: number; + dataSelection?: React.ReactChild; + onError?: (err: Error) => void; + geoList?: IGeoDataItem[]; + channelScales?: IChannelScales; + spec?: Specification; } & ( | { /** @@ -268,20 +287,31 @@ export function VizAppWithContext(props: VizProps) { }; }, [props.rawFields, props.dataSource ? props.dataSource : props.computation, props.fieldKeyGuard]); + const darkMode = useCurrentMediaTheme(props.dark); + return ( - - - - - +
+ + + + + +
); } diff --git a/packages/graphic-walker/src/FullApp.tsx b/packages/graphic-walker/src/FullApp.tsx index bae8a3a2..863901df 100644 --- a/packages/graphic-walker/src/FullApp.tsx +++ b/packages/graphic-walker/src/FullApp.tsx @@ -8,14 +8,15 @@ import type { ToolbarItemProps } from './components/toolbar'; import { VizApp } from './App'; import { CommonStore } from './store/commonStore'; import FieldsContextWrapper from './fields/fieldsContext'; +import { GWGlobalConfig } from './vis/theme'; export interface IGWProps { - hideDataSourceConfig?: boolean; i18nLang?: string; i18nResources?: { [lang: string]: Record }; keepAlive?: boolean | string; /** @default "vega" */ themeKey?: IThemeKey; + themeConfig?: GWGlobalConfig; dark?: IDarkMode; toolbar?: { extra?: ToolbarItemProps[]; @@ -37,7 +38,7 @@ export interface IGWProps { } export const AppContent = observer>(function App(props) { - const { i18nLang = 'en-US', i18nResources, themeKey = 'vega', dark = 'media', toolbar, enhanceAPI } = props; + const { i18nLang = 'en-US', i18nResources, themeKey = 'vega', dark = 'media', themeConfig, toolbar, enhanceAPI, geographicData, onError, geoList, channelScales } = props; const commonStore = useGlobalStore(); const { dataStore } = commonStore; @@ -56,7 +57,12 @@ export const AppContent = observer>(fun i18nLang={i18nLang} i18nResources={i18nResources} themeKey={themeKey} + themeConfig={themeConfig} toolbar={toolbar} + geographicData={geographicData} + geoList={geoList} + onError={onError} + channelScales={channelScales} /> diff --git a/packages/graphic-walker/src/components/askViz/index.tsx b/packages/graphic-walker/src/components/askViz/index.tsx index e80af259..40bb7442 100644 --- a/packages/graphic-walker/src/components/askViz/index.tsx +++ b/packages/graphic-walker/src/components/askViz/index.tsx @@ -57,7 +57,7 @@ const AskViz: React.FC<{ api?: string; headers?: Record }> = (pr .finally(() => { setLoading(false); }); - }, [query, allFields]); + }, [props.api, props.headers, allFields, query, vizStore]); return (
{ setCode("vega code"); } } - }, [tabKey, showCodeExportPanel]); + }, [tabKey, showCodeExportPanel, vizStore]); return ( = function PivotTableComponent(props) { +const PivotTable: React.FC = observer(function PivotTableComponent(props) { const { data, visualConfig, loading, layout, draggableFieldState } = props; const computation = useCompututaion(); const appRef = useAppRootContext(); @@ -85,7 +86,7 @@ const PivotTable: React.FC = function PivotTableComponent(props } else { aggregateThenGenerate(); } - }, [data, enableCollapse]); + }, [data, enableCollapse, vizStore]); useEffect(() => { if (!enableCollapse || showTableSummary) { @@ -233,6 +234,6 @@ const PivotTable: React.FC = function PivotTableComponent(props
); -}; +}); export default PivotTable; diff --git a/packages/graphic-walker/src/components/selectContext/index.tsx b/packages/graphic-walker/src/components/selectContext/index.tsx index 8a99450b..492506af 100644 --- a/packages/graphic-walker/src/components/selectContext/index.tsx +++ b/packages/graphic-walker/src/components/selectContext/index.tsx @@ -1,6 +1,7 @@ import React, { Fragment, useEffect, useRef, useState } from 'react'; import { Listbox, Transition } from '@headlessui/react'; import { CheckIcon, Cog6ToothIcon } from '@heroicons/react/24/outline'; +import { Float } from '@headlessui-float/react'; export interface ISelectContextOption { key: string; @@ -43,7 +44,7 @@ const SelectContext: React.FC = (props) => { return ( -
+
{props.children} @@ -85,7 +86,7 @@ const SelectContext: React.FC = (props) => { ))} -
+
); }; diff --git a/packages/graphic-walker/src/components/tabs/editableTab.tsx b/packages/graphic-walker/src/components/tabs/editableTab.tsx index 7c775387..9c6504b5 100644 --- a/packages/graphic-walker/src/components/tabs/editableTab.tsx +++ b/packages/graphic-walker/src/components/tabs/editableTab.tsx @@ -27,66 +27,69 @@ interface EditableTabsProps { onRemove?: (index: number) => void; } -const Slider = (props: { className?: string; children: React.ReactNode; safeDistance: number }) => { +const Slider = (props: { className?: string; children: React.ReactNode }) => { const [x, setX] = useState(0); const ref = useRef(); + const parentDisposeRef = useRef<() => void>(); const childDisposeRef = useRef<() => void>(); - const onWheel = useCallback( - (e: WheelEvent) => { - e.preventDefault(); - e.stopPropagation(); - const rect = ref.current?.children[0]?.getBoundingClientRect(); - if (rect) { - setX((x) => Math.min(rect.width - props.safeDistance, Math.max(0, x + e.deltaY))); - } - return false; - }, - [props.safeDistance] - ); + const onWheel = useCallback((e: WheelEvent) => { + e.preventDefault(); + e.stopPropagation(); + const parentWidth = ref.current?.getBoundingClientRect().width; + const childWidth = ref.current?.children[0]?.getBoundingClientRect().width; + if (parentWidth && childWidth) { + setX((x) => Math.min(Math.max(0, childWidth - parentWidth), Math.max(0, x + e.deltaY + e.deltaX))); + } + return false; + }, []); const refCB = useCallback( (node) => { + parentDisposeRef.current?.(); if (node == null) { - if (ref.current != null) { - ref.current.removeEventListener('wheel', onWheel); - } - return; - } - ref.current = node; - node.addEventListener('wheel', onWheel, { passive: false }); - }, - [onWheel] - ); - - const onResize = useCallback( - (rect) => { - setX((x) => Math.min(x, rect.width - props.safeDistance)); - }, - [props.safeDistance] - ); - - const childRefCB = useCallback( - (node) => { - if (node === null) { - if (childDisposeRef.current != null) { - childDisposeRef.current(); - } return; } const r = new ResizeObserver((es) => { for (const e of es) { - onResize(e.contentRect); + const parentWidth = e.contentRect.width; + const childWidth = ref.current?.children[0]?.getBoundingClientRect().width; + if (parentWidth && childWidth) { + setX((x) => Math.min(Math.max(0, childWidth - parentWidth), x)); + } } }); - childDisposeRef.current = () => { + r.observe(node); + ref.current = node; + node.addEventListener('wheel', onWheel, { passive: false }); + parentDisposeRef.current = () => { r.disconnect(); + ref.current?.removeEventListener('wheel', onWheel); }; - r.observe(node); }, - [onResize] + [onWheel] ); + const childRefCB = useCallback((node: HTMLDivElement) => { + childDisposeRef.current?.(); + if (node === null) { + return; + } + const r = new ResizeObserver((es) => { + for (const e of es) { + const parentWidth = ref.current?.getBoundingClientRect().width; + const childWidth = e.contentRect.width; + if (parentWidth && childWidth) { + setX((x) => Math.min(Math.max(0, childWidth - parentWidth), x)); + } + } + }); + childDisposeRef.current = () => { + r.disconnect(); + }; + r.observe(node); + }, []); + return (
- +