From 631322a5a288d6cd906d87d6c4362709581adc6e Mon Sep 17 00:00:00 2001 From: islxyqwe Date: Sun, 29 Sep 2024 15:45:12 +0800 Subject: [PATCH 1/2] fix: type --- packages/graphic-walker/src/store/visualSpecStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index d1e1be7f..52e6368b 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -302,7 +302,7 @@ export class VizSpecStore { get paintInfo() { const existPaintField = this.currentEncodings.dimensions.find((x) => x.fid === PAINT_FIELD_ID); if (existPaintField) { - const param: IPaintMap = existPaintField.expression?.params.find((x) => x.type === 'map')?.value; + const param = existPaintField.expression?.params.find((x) => x.type === 'map')?.value; if (param) { return { type: 'exist', @@ -310,7 +310,7 @@ export class VizSpecStore { new: this.paintFields, } as const; } - const paramV2: IPaintMapV2 = existPaintField.expression?.params.find((x) => x.type === 'newmap')?.value; + const paramV2 = existPaintField.expression?.params.find((x) => x.type === 'newmap')?.value; if (paramV2) { return { type: 'exist', From 95b538c97d5c9af86cc2d2815a28e99f3160a2df Mon Sep 17 00:00:00 2001 From: islxyqwe Date: Sun, 29 Sep 2024 18:48:02 +0800 Subject: [PATCH 2/2] feat: setting scales --- .../src/components/leafletRenderer/index.tsx | 25 +- .../src/components/rangeslider.tsx | 2 +- .../src/components/ui/dialog.tsx | 87 ++- .../src/components/ui/number-input.tsx | 32 + .../src/components/visualConfig/index.tsx | 686 +++++++++++------- .../components/visualConfig/range-scale.tsx | 113 ++- packages/graphic-walker/src/interfaces.ts | 23 +- .../graphic-walker/src/locales/en-US.json | 5 + .../graphic-walker/src/vis/react-vega.tsx | 30 +- packages/graphic-walker/src/vis/spec/view.ts | 15 +- 10 files changed, 672 insertions(+), 346 deletions(-) create mode 100644 packages/graphic-walker/src/components/ui/number-input.tsx diff --git a/packages/graphic-walker/src/components/leafletRenderer/index.tsx b/packages/graphic-walker/src/components/leafletRenderer/index.tsx index 5029dc6e..b9cb8097 100644 --- a/packages/graphic-walker/src/components/leafletRenderer/index.tsx +++ b/packages/graphic-walker/src/components/leafletRenderer/index.tsx @@ -1,5 +1,5 @@ import React, { forwardRef, useMemo } from 'react'; -import type { DraggableFieldState, IChannelScales, IConfigScale, IRow, IVisualConfigNew, IVisualLayout, VegaGlobalConfig } from '../../interfaces'; +import type { DraggableFieldState, IChannelScales, IConfigScaleSet, IRow, IVisualConfigNew, IVisualLayout, VegaGlobalConfig } from '../../interfaces'; import POIRenderer from './POIRenderer'; import ChoroplethRenderer from './ChoroplethRenderer'; @@ -11,10 +11,7 @@ export interface ILeafletRendererProps { visualLayout: IVisualLayout; data: IRow[]; scales?: IChannelScales; - scale?: { - opacity: IConfigScale; - size: IConfigScale; - }; + scale?: IConfigScaleSet; } export interface ILeafletRendererRef {} @@ -48,17 +45,13 @@ const LeafletRenderer = forwardRef(f const longitude = useMemo(() => lng ?? lngField, [lng, lngField]); const scales = useMemo(() => { const cs = channelScaleRaw ?? {}; - if (scale?.opacity) { - cs.opacity = { - ...(cs.opacity ?? {}), - ...scale.opacity, - }; - } - if (scale?.size) { - cs.size = { - ...(cs.size ?? {}), - ...scale.size, - }; + if (scale) { + for (const key of Object.keys(scale)) { + cs[key] = { + ...(cs[key] ?? {}), + ...scale[key], + }; + } } return cs; }, [channelScaleRaw, scale]); diff --git a/packages/graphic-walker/src/components/rangeslider.tsx b/packages/graphic-walker/src/components/rangeslider.tsx index 325a0580..6758fa96 100644 --- a/packages/graphic-walker/src/components/rangeslider.tsx +++ b/packages/graphic-walker/src/components/rangeslider.tsx @@ -8,7 +8,7 @@ const Slider = React.forwardRef, R diff --git a/packages/graphic-walker/src/components/ui/dialog.tsx b/packages/graphic-walker/src/components/ui/dialog.tsx index 02635a0f..50551929 100644 --- a/packages/graphic-walker/src/components/ui/dialog.tsx +++ b/packages/graphic-walker/src/components/ui/dialog.tsx @@ -28,29 +28,60 @@ const DialogOverlay = React.forwardRef, React.ComponentPropsWithoutRef>( - ({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - - ) -); +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + containerClassName?: string; + } +>(({ className, containerClassName, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); + DialogContent.displayName = DialogPrimitive.Content.displayName; +const DialogNormalContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + containerClassName?: string; + } +>(({ className, containerClassName, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); + +DialogNormalContent.displayName = DialogPrimitive.Content.displayName; + const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
); @@ -74,4 +105,16 @@ const DialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ); DialogDescription.displayName = DialogPrimitive.Description.displayName; -export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogNormalContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/packages/graphic-walker/src/components/ui/number-input.tsx b/packages/graphic-walker/src/components/ui/number-input.tsx new file mode 100644 index 00000000..0257c3fc --- /dev/null +++ b/packages/graphic-walker/src/components/ui/number-input.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import { cn } from '@/utils'; +import { useRefControledState } from '@/hooks'; + +export interface InputProps extends React.InputHTMLAttributes {} + +const NumberInput = React.forwardRef(({ className, type, value, onChange, ...props }, ref) => { + const [uncontrolledValue, setUncontrolledValue] = useRefControledState(value); + + return ( + { + setUncontrolledValue(e.target.value); + }} + onBlur={(e) => { + onChange?.(e); + }} + {...props} + /> + ); +}); +NumberInput.displayName = 'NumberInput'; + +export { NumberInput }; diff --git a/packages/graphic-walker/src/components/visualConfig/index.tsx b/packages/graphic-walker/src/components/visualConfig/index.tsx index 51ad1224..7543b6bc 100644 --- a/packages/graphic-walker/src/components/visualConfig/index.tsx +++ b/packages/graphic-walker/src/components/visualConfig/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; -import { runInAction } from 'mobx'; +import { runInAction, set } from 'mobx'; import { useTranslation } from 'react-i18next'; import { useVizStore } from '../../store'; @@ -8,20 +8,63 @@ import { GLOBAL_CONFIG } from '../../config'; import { IConfigScale, IVisualConfig, IVisualLayout } from '../../interfaces'; import Toggle from '../toggle'; import { ColorSchemes, extractRGBA } from './colorScheme'; -import { RangeScale } from './range-scale'; +import { DomainScale, RangeScale } from './range-scale'; import { ConfigItemContainer, ConfigItemContent, ConfigItemHeader, ConfigItemTitle } from './config-item'; import { KVTuple } from '../../models/utils'; import { isNotEmpty } from '../../utils'; import { timezones } from './timezone'; import { Input } from '../ui/input'; import { Button } from '../ui/button'; -import { Dialog, DialogContent, DialogFooter } from '../ui/dialog'; +import { Dialog, DialogContent, DialogFooter, DialogNormalContent } from '../ui/dialog'; import Combobox from '../dropdownSelect/combobox'; import { StyledPicker } from '../color-picker'; import { ErrorBoundary } from 'react-error-boundary'; +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu'; const DEFAULT_COLOR_SCHEME = ['#5B8FF9', '#FF6900', '#FCB900', '#7BDCB5', '#00D084', '#8ED1FC', '#0693E3', '#ABB8C3', '#EB144C', '#F78DA7', '#9900EF']; +function useDomainScale() { + const [enableMinDomain, setEnableMinDomain] = useState(false); + const [enableMaxDomain, setEnableMaxDomain] = useState(false); + const [domainMin, setDomainMin] = useState(0); + const [domainMax, setDomainMax] = useState(100); + const [enableType, setEnableType] = useState(false); + const [type, setType] = useState<'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'>('linear'); + const setValue = useCallback((value: IConfigScale) => { + setEnableMaxDomain(isNotEmpty(value.domainMax)); + setEnableMinDomain(isNotEmpty(value.domainMin)); + setEnableType(isNotEmpty(value.type)); + setDomainMin(value.domainMin ?? 0); + setDomainMax(value.domainMax ?? 100); + setType(value.type ?? 'linear'); + }, []); + + const value = useMemo( + () => ({ + ...(enableType ? { type } : {}), + ...(enableMaxDomain ? { domainMax } : {}), + ...(enableMinDomain ? { domainMin } : {}), + }), + [enableMaxDomain && domainMax, enableMinDomain && domainMin, enableType && type] + ); + return { + value, + setValue, + enableMaxDomain, + enableMinDomain, + domainMax, + domainMin, + setEnableMinDomain, + setEnableMaxDomain, + setDomainMin, + setDomainMax, + enableType, + type, + setType, + setEnableType, + }; +} + function useScale(minRange: number, maxRange: number, defaultMinRange?: number, defaultMaxRange?: number) { const [enableMinDomain, setEnableMinDomain] = useState(false); const [enableMaxDomain, setEnableMaxDomain] = useState(false); @@ -30,29 +73,39 @@ function useScale(minRange: number, maxRange: number, defaultMinRange?: number, const [domainMax, setDomainMax] = useState(100); const [rangeMin, setRangeMin] = useState(defaultMinRange ?? minRange); const [rangeMax, setRangeMax] = useState(defaultMaxRange ?? maxRange); + const [enableType, setEnableType] = useState(false); + const [type, setType] = useState<'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'>('linear'); + const setValue = useCallback( (value: IConfigScale) => { setEnableMaxDomain(isNotEmpty(value.domainMax)); setEnableMinDomain(isNotEmpty(value.domainMin)); setEnableRange(isNotEmpty(value.rangeMax) || isNotEmpty(value.rangeMin)); + setEnableType(isNotEmpty(value.type)); setDomainMin(value.domainMin ?? 0); setDomainMax(value.domainMax ?? 100); setRangeMax(value.rangeMax ?? defaultMaxRange ?? maxRange); setRangeMin(value.rangeMin ?? defaultMinRange ?? minRange); + setType(value.type ?? 'linear'); }, [defaultMaxRange, defaultMinRange, maxRange, minRange] ); const value = useMemo( () => ({ + ...(enableType ? { type } : {}), ...(enableMaxDomain ? { domainMax } : {}), ...(enableMinDomain ? { domainMin } : {}), ...(enableRange ? { rangeMax, rangeMin } : {}), }), - [enableMaxDomain && domainMax, enableMinDomain && domainMin, enableRange && rangeMax, enableRange && rangeMin] + [enableMaxDomain && domainMax, enableMinDomain && domainMin, enableRange && rangeMax, enableRange && rangeMin, enableType && type] ); return { + type, + setType, + enableType, + setEnableType, value, setValue, enableMaxDomain, @@ -103,8 +156,15 @@ const VisualConfigPanel: React.FC = () => { const [displayOffset, setDisplayOffset] = useState(undefined); const [displayOffsetEdited, setDisplayOffsetEdited] = useState(false); + const [enabledScales, setEnabledScales] = useState([]); + const columnValue = useDomainScale(); + const rowValue = useDomainScale(); + const colorValue = useDomainScale(); + const thetaValue = useDomainScale(); + const radiusValue = useDomainScale(); const opacityValue = useScale(0, 1, 0.3, 0.8); const sizeValue = useScale(0, 100); + const scalesSet = new Set(enabledScales); useEffect(() => { setZeroScale(layout.zeroScale); @@ -120,6 +180,15 @@ const VisualConfigPanel: React.FC = () => { normalizedNumberFormat: layout.format.normalizedNumberFormat, }); setColorPalette(layout.colorPalette ?? ''); + const enabledScales = Object.entries(layout.scale ?? {}) + .filter(([_k, scale]) => Object.entries(scale).filter(([_k, v]) => !!v).length > 0) + .map(([k]) => k); + setEnabledScales(enabledScales.length === 0 ? ['opacity', 'size'] : enabledScales); + columnValue.setValue(layout.scale?.column ?? {}); + rowValue.setValue(layout.scale?.row ?? {}); + colorValue.setValue(layout.scale?.color ?? {}); + thetaValue.setValue(layout.scale?.theta ?? {}); + radiusValue.setValue(layout.scale?.radius ?? {}); opacityValue.setValue(layout.scale?.opacity ?? {}); sizeValue.setValue(layout.scale?.size ?? {}); setGeoMapTileUrl(layout.geoMapTileUrl); @@ -134,296 +203,358 @@ const VisualConfigPanel: React.FC = () => { vizStore.setShowVisualConfigPanel(false); }} > - -
{ - setDisplayColorPicker(false); - }} - > - - - Colors - - -
-
- - + { + setDisplayColorPicker(false); + }} + > +
+
+ + + Colors + + +
+
+ + +
+ { + setColorEdited(true); + setDefaultColor((x) => ({ ...x, r: Number(e.target.value) })); + }} + /> + { + setColorEdited(true); + setDefaultColor((x) => ({ ...x, g: Number(e.target.value) })); + }} + /> + { + setColorEdited(true); + setDefaultColor((x) => ({ ...x, b: Number(e.target.value) })); + }} + /> +
+ } + > +
{ + e.stopPropagation(); + e.preventDefault(); + }} + >
- { - setColorEdited(true); - setDefaultColor((x) => ({ ...x, r: Number(e.target.value) })); + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + setDisplayColorPicker(true); }} - /> - { - setColorEdited(true); - setDefaultColor((x) => ({ ...x, g: Number(e.target.value) })); + >
+
+ {displayColorPicker && ( + { + setColorEdited(true); + setDefaultColor({ + ...color.rgb, + a: color.rgb.a ?? 1, + }); + }} + /> + )} +
+
+
+
+
+ + ({ + value: scheme.name, + label: ( + <> +
+
{scheme.name}
+
+ {scheme.value.map((c, index) => { + return
; + })} +
+
+ + ), + })).concat({ + value: '_none', + label: <>{t('config.default_color_palette')}, + })} + /> +
+
+ + { + setBackground(e.target.value); + }} + /> +
+
+
+
+ + +
+ Scale + + + + + + {['row', 'column', 'color', 'theta', 'radius', 'opacity', 'size'].map((scale) => ( + { + if (e) { + setEnabledScales((s) => [...s, scale]); + } else { + setEnabledScales((s) => s.filter((x) => x !== scale)); + } }} - /> + > + {t(`config.${scale}`)} + + ))} + + +
+
+ + {scalesSet.has('column') && ( +
+ + +
+ )} + {scalesSet.has('row') && ( +
+ + +
+ )} + {scalesSet.has('color') && ( +
+ + +
+ )} + {scalesSet.has('theta') && ( +
+ + +
+ )} + {scalesSet.has('radius') && ( +
+ + +
+ )} + {scalesSet.has('opacity') && ( +
+ + +
+ )} + {scalesSet.has('size') && ( +
+ + +
+ )} +
+
+ + + {t('config.format')} +

