From e4d1c411de5108bb5e265d98a8c1d1807ce5838a Mon Sep 17 00:00:00 2001 From: "Erlan (Erxuan) Zhou" Date: Tue, 14 Jul 2026 20:15:31 -0700 Subject: [PATCH] feat: support theming conversion funnel arrow via series.funnel.conversionArrow Expose the conversion funnel arrow (line / symbol / text) styles through the series.funnel.conversionArrow theme token, so they can be styled at the theme level in addition to the spec. Defaults mirror the previous hardcoded values, so existing charts render unchanged. --- ...nversion-arrow-theme_2026-07-14-08-58.json | 11 +++++ ...nversion-arrow-theme_2026-07-14-08-58.json | 11 +++++ .../conversion-funnel-transformer.ts | 48 ++++++++++++++----- .../types/series/funnel/interface.d.ts | 7 ++- .../vchart/src/series/funnel/interface.ts | 13 +++++ .../src/theme/builtin/common/series/funnel.ts | 21 ++++++++ 6 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 common/changes/@visactor/vchart-extension/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json create mode 100644 common/changes/@visactor/vchart/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json diff --git a/common/changes/@visactor/vchart-extension/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json b/common/changes/@visactor/vchart-extension/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json new file mode 100644 index 0000000000..dd5f2bc427 --- /dev/null +++ b/common/changes/@visactor/vchart-extension/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: conversion funnel arrow (line / symbol / text) styles now read from the series.funnel.conversionArrow theme token, so they can be styled at the theme level in addition to the spec", + "type": "minor", + "packageName": "@visactor/vchart-extension" + } + ], + "packageName": "@visactor/vchart-extension", + "email": "e@erlanzhou.com" +} diff --git a/common/changes/@visactor/vchart/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json b/common/changes/@visactor/vchart/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json new file mode 100644 index 0000000000..9e6fc98f22 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-funnel-conversion-arrow-theme_2026-07-14-08-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: support theming conversion funnel arrow (line / symbol / text) via series.funnel.conversionArrow theme token", + "type": "minor", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "e@erlanzhou.com" +} diff --git a/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel-transformer.ts b/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel-transformer.ts index b185223f06..12bddf0e42 100644 --- a/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel-transformer.ts +++ b/packages/vchart-extension/src/charts/conversion-funnel/conversion-funnel-transformer.ts @@ -15,7 +15,8 @@ export class ConversionFunnelChartSpecTransformer extends FunnelChartSpecTransfo transformSpec(spec: IConversionFunnelChartSpecBase): void { const { conversionArrow, extensionMark = [], funnelBackground } = spec; if (conversionArrow && conversionArrow.arrows && conversionArrow.arrows.length) { - const marks = addArrowMark(conversionArrow); + const arrowTheme = (this._option?.getTheme?.('series', 'funnel') as any)?.conversionArrow; + const marks = addArrowMark(conversionArrow, arrowTheme); if (marks && marks.length) { extensionMark.push(...marks); } @@ -39,13 +40,19 @@ export class ConversionFunnelChartSpecTransformer extends FunnelChartSpecTransfo } /** Arrow Related */ -function addArrowMark(arrowSpec: IConversionFunnelSpec['conversionArrow']) { +interface IArrowTheme { + line?: { style?: Record }; + symbol?: { style?: Record }; + text?: { style?: Record }; +} + +function addArrowMark(arrowSpec: IConversionFunnelSpec['conversionArrow'], theme?: IArrowTheme) { const { arrows, ...style } = arrowSpec; const leftArrows = arrows.filter(arrow => arrow.position === 'left'); const rightArrows = arrows.filter(arrow => arrow.position === 'right'); - const rightGroup = computeArrows(rightArrows, style); - const leftGroup = computeArrows(leftArrows, style); + const rightGroup = computeArrows(rightArrows, style, theme); + const leftGroup = computeArrows(leftArrows, style, theme); const result = []; if (rightGroup) { @@ -60,7 +67,11 @@ function addArrowMark(arrowSpec: IConversionFunnelSpec['conversionArrow']) { return result; } -function computeArrows(arrows: Arrow[], style: Omit) { +function computeArrows( + arrows: Arrow[], + style: Omit, + theme?: IArrowTheme +) { if (arrows?.length === 0) { return null; } @@ -74,15 +85,15 @@ function computeArrows(arrows: Arrow[], style: Omit = {} +) { const { style = {}, ...rest } = line; const renderable = (arrow: any, ctx: any) => { const { from, to } = arrow; @@ -112,6 +127,7 @@ function generateArrowLineSpec(line: IConversionFunnelSpec['conversionArrow']['l dataKey: arrow => `${arrow.id}`, style: { ...DEFAULT_ARROW_MARK_STYLE, + ...themeStyle, ...style, renderable, points: (arrow: any, ctx: any) => { @@ -126,7 +142,11 @@ function generateArrowLineSpec(line: IConversionFunnelSpec['conversionArrow']['l } as IExtensionMarkSpec<'polygon'>; } -function generateArrowSymbolSpec(symbol: IConversionFunnelSpec['conversionArrow']['symbol'] = {}, margin?: number) { +function generateArrowSymbolSpec( + symbol: IConversionFunnelSpec['conversionArrow']['symbol'] = {}, + margin?: number, + themeStyle: Record = {} +) { const { style = {}, ...rest } = symbol; return { type: 'symbol', @@ -135,6 +155,7 @@ function generateArrowSymbolSpec(symbol: IConversionFunnelSpec['conversionArrow' ...rest, style: { ...DEFAULT_ARROW_SYMBOL_MARK_STYLE, + ...themeStyle, ...style, x: (arrow: any, ctx: any) => { @@ -152,7 +173,11 @@ function generateArrowSymbolSpec(symbol: IConversionFunnelSpec['conversionArrow' } as IExtensionMarkSpec<'symbol'>; } -function generateArrowTextSpec(text: IConversionFunnelSpec['conversionArrow']['text'] = {}, margin?: number) { +function generateArrowTextSpec( + text: IConversionFunnelSpec['conversionArrow']['text'] = {}, + margin?: number, + themeStyle: Record = {} +) { const { style = {}, formatMethod, textMargin = 4, ...rest } = text; return { @@ -163,6 +188,7 @@ function generateArrowTextSpec(text: IConversionFunnelSpec['conversionArrow']['t ...rest, style: { ...DEFAULT_ARROW_TEXT_MARK_STYLE, + ...themeStyle, text: (arrow: any, ctx: any) => { const { text: textContent } = arrow; let displayTextContent: ReturnType = textContent; diff --git a/packages/vchart-types/types/series/funnel/interface.d.ts b/packages/vchart-types/types/series/funnel/interface.d.ts index 14f5d51364..c1259c363c 100644 --- a/packages/vchart-types/types/series/funnel/interface.d.ts +++ b/packages/vchart-types/types/series/funnel/interface.d.ts @@ -1,4 +1,4 @@ -import type { Datum, IMarkSpec, IMarkTheme, ISeriesSpec, IOrientType, IPathMarkSpec, IPolygonMarkSpec, IRuleMarkSpec, ITextMarkSpec, IPercent, IComposedTextMarkSpec, IFormatMethod } from '../../typings'; +import type { Datum, IMarkSpec, IMarkTheme, ISeriesSpec, IOrientType, IPathMarkSpec, IPolygonMarkSpec, IRuleMarkSpec, ISymbolMarkSpec, ITextMarkSpec, IPercent, IComposedTextMarkSpec, IFormatMethod } from '../../typings'; import type { IAnimationSpec } from '../../animation/spec'; import type { SeriesMarkNameEnum } from '../interface/type'; import type { ILabelSpec } from '../../component/label/interface'; @@ -49,5 +49,10 @@ export interface IFunnelSeriesTheme { line?: Partial>; }; [SeriesMarkNameEnum.transformLabel]?: Partial>; + conversionArrow?: { + line?: Partial>; + symbol?: Partial>; + text?: Partial>; + }; } export {}; diff --git a/packages/vchart/src/series/funnel/interface.ts b/packages/vchart/src/series/funnel/interface.ts index d059b3ac52..5da4803b43 100644 --- a/packages/vchart/src/series/funnel/interface.ts +++ b/packages/vchart/src/series/funnel/interface.ts @@ -7,6 +7,7 @@ import type { IPathMarkSpec, IPolygonMarkSpec, IRuleMarkSpec, + ISymbolMarkSpec, ITextMarkSpec, IPercent, IComposedTextMarkSpec, @@ -161,4 +162,16 @@ export interface IFunnelSeriesTheme { line?: Partial>; }; [SeriesMarkNameEnum.transformLabel]?: Partial>; + /** + * Style of the conversion-rate arrow (used by the `conversionFunnel` extension chart) + * @since 2.1.4 + */ + conversionArrow?: { + /** Style of the arrow leader line */ + line?: Partial>; + /** Style of the arrow head symbol */ + symbol?: Partial>; + /** Style of the arrow label text */ + text?: Partial>; + }; } diff --git a/packages/vchart/src/theme/builtin/common/series/funnel.ts b/packages/vchart/src/theme/builtin/common/series/funnel.ts index d6b54cf7f9..29e0cfd2b8 100644 --- a/packages/vchart/src/theme/builtin/common/series/funnel.ts +++ b/packages/vchart/src/theme/builtin/common/series/funnel.ts @@ -26,6 +26,27 @@ export const getFunnelTheme = (is3d?: boolean): IFunnelSeriesTheme => { fill: { type: 'palette', key: 'secondaryFontColor' }, textBaseline: 'middle' } + }, + conversionArrow: { + // Defaults intentionally mirror the original hardcoded constants (non-breaking); they are + // only surfaced to the theme layer so they can be overridden. + // To adapt to dark themes, override them in a custom theme, e.g. { type: 'palette', key: 'axisDomainColor' }. + line: { + style: { + stroke: 'black' + } + }, + symbol: { + style: { + fill: 'black' + } + }, + text: { + style: { + fontSize: 12, + fill: '#606773' + } + } } };