!!!###!!!title=主题配置解析逻辑——VisActor/VChart 社区贡献者文档!!!###!!!!!!###!!!description=---title: 11.1 主题的配置解析逻辑 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- !!!###!!!

VChart主题相关概念

VChart的主题模块是一个强大且灵活的图表样式配置系统。它允许用户通过统一和可复用的方式定制图表的视觉外观。用户可以轻松地为整个图表或特定图表类型定义全面的样式配置,包括颜色、字体、布局、组件样式等。通过预定义主题,用户可以快速实现一致的设计风格,无需为每个图表重复配置样式,从而大大简化了图表开发过程,并确保图表在不同场景下保持视觉一致性和专业性。简单来说,VChart的主题就像是图表的"设计模板",用户只需选择或自定义主题,就能快速创建美观、专业的数据可视化图表。

主题概念相关文档:VisActor/VChart tutorial documents

主题相关源码位置与内容

  • package/vchart/scr/util/theme:主题相关的工具类文件夹,包含对主题合并,解析,预处理(色板,token语义化)以及字符串主题转对象等实用的工具。

  • package/vchart/scr/core/vchart.ts:定义了核心类VChart,包括图表生命周期内的一系列钩子例如 主题初始化,注册,更新,切换,销毁。VChart 是具体的图表实例,负责应用和渲染,与主题的配置和更新有密不可分的联系。

  • package/vchart/src/theme:该文件夹包含了主题相关的特殊概念:色板(color-theme)、tokenMap、主题管理类(theme-manager)等数据结构。

核心类及之间的联系

  • VChart:负责图表的具体渲染、实例化和生命周期管理

  • ThemeManager:负责主题的全局注册、管理和切换

ThemeManager作为VChart的一个静态类暴露出来,用户可以使用诸如

VChart.ThemeManager.registerTheme('myTheme', { ... });VChart.ThemeManager.setCurrentTheme('myTheme');来管理主题

export class VChart implements IVChart {
       static readonly ThemeManager = ThemeManager;
}    

但是本质上,ThemeManager 仍然是一个独立的类,只是通过这种方式提供了更便捷的访问方式,这种静态属性暴露的设计模式做到了主题管理和图表渲染的解耦。

主题的配置解析逻辑

VChart 提供了两种方式配置图表主题:

  • 通过图表 spec 配置

  • 通过 ThemeManager 注册主题

主题配置的获取与优先级比较 (core/vchart.ts)

这两种配置都可以通过配置一套 ITheme 类型的主题对象,但是这两种配置的优先级是什么呢?这在updateCurrentTheme 方法里处理了优先级问题:

注意:严谨地说是三种主题来源:

  • currentTheme:通过 ThemeManager 注册的全局默认主题
  • optionTheme:在 VChart 构造函数的 options 中传入的主题
  • specTheme:在图表规格(spec)中指定的主题

它们的优先级从低到高依次是:

  • currentTheme < optionTheme < specTheme

src/core/vchart.ts 中有如下属性,获取到了用户配置的主题内容:

  • _spec.theme:用户在图表 spec 对象配置中指定的主题

  • _currentThemeName:通过 VChart.ThemeManager.registerTheme 注册的当前全局主题名称

简析主题合并的逻辑 (util/theme/merge-theme.ts)

mergeTheme 函数

export function mergeTheme(target: Maybe<ITheme>, ...sources: Maybe<ITheme>[]): Maybe<ITheme> {
  return mergeSpec(transformThemeToMerge(target), ...sources.map(transformThemeToMerge));
}    

  • 是合并主题的基础,一层简单的封装,简单地说是对象的属性覆盖

  • 表现结果是后出现的 sources 会覆盖前出现的 theme

示例

const baseTheme = { color: 'blue', fontSize: 12 };
const optionTheme = { color: 'red' };
const specTheme = { fontSize: 14 };

const finalTheme = mergeTheme({}, baseTheme, optionTheme, specTheme);
// 结果:{ color: 'red', fontSize: 14 }    

transformThemeToMerge函数

 function transformThemeToMerge(theme?: Maybe<ITheme>): Maybe<ITheme> {
  if (!theme) {
    return theme;
  }
  // 将色板转化为标准形式
  const colorScheme = transformColorSchemeToMerge(theme.colorScheme);

  return Object.assign({}, theme, {
    colorScheme,
    token: theme.token ?? {},
    series: Object.assign({}, theme.series)
  } as Partial<ITheme>);
}

/** 将色板转化为标准形式 */
export function transformColorSchemeToMerge(colorScheme?: Maybe<IThemeColorScheme>): Maybe<IThemeColorScheme> {
  if (colorScheme) {
    colorScheme = Object.keys(colorScheme).reduce<IThemeColorScheme>((scheme, key) => {
      const value = colorScheme[key];
      scheme[key] = transformColorSchemeToStandardStruct(value);
      return scheme;
    }, {} as IThemeColorScheme);
  }
  return colorScheme;
}    