+ {t(`config.formatGuidesDocs`)}:{' '} + + {t(`config.readHere`)} + +

+
+ +
+ {formatConfigList.map((fc) => ( +
+ +
{ - setColorEdited(true); - setDefaultColor((x) => ({ ...x, b: Number(e.target.value) })); + setFormat((f) => ({ + ...f, + [fc]: e.target.value, + })); }} />
- } - > -
{ - e.stopPropagation(); - e.preventDefault(); - }} - > -
{ - e.stopPropagation(); - e.preventDefault(); - setDisplayColorPicker(true); - }} - >
-
- {displayColorPicker && ( - { - setColorEdited(true); - setDefaultColor({ - ...color.rgb, - a: color.rgb.a ?? 1, - }); - }} - /> - )} -
- -
-
- - ({ - value: scheme.name, - label: ( - <> -
-
{scheme.name}
-
- {scheme.value.map((c, index) => { - return
; - })} -
-
- - ), - })).concat({ - value: '_none', - label: <>{t('config.default_color_palette')}, - })} - /> + ))}
-
- - { - setBackground(e.target.value); - }} - /> + + + + + {t('config.independence')} + + +
+ {GLOBAL_CONFIG.POSITION_CHANNEL_CONFIG_LIST.map((pc) => ( + { + setResolve((r) => ({ + ...r, + [pc]: e, + })); + }} + /> + ))} + {GLOBAL_CONFIG.NON_POSITION_CHANNEL_CONFIG_LIST.map((npc) => ( + { + setResolve((r) => ({ + ...r, + [npc]: e, + })); + }} + /> + ))}
-
- - - - - Scale - - -
- - -
-
- - -
-
-
- - - {t('config.format')} -

