!!!###!!!title=Theme Configuration Parsing Logic——VisActor/VChart Contributing Documents!!!###!!!!!!###!!!description=---title: 11.1 Theme Configuration Parsing Logic key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

VChart Theme Related Concepts

The theme module of VChart is a powerful and flexible chart style configuration system. It allows users to customize the visual appearance of charts in a unified and reusable way. Users can easily define comprehensive style configurations for the entire chart or specific chart types, including colors, fonts, layouts, component styles, etc. By using predefined themes, users can quickly achieve a consistent design style without having to repeatedly configure styles for each chart, greatly simplifying the chart development process and ensuring visual consistency and professionalism across different scenarios. In simple terms, the theme of VChart is like a "design template" for charts, allowing users to quickly create beautiful and professional data visualization charts by simply selecting or customizing a theme.

Theme concept related documents: VisActor/VChart tutorial documents

  • package/vchart/scr/util/theme: A folder of utility classes related to themes, including tools for theme merging, parsing, preprocessing (color palettes, token semantics), and converting string themes to objects.

  • package/vchart/scr/core/vchart.ts: Defines the core class VChart, including a series of hooks throughout the chart lifecycle such as theme initialization, registration, updating, switching, and destruction. VChart is a specific chart instance responsible for application and rendering, closely related to theme configuration and updates.

  • package/vchart/src/theme: This folder contains special concepts related to themes: color palettes (color-theme), tokenMap, theme manager class (theme-manager), and other data structures.

Core Classes and Their Relationships

  • VChart: Responsible for specific rendering, instantiation, and lifecycle management of charts

  • ThemeManager: Responsible for global registration, management, and switching of themes

ThemeManager is exposed as a static class of VChart, allowing users to manage themes using commands like

VChart.ThemeManager.registerTheme('myTheme', { ... }); or VChart.ThemeManager.setCurrentTheme('myTheme');

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

However, essentially, ThemeManager is still an independent class, but it provides a more convenient way to access it through this method. This design pattern of exposing static properties achieves the decoupling of theme management and chart rendering.

Theme Configuration Parsing Logic

VChart provides two ways to configure chart themes:

  • Through chart spec configuration

  • By registering themes through ThemeManager

Theme Configuration Retrieval and Priority Comparison (core/vchart.ts)

Both configurations can be set up with a ITheme type theme object, but what is the priority of these two configurations? This priority issue is handled in the updateCurrentTheme method:

Note: Strictly speaking, there are three sources of themes:

  • currentTheme: The global default theme registered through ThemeManager
  • optionTheme: The theme passed in the options of the VChart constructor
  • specTheme: The theme specified in the chart specification (spec)

Their priority from low to high is:

  • currentTheme < optionTheme < specTheme

In src/core/vchart.ts, the following properties are used to obtain the theme content configured by the user:

  • _spec.theme: The theme specified by the user in the chart spec object configuration

  • _currentThemeName: The current global theme name registered through VChart.ThemeManager.registerTheme

Brief Analysis of Theme Merging Logic (util/theme/merge-theme.ts)

mergeTheme Function

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

  • It is the basis for merging themes, a simple layer of encapsulation. Simply put, it is the overriding of object properties.

  • The result is that the later appearing sources will override the earlier appearing theme.

Example

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

 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 generally serves to standardize and normalize the theme object, addressing the following:

  • Colors are always in array form

  • Always have token and series attributes

This ensures that regardless of the theme configuration provided by the user, it can be transformed into a structurally complete, consistent, and predictable theme object, providing a standardized data structure for subsequent theme merging and application.

processThemeByChartType Function

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

processThemeByChartType is a key function in the VChart theme system that implements chart type personalization. It achieves the ability to provide customized styles for different chart types while maintaining global theme consistency through conditional merging and mergeTheme.

Parsing and Processing of String Themes and Object Themes

When configuring themes, users can easily and conveniently pass in string themes (usually themes exported from third-party theme packages), for example:

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

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

VChart.ThemeManager.setCurrentTheme('vScreenVolcanoBlue');    

You can also pass in a custom theme with detailed configuration, for example:

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

The core of handling both in the source code is to determine the type in _updateCurrentTheme and convert it through getThemeObject(), uniformly processing it into an object theme for parsing. This is a simple logic, yet it provides flexibility and convenience for VChart's configuration.

Ultimately, after layers of priority comparison, merging of table types (processThemeByChartType), and theme merging processing logic, the currentTheme attribute mounted on the VChart object is finally obtained.

Preprocessing of Theme Configuration

When the theme configuration is merged, it enters the preprocessing stage. Theme preprocessing is a key step in the VChart theme system, converting abstract theme descriptions into specific style configurations, providing developers with intuitive configuration capabilities.

Mainly accomplishes the following tasks:

  • Semantic color conversion
  • Convert color semantics like { color: 'brand.primary' } into specific color values
  • Token replacement
  • Convert token semantics like { fontSize: 'size.m' } into specific font sizes
  • Recursive processing of nested objects

Preprocessing Flow:

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

Preprocessing and Parsing of Themes

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

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

  • colorScheme: Color scheme

  • tokenMap: Token mapping

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

Developers can use the registerTheme method during registration to register a complex theme configuration based on these two concepts, as shown in the example above. In actual use, developers can reference these definitions through { color: 'data.positive' } or { fontSize: { token: 'typography.fontSize.medium' } }. Here, let's discuss how VChart parses this complex object.

First, analyze layer by layer, the key algorithm of this processing function processTheme is to recursively traverse the object:

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;
  }
});    

Next, analyze the specific handling and parsing of color semantics and token semantics

getActualColor Color Semantics

/** 查询语义化颜色 */
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 is the core function for color processing in the VChart theme system. It receives a color scheme (colorScheme), a color key (colorKey), and an optional series specification (seriesSpec). Through a series of complex color lookup and conversion algorithms, it achieves precise localization and dynamic enhancement of semantic colors.

The core logic of the function is: first, obtain a specific color scheme based on the series specification, and then look up the corresponding color from the palette.

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;
}    

This algorithm prioritizes matching the color scheme of a specific seriesType_direction, then matches the general seriesType color scheme, and finally matches the default color scheme.

It is worth mentioning that this function also provides two advanced color processing capabilities, dynamically handling color characteristics based on the l or a attributes in colorKey:

  • Dynamic adjustment of color brightness through HSL color space conversion

    Algorithm Principle

    Color space conversion: RGB → HSL → RGB

    Core code for HSL brightness adjustment

       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;
       }    

Simply put, it is to adjust the brightness level (L) of the color while maintaining the original hue (H) and saturation (S). The conversion algorithm between hsl and rgb formats is not the focus of the topic analysis, so it is briefly mentioned:

RGB to HSL algorithm: 1. Normalize RGB values to [0,1] 1. Find the maximum and minimum values among R, G, B 1. Calculate brightness L = (max + min) / 2 1. Calculate saturation S 1. Calculate hue H HSL to RGB algorithm: 1. Divide H into 6 intervals 1. Calculate intermediate variables based on S and L 1. Calculate R, G, B values using different formulas 1. Map the results to [0,255]
* If max == min, S = 0
  • Otherwise S = (max - min) / (1 - |2L - 1|)

  • Use different formulas based on which color component is the largest

  • Range 0-360 degrees

  • Set the transparency of the color

Core code for transparency adjustment

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

queryToken Token Semantics

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

This function is used to query the corresponding token value based on tokenMap and tokenKey. If the corresponding token exists in tokenMap, it returns the corresponding value; otherwise, it returns the default value.


This document is provided by the following personnel

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

This document is revised and organized by the following personnel

Xuanhun