transformThemeToMerge总的作用是完成了对主题对象进行标准化和规范化处理,他解决了

  • 颜色总是数组形式

  • 始终存在 token series 属性

确保无论用户传入的主题配置如何,都能转换成一个结构完整、一致且可预测的主题对象,为后续的主题合并和应用提供一个标准化的数据结构。

processThemeByChartType 函数

const processThemeByChartType = (type: string, theme: ITheme) => {
  if (theme.chart?.[type]) {
    theme = mergeTheme({}, theme, theme.chart[type]);
  }
  return theme;
};    

processThemeByChartType 是 VChart 主题系统中实现图表类型个性化的关键函数。它通过条件合并和 mergeTheme,实现了在保持全局主题一致性的同时,为不同图表类型提供定制化样式的能力。

字符串主题与对象主题的解析处理

用户配置主题时可以简单便捷的传入字符串主题(通常是从第三方主题包中导出的主题),例如:

import vScreenVolcanoBlue from '@visactor/vchart-theme/public/vScreenVolcanoBlue.json';
import VChart from '@visactor/vchart';

VChart.ThemeManager.registerTheme('vScreenVolcanoBlue', vScreenVolcanoBlue);

VChart.ThemeManager.setCurrentTheme('vScreenVolcanoBlue');    

也可以传入详细配置的自定义主题,例如:

const chart = new VChart({
  theme: {
    color: { primary: 'red' },
    fontSize: 14,
    chart: {
      bar: {
        color: 'blue'
      }
    }
  }
});    

在源码里针对两者的处理的核心,在_updateCurrentTheme 里判断类型,并通过 getThemeObject()做转化,统一处理成对象主题来解析的,这是个简单的逻辑,却为 VChart 的配置提供了灵活性和便捷性。

最终,经过层层关于优先级比较,表格类型的合并(processThemeByChartType),主题的合并处理逻辑,最终得到挂载在 VChart 对象里的 currentTheme 属性。

主题配置的预处理

当主题配置,合并后,会进入预处理阶段。主题预处理是 VChart 主题系统的关键步骤,将抽象的主题描述转换为具体的样式配置,为开发者提供直观的配置能力。

主要完成以下工作:

  • 语义化颜色转换
  • 将形如 { color: 'brand.primary' } 的颜色语义转换为具体颜色值
  • Token 替换
  • 将形如 { fontSize: 'size.m' } 的 token 语义转换为具体字号
  • 递归处理嵌套对象

预处理流程

this._currentTheme = preprocessTheme(processThemeByChartType(chartType, finalTheme));    

主题的预处理与解析

export function preprocessTheme(
  obj: any, //主题对象
  colorScheme?: IThemeColorScheme, // 颜色方案
  tokenMap?: TokenMap, // 标记映射
  seriesSpec?: ISeriesSpec // 系列规格
);    

这里涉及了 VChart 主题配置的重要概念:

  • colorScheme: 颜色方案

  • tokenMap: 标记映射

VChart.ThemeManager.registerTheme('dataVizTheme', {
  colorScheme: {
    brand: { primary: '#3A8DFF' },
    data: {
      positive: '#48BB78',
      negative: '#F56565'
    }
  },
  tokenMap: {
    typography: {
      fontSize: {
        small: 12,
        medium: 14,
        large: 16
      }
    }
  }
});    

开发者可以在注册时利用registerTheme方法仿照如上案例注册一套基于这 2 个概念的复杂主题配置,在实际使用中,开发者可以通过 { color: 'data.positive' } 或 { fontSize: { token: 'typography.fontSize.medium' } } 的方式引用这些定义。这里谈谈 VChart 是如何解析这个复杂对象的。

先逐层分析,这个处理函数 processTheme 的关键算法是递归遍历对象:

Object.keys(obj).forEach(key => {
  const value = obj[key];
  if (IGNORE_KEYS.includes(key)) {
    newObj[key] = value;
  }
  // 处理颜色语义化转换、Token 语义化转换
  else if (isPlainObject(value)) {
    if (isColorKey(value)) {
      newObj[key] = getActualColor(value, colorScheme, seriesSpec);
    } else if (isTokenKey(value)) {
      newObj[key] = queryToken(tokenMap, value);
    }
    // 这里使用了递归处理嵌套对象,使得能够处理任意深度的嵌套对象
    else {
      newObj[key] = preprocessTheme(value, colorScheme, tokenMap, seriesSpec);
    }
  }
  // 非对象类型直接赋值
  else {
    newObj[key] = value;
  }
});    

接下来分析具体的对于颜色语义和 token 语义的处理与解析

getActualColor 颜色语义化