- {t(`config.formatGuidesDocs`)}:{' '} - - {t(`config.readHere`)} - -

-
- -
- {formatConfigList.map((fc) => ( -
- -
+ + + + + {t('config.misc')} + + +
+
+ { + setGeoMapTileUrl(e ? 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' : undefined); + }} + /> + {isNotEmpty(geoMapTileUrl) && ( { - setFormat((f) => ({ - ...f, - [fc]: e.target.value, - })); + setGeoMapTileUrl(e.target.value); }} /> -
+ )}
- ))} -
- - - - - {t('config.independence')} - - -
- {GLOBAL_CONFIG.POSITION_CHANNEL_CONFIG_LIST.map((pc) => ( - { - setResolve((r) => ({ - ...r, - [pc]: e, - })); - }} - /> - ))} - {GLOBAL_CONFIG.NON_POSITION_CHANNEL_CONFIG_LIST.map((npc) => ( - { - setResolve((r) => ({ - ...r, - [npc]: e, - })); - }} - /> - ))} -
-
-
- - - {t('config.misc')} - - -
-
- { - setGeoMapTileUrl(e ? 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' : undefined); - }} - /> - {isNotEmpty(geoMapTileUrl) && ( - { - setGeoMapTileUrl(e.target.value); +
+ { + setZeroScale(en); }} /> - )} -
-
- { - setZeroScale(en); - }} - /> - { - setSvg(en); - }} - /> - { - setScaleIncludeUnmatchedChoropleth(en); - }} - /> - { - setShowAllGeoshapeInChoropleth(en); - }} - /> -
-
- { - setDisplayOffsetEdited(true); - setDisplayOffset(e ? new Date().getTimezoneOffset() : undefined); - }} - /> - {isNotEmpty(displayOffset) && ( - { + { + setSvg(en); + }} + /> + { + setScaleIncludeUnmatchedChoropleth(en); + }} + /> + { + setShowAllGeoshapeInChoropleth(en); + }} + /> +
+
+ { setDisplayOffsetEdited(true); - setDisplayOffset(parseInt(e)); + setDisplayOffset(e ? new Date().getTimezoneOffset() : undefined); }} - options={timezones.map((tz) => ({ - value: `${tz.value}`, - label: {tz.name}, - }))} /> - )} + {isNotEmpty(displayOffset) && ( + { + setDisplayOffsetEdited(true); + setDisplayOffset(parseInt(e)); + }} + options={timezones.map((tz) => ({ + value: `${tz.value}`, + label: {tz.name}, + }))} + /> + )} +
-
-
-
- - + + +
+
- + ); }; diff --git a/packages/graphic-walker/src/components/visualConfig/range-scale.tsx b/packages/graphic-walker/src/components/visualConfig/range-scale.tsx index 420aefec..3fd2ab42 100644 --- a/packages/graphic-walker/src/components/visualConfig/range-scale.tsx +++ b/packages/graphic-walker/src/components/visualConfig/range-scale.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Slider } from '../../components/rangeslider'; import { Checkbox } from '../ui/checkbox'; -import { Input } from '../ui/input'; import { Label } from '../ui/label'; +import { NumberInput } from '../ui/number-input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; export function RangeScale(props: { text: string; @@ -12,28 +13,52 @@ export function RangeScale(props: { enableMaxDomain: boolean; enableMinDomain: boolean; enableRange: boolean; + enableType: boolean; rangeMax: number; rangeMin: number; domainMax: number; domainMin: number; + type: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'; setEnableMinDomain: (v: boolean) => void; setEnableMaxDomain: (v: boolean) => void; setEnableRange: (v: boolean) => void; + setEnableType: (v: boolean) => void; setDomainMin: (v: number) => void; setDomainMax: (v: number) => void; setRangeMin: (v: number) => void; setRangeMax: (v: number) => void; + setType: (v: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog') => void; }) { const { t } = useTranslation(); return ( -
+
+
+
+ + +
+ +
+
- { const v = Number(e.target.value); @@ -50,7 +75,8 @@ export function RangeScale(props: {
- { const v = Number(e.target.value); @@ -69,6 +95,7 @@ export function RangeScale(props: {
); } + +export function DomainScale(props: { + text: string; + enableMaxDomain: boolean; + enableMinDomain: boolean; + enableType: boolean; + domainMax: number; + domainMin: number; + type: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'; + setEnableMinDomain: (v: boolean) => void; + setEnableMaxDomain: (v: boolean) => void; + setEnableType: (v: boolean) => void; + setDomainMin: (v: number) => void; + setDomainMax: (v: number) => void; + setType: (v: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog') => void; +}) { + const { t } = useTranslation(); + + return ( +
+
+
+ + +
+ +
+ +
+
+ + +
+ { + const v = Number(e.target.value); + if (!isNaN(v)) { + props.setDomainMin(v); + } + }} + type="number" + disabled={!props.enableMinDomain} + /> +
+
+
+ + +
+ { + const v = Number(e.target.value); + if (!isNaN(v)) { + props.setDomainMax(v); + } + }} + type="number" + disabled={!props.enableMaxDomain} + /> +
+
+ ); +} diff --git a/packages/graphic-walker/src/interfaces.ts b/packages/graphic-walker/src/interfaces.ts index 1d28d77a..5f49d1e2 100644 --- a/packages/graphic-walker/src/interfaces.ts +++ b/packages/graphic-walker/src/interfaces.ts @@ -350,6 +350,7 @@ export type IConfigScale = { rangeMin?: number; domainMin?: number; domainMax?: number; + type?: 'linear' | 'log' | 'pow' | 'sqrt' | 'symlog'; }; export interface IVisualConfig { @@ -374,10 +375,7 @@ export interface IVisualConfig { }; primaryColor?: string; colorPalette?: string; - scale?: { - opacity: IConfigScale; - size: IConfigScale; - }; + scale?: IConfigScaleSet; resolve: { x?: boolean; y?: boolean; @@ -398,6 +396,16 @@ export interface IVisualConfig { folds?: string[]; } +export interface IConfigScaleSet { + row?: IConfigScale; + column?: IConfigScale; + color?: IConfigScale; + opacity?: IConfigScale; + size?: IConfigScale; + radius?: IConfigScale; + theta?: IConfigScale; +} + export interface IVisualLayout { showTableSummary: boolean; format: { @@ -407,10 +415,7 @@ export interface IVisualLayout { }; primaryColor?: string; colorPalette?: string; - scale?: { - opacity: IConfigScale; - size: IConfigScale; - }; + scale?: IConfigScaleSet; resolve: { x?: boolean; y?: boolean; @@ -853,6 +858,8 @@ export type IFieldInfos = { }; export interface IChannelScales { + row?: IScale | ((info: IFieldInfos) => IScale); + column?: IScale | ((info: IFieldInfos) => IScale); color?: IColorScale | ((info: IFieldInfos) => IColorScale); opacity?: IScale | ((info: IFieldInfos) => IScale); size?: IScale | ((info: IFieldInfos) => IScale); diff --git a/packages/graphic-walker/src/locales/en-US.json b/packages/graphic-walker/src/locales/en-US.json index cd47d91e..6efaeaf2 100644 --- a/packages/graphic-walker/src/locales/en-US.json +++ b/packages/graphic-walker/src/locales/en-US.json @@ -3,9 +3,14 @@ "scheme": "Scheme", "primary_color": "Primary Color", "color_palette": "Color Palette", + "row": "Row", + "column": "Column", + "radius": "Radius", + "theta": "Theta", "opacity": "Opacity", "default_color_palette": "Default", "size": "Size", + "type": "Scale Type", "min_domain": "Domain Min", "max_domain": "Domain Max", "range": "Range", diff --git a/packages/graphic-walker/src/vis/react-vega.tsx b/packages/graphic-walker/src/vis/react-vega.tsx index 6a1a1adf..4c772d78 100644 --- a/packages/graphic-walker/src/vis/react-vega.tsx +++ b/packages/graphic-walker/src/vis/react-vega.tsx @@ -5,7 +5,7 @@ import * as op from 'rxjs/operators'; import { expressionFunction, type ScenegraphEvent } from 'vega'; import styled from 'styled-components'; import { useVegaExportApi } from '../utils/vegaApiExport'; -import { IViewField, IRow, IStackMode, VegaGlobalConfig, IVegaChartRef, IChannelScales, IDarkMode, IConfigScale } from '../interfaces'; +import { IViewField, IRow, IStackMode, VegaGlobalConfig, IVegaChartRef, IChannelScales, IDarkMode, IConfigScaleSet } from '../interfaces'; import { getVegaTimeFormatRules } from './temporalFormat'; import canvasSize from 'canvas-size'; import { Errors, useReporter } from '../utils/reportError'; @@ -91,10 +91,7 @@ interface ReactVegaProps { useSvg?: boolean; dark?: IDarkMode; scales?: IChannelScales; - scale?: { - opacity: IConfigScale; - size: IConfigScale; - }; + scale?: IConfigScaleSet; onReportSpec?: (spec: string) => void; displayOffset?: number; } @@ -159,17 +156,13 @@ const ReactVega = forwardRef(function ReactVe const mediaTheme = useContext(themeContext); const scales = useMemo(() => { const cs = channelScaleRaw ?? {}; - if (scale?.opacity) { - cs.opacity = { - ...(cs.opacity ?? {}), - ...scale.opacity, - }; - } - if (scale?.size) { - cs.size = { - ...(cs.size ?? {}), - ...scale.size, - }; + if (scale) { + for (const key of Object.keys(scale)) { + cs[key] = { + ...(cs[key] ?? {}), + ...scale[key], + }; + } } return cs; }, [channelScaleRaw, scale]); @@ -382,7 +375,10 @@ const ReactVega = forwardRef(function ReactVe for (let i = 0; i < leastOne(rowRepeatFields.length); i++) { for (let j = 0; j < leastOne(colRepeatFields.length); j++, index++) { const sourceId = index; - const node = i * leastOne(colRepeatFields.length) + j < viewPlaceholders.length ? viewPlaceholders[i * leastOne(colRepeatFields.length) + j].current : null; + const node = + i * leastOne(colRepeatFields.length) + j < viewPlaceholders.length + ? viewPlaceholders[i * leastOne(colRepeatFields.length) + j].current + : null; const ans = specs[index]; if (node) { const id = index; diff --git a/packages/graphic-walker/src/vis/spec/view.ts b/packages/graphic-walker/src/vis/spec/view.ts index ac3f6d6d..ccf85c76 100644 --- a/packages/graphic-walker/src/vis/spec/view.ts +++ b/packages/graphic-walker/src/vis/spec/view.ts @@ -162,21 +162,24 @@ export function resolveScale( export function resolveScales(scale: IChannelScales, view: any, data: readonly IRow[], theme: 'dark' | 'light') { const newEncoding = { ...view.encoding }; - function addScale(c: string) { - if (scale[c] && newEncoding[c]) { + function addScale(c: string, encodingName?: string) { + encodingName = encodingName ?? c; + if (scale[c] && newEncoding[encodingName]) { if (typeof scale[c] === 'function') { - const field = newEncoding[c].field; + const field = newEncoding[encodingName].field; const values = data.map((x) => x[field]); - newEncoding[c].scale = scale[c]({ - semanticType: newEncoding[c].type, + newEncoding[encodingName].scale = scale[c]({ + semanticType: newEncoding[encodingName].type, theme, values, }); } else { - newEncoding[c].scale = scale[c]; + newEncoding[encodingName].scale = scale[c]; } } } + addScale('row', 'y'); + addScale('column', 'x'); addScale('color'); addScale('opacity'); addScale('size');