/** 查询语义化颜色 */
export const getActualColor = (value: any, colorScheme?: IThemeColorScheme, seriesSpec?: ISeriesSpec) => {
  if (colorScheme && isColorKey(value)) {
    const color = queryColorFromColorScheme(colorScheme, value, seriesSpec);
    if (color) {
      return color;
    }
  }
  return value;
};

export function queryColorFromColorScheme(
  colorScheme: IThemeColorScheme,
  colorKey: IColorKey,
  seriesSpec?: ISeriesSpec
): ColorSchemeItem | undefined {
  const scheme = getColorSchemeBySeries(colorScheme, seriesSpec);
  if (!scheme) {
    return undefined;
  }
  let color;
  const { palette } = scheme as IColorSchemeStruct;
  if (isObject(palette)) {
    color = getUpgradedTokenValue(palette, colorKey.key) ?? colorKey.default;
  }
  if (!color) {
    return undefined;
  }
  if ((isNil(colorKey.a) && isNil(colorKey.l)) || !isString(color)) {
    return color;
  }
  let c = new Color(color);
  if (isValid(colorKey.l)) {
    const { r, g, b } = c.color;
    const { h, s } = rgbToHsl(r, g, b);
    const rgb = hslToRgb(h, s, colorKey.l);
    const newColor = new Color(`rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`);
    newColor.setOpacity(c.color.opacity);
    c = newColor;
  }
  if (isValid(colorKey.a)) {
    c.setOpacity(colorKey.a);
  }
  return c.toRGBA();
}    

queryColorFromColorScheme 是 VChart 主题系统中颜色处理的核心函数,它接收颜色方案(colorScheme)、颜色键(colorKey)和可选的系列规格(seriesSpec),通过一系列复杂的颜色查找和转换算法,实现了语义化颜色的精确定位和动态增强。

函数的核心逻辑是:首先根据系列规格获取特定的颜色方案,然后从调色板中查找对应的颜色。

export function getColorSchemeBySeries(
  colorScheme?: IThemeColorScheme,
  seriesSpec?: ISeriesSpec
): ColorScheme | undefined {
  const { type: seriesType } = seriesSpec ?? {};
  let scheme: ColorScheme | undefined;
  if (!seriesSpec || isNil(seriesType)) {
    scheme = colorScheme?.default;
  } else {
    const direction = getDirectionFromSeriesSpec(seriesSpec);
    scheme = colorScheme?.[`${seriesType}_${direction}`] ?? colorScheme?.[seriesType] ?? colorScheme?.default;
  }
  return scheme;
}    

这个算法优先匹配具体 seriesType_direction 的颜色方案,然后再匹配通用 seriesType 的颜色方案,最后再匹配默认颜色方案。

值得一提的是,此外函数还提供了两种高级颜色处理能力,根据 colorKeyla 的属性来动态处理颜色特性:

  • 通过 HSL 色彩空间转换实现颜色亮度的动态调整

    算法原理

    色彩空间转换:RGB → HSL → RGB

    HSL 亮度调整核心代码

       if (isValid(colorKey.l)) {
         const { r, g, b } = c.color;
         const { h, s } = rgbToHsl(r, g, b);
         const rgb = hslToRgb(h, s, colorKey.l);
         const newColor = new Color(rgb(${rgb.r}, ${rgb.g}, ${rgb.b}));
         newColor.setOpacity(c.color.opacity);
         c = newColor;
       }    

简单来说,就是在保持颜色原有色调(H)和饱和度(S)的情况下,仅调整颜色的明暗程度(L)。有关hsl和rgb格式的转换算法不是主题解析的重点,就简单提一下:

RGB 转 HSL 算法: 1. 将 RGB 值归一化到 [0,1] 1. 找出 R、G、B 中的最大值和最小值 1. 计算亮度 L = (max + min) / 2 1. 计算饱和度 S 1. 计算色相 H HSL 转 RGB 算法: 1. 将 H 分成 6 个区间 1. 根据 S 和 L 计算中间变量 1. 通过不同公式计算 R、G、B 值 1. 将结果映射到 [0,255]
* 如果 max == min,S = 0
  • 否则 S = (max - min) / (1 - |2L - 1|)

  • 根据哪个颜色分量最大,用不同公式计算

  • 范围 0-360 度

  • 设置颜色的透明度

透明度调整核心代码

if (isValid(colorKey.a)) {
  c.setOpacity(colorKey.a);
}    

queryToken Token 语义化

export function queryToken<T>(tokenMap: TokenMap, tokenKey: ITokenKey<T>): T | undefined {
  if (tokenMap && tokenKey.key in tokenMap) {
    return tokenMap[tokenKey.key];
  }
  return tokenKey.default;
}    

这个函数用于根据 tokenMap 和 tokenKey 查询对应的 token 值,如果 tokenMap 中存在对应的 token,就返回对应的值,否则返回默认值。


本文档由以下人员提供

吨吨(https://github.com/Shabi-x)

本文档由以下人员修正整理

玄魂