diff --git a/.dumi/theme/SiteThemeProvider.tsx b/.dumi/theme/SiteThemeProvider.tsx index 9ac2799fb..69ae0aa3d 100644 --- a/.dumi/theme/SiteThemeProvider.tsx +++ b/.dumi/theme/SiteThemeProvider.tsx @@ -1,4 +1,5 @@ import { ConfigProvider, token } from '@oceanbase/design'; +import { ChartProvider } from '@oceanbase/charts'; import type { ThemeProviderProps } from 'antd-style'; import { ThemeProvider } from 'antd-style'; import type { FC } from 'react'; @@ -40,7 +41,7 @@ const SiteThemeProvider: FC = ({ children, theme, ...rest }) direction={direction} locale={lang === 'cn' ? zhCN : undefined} > - {children} + {children} ); diff --git a/.dumi/theme/layouts/GlobalLayout.tsx b/.dumi/theme/layouts/GlobalLayout.tsx index 98de0a0bf..b10d46199 100644 --- a/.dumi/theme/layouts/GlobalLayout.tsx +++ b/.dumi/theme/layouts/GlobalLayout.tsx @@ -7,7 +7,7 @@ import { } from '@ant-design/cssinjs'; import { App, theme as obTheme } from '@oceanbase/design'; import type { DirectionType } from '@oceanbase/design/es/config-provider'; -import { createSearchParams, useOutlet, useSearchParams } from 'dumi'; +import { usePrefersColor, createSearchParams, useOutlet, useSearchParams } from 'dumi'; import React, { useCallback, useEffect, useMemo } from 'react'; import useLayoutState from '../../hooks/useLayoutState'; import SiteThemeProvider from '../SiteThemeProvider'; @@ -42,6 +42,7 @@ const GlobalLayout: React.FC = () => { const outlet = useOutlet(); const { pathname } = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); + const [, , setPrefersColor] = usePrefersColor(); const [{ theme = [], direction, isMobile }, setSiteState] = useLayoutState({ isMobile: false, direction: 'ltr', @@ -69,6 +70,8 @@ const GlobalLayout: React.FC = () => { ...nextSearchParams, theme: value.filter(t => t !== 'light'), }); + // Set theme of dumi site + setPrefersColor(value?.filter(t => t === 'dark' || t === 'light')?.[0]); } }); @@ -88,6 +91,8 @@ const GlobalLayout: React.FC = () => { const _direction = searchParams.get('direction') as DirectionType; setSiteState({ theme: _theme, direction: _direction === 'rtl' ? 'rtl' : 'ltr' }); + // Set theme of dumi site + setPrefersColor(_theme?.filter(t => t === 'dark' || t === 'light')?.[0]); // Handle isMobile updateMobileMode(); diff --git a/docs/charts/charts-theme.md b/docs/charts/charts-theme.md index 291389e98..5ba619167 100644 --- a/docs/charts/charts-theme.md +++ b/docs/charts/charts-theme.md @@ -6,16 +6,33 @@ group: 可视化图表 OceanBase Charts 的设计体系遵循 AntV 设计规范,并在此基础上扩展出了很多具备 OceanBase 产品风格的设计规范模式,包括但不限于全局样式(色板、圆角、边框)和特定图表的视觉定制,以传递 OceanBase 科技、活力、专注和关怀的品牌特点。 -## 使用主题 Token +## 使用主题配置 -```ts -import { theme } from '@oceanbase/charts'; +```tsx | pure +import { ChartProvider, useTheme } from '@oceanbase/charts'; -// 主题色 -console.log(theme.defaultColor); - -// 折线图线宽 -console.log(theme.styleSheet.lineBorder); +export default () { + // 获取主题配置 + const themeConfig = useTheme(); + // 主题色 + console.log(themeConfig.defaultColor); + // 折线图线宽 + console.log(themeConfig.styleSheet.lineBorder); + // 设置主题 + return ( + <> + + {...} + + + {...} + + + {...} + + + ); +}; ``` - 主题的全量 Token 可参考 https://github.com/oceanbase/charts/blob/master/src/theme/index.ts#L29 。 diff --git a/packages/charts/src/Area/index.tsx b/packages/charts/src/Area/index.tsx index cd083b81e..cc62b236f 100644 --- a/packages/charts/src/Area/index.tsx +++ b/packages/charts/src/Area/index.tsx @@ -5,14 +5,20 @@ import { sortByMoment } from '@oceanbase/util'; import useResizeObserver from 'use-resize-observer'; import type { Tooltip } from '../hooks/useTooltipScrollable'; import useTooltipScrollable from '../hooks/useTooltipScrollable'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; export interface AreaConfig extends AntAreaConfig { tooltip?: false | Tooltip; + theme?: Theme; } const Area: React.FC = forwardRef( - ({ data, line, xField, xAxis, yAxis, tooltip, legend, interactions, ...restConfig }, ref) => { + ( + { data, line, xField, xAxis, yAxis, tooltip, legend, interactions, theme, ...restConfig }, + ref + ) => { + const themeConfig = useTheme(theme); const { ref: containerRef, height: containerHeight } = useResizeObserver({ // 包含 padding 和 border box: 'border-box', @@ -30,7 +36,7 @@ const Area: React.FC = forwardRef( line: { ...line, style: { - lineWidth: theme.styleSheet.lineBorder, + lineWidth: themeConfig.styleSheet.lineBorder, ...line?.style, }, }, @@ -50,8 +56,8 @@ const Area: React.FC = forwardRef( line: { ...xAxis?.grid?.line, style: { - lineWidth: theme.styleSheet.axisGridBorder, - stroke: theme.styleSheet.axisGridBorderColor, + lineWidth: themeConfig.styleSheet.axisGridBorder, + stroke: themeConfig.styleSheet.axisGridBorderColor, lineDash: [4, 4], ...xAxis?.grid?.line?.style, }, @@ -81,7 +87,7 @@ const Area: React.FC = forwardRef( type: 'brush-x', }, ], - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ( diff --git a/packages/charts/src/Bar/demo/basic.tsx b/packages/charts/src/Bar/demo/basic.tsx index 7ba2f26aa..d230113ad 100644 --- a/packages/charts/src/Bar/demo/basic.tsx +++ b/packages/charts/src/Bar/demo/basic.tsx @@ -31,7 +31,6 @@ export default () => { position: 'top-left', }, }; - console.log(Bar); return ( diff --git a/packages/charts/src/Bar/index.tsx b/packages/charts/src/Bar/index.tsx index af2854751..a0e96e9bd 100644 --- a/packages/charts/src/Bar/index.tsx +++ b/packages/charts/src/Bar/index.tsx @@ -1,8 +1,10 @@ import React, { forwardRef } from 'react'; import type { BarConfig as AntBarConfig } from '@ant-design/charts'; import { Bar as AntBar } from '@ant-design/charts'; -import { theme } from '../theme'; +import { uniq } from 'lodash'; import { toPercent } from '../util/number'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; export interface BarConfig extends AntBarConfig { // 是否为进度条形图,数值范围为 0 ~ 1 @@ -11,6 +13,7 @@ export interface BarConfig extends AntBarConfig { warningPercent?: number; // 危险水位线,仅 isProgress 为 true 时生效 dangerPercent?: number; + theme?: Theme; } const Bar: React.FC = forwardRef( @@ -32,14 +35,16 @@ const Bar: React.FC = forwardRef( xAxis, yAxis, legend, + theme, ...restConfig }, ref ) => { + const themeConfig = useTheme(theme); const stackValues = (isStack && seriesField && - data?.filter(item => item[seriesField]).map(item => item[seriesField])) || + uniq(data?.filter(item => item[seriesField]).map(item => item[seriesField]))) || []; // 堆叠柱状图中最后一段对应的值 const lastStackValue = stackValues?.[stackValues?.length - 1]; @@ -51,8 +56,8 @@ const Bar: React.FC = forwardRef( isPercent, isRange, seriesField, - maxBarWidth: theme.barWidth, - minBarWidth: theme.barWidth, + maxBarWidth: themeConfig.barWidth, + minBarWidth: themeConfig.barWidth, meta: isProgress ? { ...meta, @@ -78,9 +83,9 @@ const Bar: React.FC = forwardRef( let color; if (isProgress) { if (dangerPercent && datum[xField] >= dangerPercent) { - color = theme.semanticRed; + color = themeConfig.semanticRed; } else if (warningPercent && datum[xField] >= warningPercent) { - color = theme.semanticYellow; + color = themeConfig.semanticYellow; } } @@ -112,7 +117,7 @@ const Bar: React.FC = forwardRef( ? { ...barBackground, style: { - fill: theme.barBackgroundColor, + fill: themeConfig.barBackgroundColor, ...barBackground?.style, }, } @@ -125,8 +130,8 @@ const Bar: React.FC = forwardRef( line: { ...yAxis?.grid?.line, style: { - lineWidth: theme.styleSheet.axisGridBorder, - stroke: theme.styleSheet.axisGridBorderColor, + lineWidth: themeConfig.styleSheet.axisGridBorder, + stroke: themeConfig.styleSheet.axisGridBorderColor, lineDash: [4, 4], ...yAxis?.grid?.line?.style, }, @@ -142,7 +147,7 @@ const Bar: React.FC = forwardRef( ...legend?.marker, }, }, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/ChartProvider/__tests__/theme.test.tsx b/packages/charts/src/ChartProvider/__tests__/theme.test.tsx new file mode 100644 index 000000000..27fad764f --- /dev/null +++ b/packages/charts/src/ChartProvider/__tests__/theme.test.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { ChartProvider, useTheme } from '@oceanbase/charts'; + +describe('ChartProvider theme', () => { + it('default theme', () => { + const Child = () => { + const themeConfig = useTheme(); + expect(themeConfig.theme).toBe('light'); + expect(themeConfig.defaultColor).toBe('#4C96FF'); + return
; + }; + render( + + + + ); + }); + + it('light theme', () => { + const Child = () => { + const themeConfig = useTheme(); + expect(themeConfig.theme).toBe('light'); + expect(themeConfig.defaultColor).toBe('#4C96FF'); + return
; + }; + render( + + + + ); + }); + + it('dark theme', () => { + const Child = () => { + const themeConfig = useTheme(); + expect(themeConfig.theme).toBe('dark'); + expect(themeConfig.defaultColor).toBe('#4D97FF'); + return
; + }; + render( + + + + ); + }); + + it('custom theme config', () => { + const Child = () => { + const themeConfig = useTheme(); + expect(themeConfig.theme).toBe('custom-theme'); + expect(themeConfig.defaultColor).toBe('#ff0000'); + expect(themeConfig.subColor).toBe('#00ff00'); + return
; + }; + render( + + + + ); + }); +}); diff --git a/packages/charts/src/ChartProvider/context.ts b/packages/charts/src/ChartProvider/context.ts new file mode 100644 index 000000000..8a0d8a923 --- /dev/null +++ b/packages/charts/src/ChartProvider/context.ts @@ -0,0 +1,11 @@ +import React from 'react'; + +export type Theme = 'light' | 'dark' | string | object; + +export interface ChartConsumerProps { + theme?: Theme; +} + +export const ChartContext = React.createContext({ + theme: 'light', +}); diff --git a/packages/charts/src/ChartProvider/index.tsx b/packages/charts/src/ChartProvider/index.tsx new file mode 100644 index 000000000..ed9632c2a --- /dev/null +++ b/packages/charts/src/ChartProvider/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { ChartContext } from './context'; +import type { ChartConsumerProps } from './context'; + +export * from './context'; + +export interface ChartProviderProps extends ChartConsumerProps { + children?: React.ReactNode; +} + +const ChartProvider: React.FC & { + ChartContext: typeof ChartContext; +} = ({ children, theme = 'light', ...restProps }) => { + return ( + + {children} + + ); +}; + +ChartProvider.ChartContext = ChartContext; + +export default ChartProvider; diff --git a/packages/charts/src/Column/index.tsx b/packages/charts/src/Column/index.tsx index 8c8733659..618dea96d 100644 --- a/packages/charts/src/Column/index.tsx +++ b/packages/charts/src/Column/index.tsx @@ -1,16 +1,24 @@ import React, { forwardRef } from 'react'; import type { ColumnConfig as AntColumnConfig } from '@ant-design/charts'; import { Column as AntColumn } from '@ant-design/charts'; -import { theme } from '../theme'; +import { uniq } from 'lodash'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -export type ColumnConfig = AntColumnConfig; +export interface ColumnConfig extends AntColumnConfig { + theme?: Theme; +} const Column: React.FC = forwardRef( - ({ data, isStack, isGroup, isRange, seriesField, label, xAxis, legend, ...restConfig }, ref) => { + ( + { data, isStack, isGroup, isRange, seriesField, label, xAxis, legend, theme, ...restConfig }, + ref + ) => { + const themeConfig = useTheme(theme); const stackValues = (isStack && seriesField && - data?.filter(item => item[seriesField]).map(item => item[seriesField])) || + uniq(data?.filter(item => item[seriesField]).map(item => item[seriesField]))) || []; // 堆叠柱状图中最后一段对应的值 const lastStackValue = stackValues?.[0]; @@ -20,8 +28,8 @@ const Column: React.FC = forwardRef( isGroup, isRange, seriesField, - maxColumnWidth: theme.columnWidth, - minColumnWidth: theme.columnWidth, + maxColumnWidth: themeConfig.columnWidth, + minColumnWidth: themeConfig.columnWidth, // 普通柱状图 label 会展示在顶部,需要留出一定空间,否则 label 会被遮挡 appendPadding: isStack || isGroup || isRange ? 0 : [16, 0, 0, 0], // 分组柱状图组内柱子间距,仅分组柱状图生效 @@ -62,8 +70,8 @@ const Column: React.FC = forwardRef( line: { ...xAxis?.grid?.line, style: { - lineWidth: theme.styleSheet.axisGridBorder, - stroke: theme.styleSheet.axisGridBorderColor, + lineWidth: themeConfig.styleSheet.axisGridBorder, + stroke: themeConfig.styleSheet.axisGridBorderColor, lineDash: [4, 4], ...xAxis?.grid?.line?.style, }, @@ -79,7 +87,7 @@ const Column: React.FC = forwardRef( ...legend?.marker, }, }, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/DualAxes/index.tsx b/packages/charts/src/DualAxes/index.tsx index 510e0a33a..eb018ce3a 100644 --- a/packages/charts/src/DualAxes/index.tsx +++ b/packages/charts/src/DualAxes/index.tsx @@ -1,25 +1,31 @@ import React, { forwardRef } from 'react'; import type { DualAxesConfig as AntDualAxesConfig } from '@ant-design/charts'; import { DualAxes as AntDualAxes } from '@ant-design/charts'; +// @ts-ignore import type { GeometryColumnOption } from '@antv/g2plot/esm/plots/dual-axes/types'; +// @ts-ignore import type { Axis } from '@antv/g2plot/esm/types/axis'; import { sortByMoment } from '@oceanbase/util'; import useResizeObserver from 'use-resize-observer'; import type { Tooltip } from '../hooks/useTooltipScrollable'; import useTooltipScrollable from '../hooks/useTooltipScrollable'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; export interface DualAxesConfig extends Omit { // 限制双轴图的 yAxis 为对象格式,而非数组格式。官方文档的示例均为对象格式,方便统一用法 - yAxis: false | Record; + yAxis?: Record; tooltip?: false | Tooltip; + theme?: Theme; } const DualAxes: React.FC = forwardRef( ( - { data, xField, yField, xAxis, yAxis, tooltip, legend, geometryOptions, ...restConfig }, + { data, xField, yField, xAxis, yAxis, tooltip, legend, geometryOptions, theme, ...restConfig }, ref ) => { + const themeConfig = useTheme(theme); + const yField1 = yField?.[0]; const yField2 = yField?.[1]; @@ -59,15 +65,15 @@ const DualAxes: React.FC = forwardRef( line: { ...xAxis?.grid?.line, style: { - lineWidth: theme.styleSheet.axisGridBorder, - stroke: theme.styleSheet.axisGridBorderColor, + lineWidth: themeConfig.styleSheet.axisGridBorder, + stroke: themeConfig.styleSheet.axisGridBorderColor, lineDash: [4, 4], ...xAxis?.grid?.line?.style, }, }, }, }, - yAxis: yAxis !== false && { + yAxis: yAxis && { [yField1]: yAxis?.[yField1] !== false && { // 避免超出 Y 轴刻度线 nice: true, @@ -106,8 +112,8 @@ const DualAxes: React.FC = forwardRef( // 堆叠柱状图中最后一段对应的值 const lastStackValue = stackValues?.[0]; defaultGeometryOption = { - maxColumnWidth: theme.columnWidth, - minColumnWidth: theme.columnWidth, + maxColumnWidth: themeConfig.columnWidth, + minColumnWidth: themeConfig.columnWidth, columnStyle: datum => { return { radius: @@ -130,7 +136,7 @@ const DualAxes: React.FC = forwardRef( ...item, }; }), - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ( diff --git a/packages/charts/src/Gauge/demo/custom-color.tsx b/packages/charts/src/Gauge/demo/custom-color.tsx index 04713f0da..3646be399 100644 --- a/packages/charts/src/Gauge/demo/custom-color.tsx +++ b/packages/charts/src/Gauge/demo/custom-color.tsx @@ -1,11 +1,12 @@ -import { Gauge, theme } from '@oceanbase/charts'; +import { Gauge, useTheme } from '@oceanbase/charts'; export default () => { + const themeConfig = useTheme(); const config = { percent: 0.75, range: { ticks: [0, 1 / 3, 2 / 3, 1], - color: [theme.semanticGreen, theme.semanticYellow, theme.semanticRed], + color: [themeConfig.semanticGreen, themeConfig.semanticYellow, themeConfig.semanticRed], }, }; return ; diff --git a/packages/charts/src/Gauge/demo/meter.tsx b/packages/charts/src/Gauge/demo/meter.tsx index 11b480c4b..fd334e6a0 100644 --- a/packages/charts/src/Gauge/demo/meter.tsx +++ b/packages/charts/src/Gauge/demo/meter.tsx @@ -1,13 +1,14 @@ -import { Gauge, theme } from '@oceanbase/charts'; +import { Gauge, useTheme } from '@oceanbase/charts'; export default () => { + const themeConfig = useTheme(); const config = { percent: 0.75, type: 'meter', innerRadius: 0.75, range: { ticks: [0, 1 / 3, 2 / 3, 1], - color: [theme.semanticGreen, theme.semanticYellow, theme.semanticRed], + color: [themeConfig.semanticGreen, themeConfig.semanticYellow, themeConfig.semanticRed], }, }; return ; diff --git a/packages/charts/src/Gauge/index.tsx b/packages/charts/src/Gauge/index.tsx index a1d3eea4e..c055629a1 100644 --- a/packages/charts/src/Gauge/index.tsx +++ b/packages/charts/src/Gauge/index.tsx @@ -1,19 +1,24 @@ import React, { forwardRef } from 'react'; import type { GaugeConfig as AntGaugeConfig } from '@ant-design/charts'; import { Gauge as AntGauge } from '@ant-design/charts'; -import { theme } from '../theme'; import { toPercent } from '../util/number'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -export type GaugeConfig = AntGaugeConfig; +export interface GaugeConfig extends AntGaugeConfig { + theme?: Theme; +} const Gauge: React.FC = forwardRef( - ({ percent, range, axis, indicator, statistic, ...restConfig }, ref) => { + ({ percent, range, axis, indicator, statistic, theme, ...restConfig }, ref) => { + const themeConfig = useTheme(theme); + const newConfig: GaugeConfig = { percent, startAngle: (Math.PI * 11) / 12, endAngle: (Math.PI * 1) / 12, range: { - color: theme.semanticGreen, + color: themeConfig.semanticGreen, ...range, }, axis: axis !== false && { @@ -58,7 +63,7 @@ const Gauge: React.FC = forwardRef( }, }, }, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/Histogram/index.tsx b/packages/charts/src/Histogram/index.tsx index b1faf0854..a89191581 100644 --- a/packages/charts/src/Histogram/index.tsx +++ b/packages/charts/src/Histogram/index.tsx @@ -1,14 +1,23 @@ import React, { forwardRef } from 'react'; import type { HistogramConfig as AntHistogramConfig } from '@ant-design/charts'; import { Histogram as AntHistogram } from '@ant-design/charts'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -export type HistogramConfig = AntHistogramConfig; +export interface HistogramConfig extends AntHistogramConfig { + theme?: Theme; +} const Histogram: React.FC = forwardRef( - ({ binWidth, meta, xAxis, legend, ...restConfig }, ref) => { + ({ binWidth, columnStyle, meta, xAxis, legend, theme, ...restConfig }, ref) => { + const themeConfig = useTheme(theme); + const newConfig: HistogramConfig = { binWidth, + columnStyle: { + stroke: themeConfig.backgroundColor, + ...columnStyle, + }, meta: { ...meta, range: { @@ -29,8 +38,8 @@ const Histogram: React.FC = forwardRef( line: { ...xAxis?.grid?.line, style: { - lineWidth: theme.styleSheet.axisGridBorder, - stroke: theme.styleSheet.axisGridBorderColor, + lineWidth: themeConfig.styleSheet.axisGridBorder, + stroke: themeConfig.styleSheet.axisGridBorderColor, lineDash: [4, 4], ...xAxis?.grid?.line?.style, }, @@ -46,7 +55,7 @@ const Histogram: React.FC = forwardRef( ...legend?.marker, }, }, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/Line/index.tsx b/packages/charts/src/Line/index.tsx index c1366f315..1275f94f9 100644 --- a/packages/charts/src/Line/index.tsx +++ b/packages/charts/src/Line/index.tsx @@ -5,14 +5,21 @@ import { Line as AntLine } from '@ant-design/charts'; import useResizeObserver from 'use-resize-observer'; import type { Tooltip } from '../hooks/useTooltipScrollable'; import useTooltipScrollable from '../hooks/useTooltipScrollable'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; export interface LineConfig extends AntLineConfig { tooltip?: false | Tooltip; + theme?: Theme; } const Line: React.FC = forwardRef( - ({ data, stepType, xField, xAxis, yAxis, tooltip, legend, interactions, ...restConfig }, ref) => { + ( + { data, stepType, xField, xAxis, yAxis, tooltip, legend, interactions, theme, ...restConfig }, + ref + ) => { + const themeConfig = useTheme(theme); + const { ref: containerRef, height: containerHeight } = useResizeObserver({ // 包含 padding 和 border box: 'border-box', @@ -44,8 +51,8 @@ const Line: React.FC = forwardRef( line: { ...xAxis?.grid?.line, style: { - lineWidth: theme.styleSheet.axisGridBorder, - stroke: theme.styleSheet.axisGridBorderColor, + lineWidth: themeConfig.styleSheet.axisGridBorder, + stroke: themeConfig.styleSheet.axisGridBorderColor, lineDash: [4, 4], ...xAxis?.grid?.line?.style, }, @@ -75,7 +82,7 @@ const Line: React.FC = forwardRef( type: 'brush-x', }, ], - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ( diff --git a/packages/charts/src/Liquid/index.tsx b/packages/charts/src/Liquid/index.tsx index 96c808a67..8d4d6ed00 100644 --- a/packages/charts/src/Liquid/index.tsx +++ b/packages/charts/src/Liquid/index.tsx @@ -1,25 +1,28 @@ import React, { forwardRef } from 'react'; import type { LiquidConfig as AntLiquidConfig } from '@ant-design/charts'; import { Liquid as AntLiquid } from '@ant-design/charts'; +// @ts-ignore +import type { PathCommand } from '@antv/g-base'; import { toPercent } from '../util/number'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -function rectWithRadius(x: number, y: number, width: number, height: number) { +function rectWithRadius(x: number, y: number, width: number, height: number): PathCommand[] { const GOLDEN_SECTION_RATIO = 0.618; const h = height / 2; const w = (width / 2) * GOLDEN_SECTION_RATIO; const radius = 6; - return ` - M ${x - w + radius} ${y - h} - L ${x + w - radius} ${y - h} - a ${radius}, ${radius} 0 0 1 ${radius}, ${radius} - L ${x + w} ${y + h - radius} - a ${radius}, ${radius} 0 0 1 ${-radius}, ${radius} - L ${x - w + radius} ${y + h} - a ${radius}, ${radius} 0 0 1 ${-radius}, ${-radius} - L ${x - w} ${y - h + radius} - a ${radius}, ${radius} 0 0 1 ${radius}, ${-radius} - `; + return [ + ['M', x - w + radius, y - h], + ['L', x + w - radius, y - h], + ['a', radius, radius, 0, 0, 1, radius, radius], + ['L', x + w, y + h - radius], + ['a', radius, radius, 0, 0, 1, -radius, radius], + ['L', x - w + radius, y + h], + ['a', radius, radius, 0, 0, 1, -radius, -radius], + ['L', x - w, y - h + radius], + ['a', radius, radius, 0, 0, 1, radius, -radius], + ]; } export interface LiquidConfig extends AntLiquidConfig { @@ -32,6 +35,7 @@ export interface LiquidConfig extends AntLiquidConfig { dangerPercent?: number; // 百分比最多保留的有效小数位数 decimal?: number; + theme?: Theme; } const Liquid: React.FC = forwardRef( @@ -52,15 +56,18 @@ const Liquid: React.FC = forwardRef( outline, wave, statistic, + theme, ...restConfig }, ref ) => { + const themeConfig = useTheme(theme); + let color; if (dangerPercent && percent >= dangerPercent) { - color = theme.semanticRed; + color = themeConfig.semanticRed; } else if (warningPercent && percent >= warningPercent) { - color = theme.semanticYellow; + color = themeConfig.semanticYellow; } const newConfig: LiquidConfig = { height, @@ -74,13 +81,13 @@ const Liquid: React.FC = forwardRef( percent, liquidStyle: { // 水波背景色 - fill: color || theme.semanticGreen, + fill: color || themeConfig.semanticGreen, radius: 10, ...liquidStyle, }, shapeStyle: { // 形状背景色 - fill: '#F5F8FE', + fill: themeConfig.subColor, radius: 10, ...shapeStyle, }, @@ -103,7 +110,7 @@ const Liquid: React.FC = forwardRef( content: false, ...statistic, }, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return layout === 'horizontal' ? ( @@ -119,7 +126,9 @@ const Liquid: React.FC = forwardRef( height, }} > - + {title} = forwardRef( {title && (
diff --git a/packages/charts/src/Pie/index.tsx b/packages/charts/src/Pie/index.tsx index 6c70d3bce..428ff1c8b 100644 --- a/packages/charts/src/Pie/index.tsx +++ b/packages/charts/src/Pie/index.tsx @@ -2,6 +2,8 @@ import React, { forwardRef } from 'react'; import type { PieConfig as AntPieConfig } from '@ant-design/charts'; import { Pie as AntPie } from '@ant-design/charts'; import { isString } from 'lodash'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; export const measureTextSize = (text: string, font: any = {}) => { const { fontSize, fontFamily = 'sans-serif', fontWeight, fontStyle, fontVariant } = font; @@ -49,6 +51,7 @@ export interface PieConfig extends AntPieConfig { isHalfDonut?: boolean; // 统计标题,仅环图生效 statisticTitle?: string; + theme?: Theme; } const Pie: React.FC = forwardRef( @@ -67,13 +70,15 @@ const Pie: React.FC = forwardRef( interactions, statistic, statisticTitle = '总数', + theme, ...restConfig }, ref ) => { + const themeConfig = useTheme(theme); + // 是否为环图 const isDonut = isDonutProp || isHalfDonut || !!innerRadius; - const titleFontSize = 14; const contentFontSize = 32; @@ -104,6 +109,7 @@ const Pie: React.FC = forwardRef( pieStyle: { // 环图间距为 2,饼图间距为 1 lineWidth: isDonut ? 2 : 1, + stroke: themeConfig.backgroundColor, ...pieStyle, }, legend: legend !== false && { @@ -141,6 +147,7 @@ const Pie: React.FC = forwardRef( style: { fontSize: `${titleFontSize}px`, fontFamily: 'Avenir-Heavy', + color: themeConfig.styleSheet.annotationTextFillColor, ...statistic?.title?.style, }, }, @@ -166,11 +173,12 @@ const Pie: React.FC = forwardRef( style: { fontSize: `${contentFontSize}px`, fontFamily: 'PingFangSC', + color: themeConfig.styleSheet.annotationTextFillColor, ...statistic?.content?.style, }, }, }, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/Score/index.less b/packages/charts/src/Score/index.less index f281c207d..3831caade 100644 --- a/packages/charts/src/Score/index.less +++ b/packages/charts/src/Score/index.less @@ -39,7 +39,6 @@ left: 5%; width: 88%; height: 88%; - background: #fff; border-radius: 70% 60% 60% 60%; .@{prefixCls}-circle-content { position: absolute; diff --git a/packages/charts/src/Score/index.tsx b/packages/charts/src/Score/index.tsx index cf3ff617d..54a98a29b 100644 --- a/packages/charts/src/Score/index.tsx +++ b/packages/charts/src/Score/index.tsx @@ -3,7 +3,8 @@ import { calculateFontSize } from '../util/measureText'; import classNames from 'classnames'; import { sortByNumber } from '@oceanbase/util'; import { toNumber, toString } from 'lodash'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; import './index.less'; export interface ScoreConfig { @@ -13,8 +14,10 @@ export interface ScoreConfig { valueStyle?: React.CSSProperties; unit?: string; unitStyle?: React.CSSProperties; - className?: string; + thresholds?: Record; + theme?: Theme; + className?: string; } const prefixCls = 'ob-score'; @@ -24,11 +27,6 @@ const largeSize = 200; const LINE_HEIGHT = 1.2; const VALUE_FONT_WEIGHT = 500; -const successColor = theme.semanticGreen; -const infoColor = theme.defaultColor; -const warningColor = theme.semanticYellow; -const errorColor = theme.semanticRed; - const Score: React.FC = ({ size, color, @@ -36,11 +34,17 @@ const Score: React.FC = ({ valueStyle, unit, unitStyle, - className, thresholds = {}, + className, + theme, ...restConfig }) => { - const showUnit = unit === '' ? null : unit || '分'; + const themeConfig = useTheme(theme); + + const successColor = themeConfig.semanticGreen; + const infoColor = themeConfig.defaultColor; + const warningColor = themeConfig.semanticYellow; + const errorColor = themeConfig.semanticRed; let currentColor = ''; if (color) { @@ -55,6 +59,8 @@ const Score: React.FC = ({ currentColor = errorColor; } + const showUnit = unit === '' ? null : unit || '分'; + const thresholdList = Object.keys(thresholds) .map(key => ({ value: toNumber(key), @@ -70,46 +76,49 @@ const Score: React.FC = ({ )?.color || currentColor; const bgColorStyle: React.CSSProperties = { - backgroundColor: currentColor - } + backgroundColor: currentColor, + }; const fontColorStyle: React.CSSProperties = { - color: currentColor - } + color: currentColor, + }; - let style: React.CSSProperties = {}; + let style = { + width: 0, + height: 0, + }; if (size) { switch (size) { case 'small': style = { width: smallSize, height: smallSize, - } + }; break; case 'middle': style = { width: middleSize, height: middleSize, - } + }; break; case 'large': style = { width: largeSize, height: largeSize, - } + }; break; default: style = { - width: size, - height: size, - } - break + width: size as number, + height: size as number, + }; + break; } } else { style = { width: middleSize, height: middleSize, - } + }; } const numberFontSize = calculateFontSize( @@ -131,17 +140,35 @@ const Score: React.FC = ({ ); return ( -
+
-
+
- {value || 0} - {showUnit ? {showUnit} : null} + + {value || 0} + + {showUnit ? ( + + {showUnit} + + ) : null}
diff --git a/packages/charts/src/Stat/demo/basic.tsx b/packages/charts/src/Stat/demo/basic.tsx index d577e9b4d..99eda1a42 100644 --- a/packages/charts/src/Stat/demo/basic.tsx +++ b/packages/charts/src/Stat/demo/basic.tsx @@ -7,7 +7,6 @@ export default () => { const [height, setHeight] = useState(100); const [span, setSpan] = useState(8); const [layout, setLayout] = useState('vertical'); - const [themeMode, setThemeMode] = useState('light'); const [colorMode, setColorMode] = useState('background'); const [chartMode, setChartMode] = useState('area'); const [textAlign, setTextAlign] = useState('auto'); @@ -16,11 +15,10 @@ export default () => { height, ...(extra ? { - [extra]: extra === 'prefix' ? '$' : 'USD', - } + [extra]: extra === 'prefix' ? '$' : 'USD', + } : {}), layout, - themeMode, colorMode, chartMode, chartData: [ @@ -76,14 +74,6 @@ export default () => { - - - setThemeMode(e.target.value)}> - Light - Dark - - - setColorMode(e.target.value)}> diff --git a/packages/charts/src/Stat/index.md b/packages/charts/src/Stat/index.md index 7b9ce78b5..5e2ee7936 100644 --- a/packages/charts/src/Stat/index.md +++ b/packages/charts/src/Stat/index.md @@ -11,20 +11,20 @@ nav: ## API -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| :--------- | :------------------- | :-------------------------------- | :----------- | :--- | -| width | 图表宽度 | number | 容器宽度 | - | -| height | 图表高度 | number | 400 | - | -| title | 标题 | string | - | - | -| value | 数值 | number | - | - | -| prefix | 数值前缀 | string | - | - | -| suffix | 数值后缀 | string | - | - | -| layout | 布局 | vertical \| horizontal | vertical | - | -| themeMode | 主题模式 | light \| dark | light | - | +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| :--------- | :------------------- | :-------------------------- | :--------- | :--- | +| width | 图表宽度 | number | 容器宽度 | - | +| height | 图表高度 | number | 400 | - | +| title | 标题 | string | - | - | +| value | 数值 | number | - | - | +| prefix | 数值前缀 | string | - | - | +| suffix | 数值后缀 | string | - | - | +| layout | 布局 | vertical \| horizontal | vertical | - | +| theme | 主题模式 | light \| dark | light | - | | colorMode | 着色模式 | none \| value \| background | background | - | | chartMode | 图表模式 | none \| line \| area | none | - | -| chartData | 图表数据 | number[] | auto | - | -| textAlign | 内容对齐方式 | auto \| center | - | - | -| thresholds | 阈值和颜色值映射对象 | object | - | - | -| className | 类名 | string | - | - | -| style | 样式 | CSSProperties | - | - | +| chartData | 图表数据 | number[] | auto | - | +| textAlign | 内容对齐方式 | auto \| center | - | - | +| thresholds | 阈值和颜色值映射对象 | object | - | - | +| className | 类名 | string | - | - | +| style | 样式 | CSSProperties | - | - | diff --git a/packages/charts/src/Stat/index.tsx b/packages/charts/src/Stat/index.tsx index fb2d674e1..b0c5fc6aa 100644 --- a/packages/charts/src/Stat/index.tsx +++ b/packages/charts/src/Stat/index.tsx @@ -4,10 +4,10 @@ import { toNumber, toString } from 'lodash'; import classNames from 'classnames'; import useResizeObserver from 'use-resize-observer'; import { sortByNumber } from '@oceanbase/util'; -import type { Plot, AllBaseConfig } from '@ant-design/charts'; -import type { TinyAreaConfig } from '../Tiny/TinyArea'; +import type { TinyAreaConfig, TinyAreaRef } from '../Tiny/TinyArea'; import TinyArea from '../Tiny/TinyArea'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; import { calculateFontSize } from '../util/measureText'; import './index.less'; @@ -19,7 +19,7 @@ export interface StatConfig { prefix?: string; suffix?: string; layout?: 'vertical' | 'horizontal'; - themeMode?: 'light' | 'dark'; + theme?: Theme; colorMode?: 'none' | 'value' | 'background'; chartMode?: 'none' | 'line' | 'area'; chartData?: number[]; @@ -53,7 +53,7 @@ const Stat: React.FC = ({ prefix, suffix, layout = 'vertical', - themeMode = 'light', + theme, colorMode = 'background', chartMode, chartData = [], @@ -63,6 +63,7 @@ const Stat: React.FC = ({ style, ...restConfig }) => { + const themeConfig = useTheme(theme); const { ref: containerRef, width: containerWidth = 0, @@ -84,10 +85,10 @@ const Stat: React.FC = ({ !value || (value >= item.value && (!thresholdList[index + 1] || value < thresholdList[index + 1]?.value)) - )?.color || theme.defaultColor; + )?.color || themeConfig.defaultColor; // ref: https://github.com/grafana/grafana/blob/main/packages/grafana-ui/src/components/BigValue/BigValueLayout.tsx#L131 - const themeFactor = themeMode === 'dark' || theme.isDark ? 1 : -0.7; + const themeFactor = themeConfig.theme === 'dark' ? 1 : -0.7; const bgColor1 = tinycolor(color) .darken(15 * themeFactor) .spin(8) @@ -187,9 +188,7 @@ const Stat: React.FC = ({ ); const prefixAndSuffixFontSize = valueFontSize * fontSizeReductionFactor(valueFontSize); - const chartRef = useRef<{ - getChart: () => Plot; - }>(); + const chartRef = useRef(); // 图表占据一半高度 const chartHeight = containerHeight * chartHeightPercent; @@ -285,7 +284,11 @@ const Stat: React.FC = ({
{containerHeight > 0 && hasChart && (
- +
)}
diff --git a/packages/charts/src/Tiny/Progress.tsx b/packages/charts/src/Tiny/Progress.tsx index 79333a848..be9d3139b 100644 --- a/packages/charts/src/Tiny/Progress.tsx +++ b/packages/charts/src/Tiny/Progress.tsx @@ -3,7 +3,8 @@ import type { ProgressConfig as AntProgressConfig } from '@ant-design/charts'; import { Progress as AntProgress } from '@ant-design/charts'; import useResizeObserver from 'use-resize-observer'; import { toPercent } from '../util/number'; -import { theme } from '../theme'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; export interface ProgressConfig extends AntProgressConfig { // 是否为紧凑型布局,此时进度条与标签的间距较小,常用于表格等场景 @@ -20,6 +21,7 @@ export interface ProgressConfig extends AntProgressConfig { formatter?: (percent: number) => React.ReactNode; // 百分比标签样式 percentStyle?: React.CSSProperties; + theme?: Theme; } const Progress: React.FC = forwardRef( @@ -32,12 +34,14 @@ const Progress: React.FC = forwardRef( dangerPercent, decimal = 2, formatter = value => `${toPercent(value, decimal)}%`, - style, percentStyle, + theme, + style, ...restConfig }, ref ) => { + const themeConfig = useTheme(theme); const { ref: titleRef, width: titleWidth } = useResizeObserver({ // 包含 padding 和 border box: 'border-box', @@ -49,9 +53,9 @@ const Progress: React.FC = forwardRef( let color; if (dangerPercent && percent >= dangerPercent) { - color = theme.semanticRed; + color = themeConfig.semanticRed; } else if (warningPercent && percent >= warningPercent) { - color = theme.semanticYellow; + color = themeConfig.semanticYellow; } const padding = compact ? 8 : 16; @@ -59,8 +63,8 @@ const Progress: React.FC = forwardRef( const newConfig: ProgressConfig = { height: 20, percent, - theme: 'ob', - color: [color || theme.defaultColor, theme.barBackgroundColor], + color: [color || themeConfig.defaultColor, themeConfig.barBackgroundColor], + theme: themeConfig.theme, ...restConfig, }; return ( @@ -79,7 +83,7 @@ const Progress: React.FC = forwardRef( textAlign: 'left', // 避免换行 whiteSpace: 'nowrap', - color: color || theme.styleSheet.axisLabelFillColor, + color: color || themeConfig.styleSheet.axisLabelFillColor, }} > {title} diff --git a/packages/charts/src/Tiny/TinyArea.tsx b/packages/charts/src/Tiny/TinyArea.tsx index 8eae6e42e..e19f1f8f1 100644 --- a/packages/charts/src/Tiny/TinyArea.tsx +++ b/packages/charts/src/Tiny/TinyArea.tsx @@ -1,12 +1,22 @@ import React, { forwardRef } from 'react'; import type { TinyAreaConfig as AntTinyAreaConfig } from '@ant-design/charts'; import { TinyArea as AntTinyArea } from '@ant-design/charts'; -import { theme } from '../theme'; +import type { Plot, AllBaseConfig } from '@ant-design/charts'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -export type TinyAreaConfig = AntTinyAreaConfig; +export interface TinyAreaRef { + getChart: () => Plot; +} + +export interface TinyAreaConfig extends AntTinyAreaConfig { + theme?: Theme; +} + +const TinyArea: React.FC = forwardRef( + ({ height = 60, color, line, point, areaStyle, theme, ...restConfig }, ref) => { + const themeConfig = useTheme(theme); -const TinyArea: React.FC = forwardRef( - ({ height = 60, color, line, point, ...restConfig }, ref) => { const newConfig: TinyAreaConfig = { height, line: { @@ -24,14 +34,18 @@ const TinyArea: React.FC = forwardRef( size: 1.5, ...point, style: { - fill: typeof color === 'string' ? color : theme.defaultColor, + fill: typeof color === 'string' ? color : themeConfig.defaultColor, // 去掉边框 lineWidth: 0, ...point.style, }, } : undefined, - theme: 'ob', + areaStyle: { + fill: themeConfig.semanticBlueGradient, + ...areaStyle, + }, + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/Tiny/TinyColumn.tsx b/packages/charts/src/Tiny/TinyColumn.tsx index 374026de6..1657d20a2 100644 --- a/packages/charts/src/Tiny/TinyColumn.tsx +++ b/packages/charts/src/Tiny/TinyColumn.tsx @@ -1,17 +1,27 @@ import React, { forwardRef } from 'react'; import type { TinyColumnConfig as AntTinyColumnConfig } from '@ant-design/charts'; import { TinyColumn as AntTinyColumn } from '@ant-design/charts'; -import { theme } from '../theme'; +// @ts-ignore +import type { Label } from '@antv/g2plot/esm/types/label'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -export type TinyColumnConfig = AntTinyColumnConfig; +export interface TinyColumnConfig extends AntTinyColumnConfig { + theme?: Theme; + label?: Label; + maxColumnWidth?: number; + minColumnWidth?: number; +} const TinyColumn: React.FC = forwardRef( - ({ height = 60, columnStyle, label, ...restConfig }, ref) => { + ({ height = 60, columnStyle, label, theme, ...restConfig }, ref) => { + const themeConfig = useTheme(theme); + const newConfig: TinyColumnConfig = { height, appendPadding: label ? [16, 0, 0, 0] : 0, - maxColumnWidth: theme.columnWidth, - minColumnWidth: theme.columnWidth, + maxColumnWidth: themeConfig.columnWidth, + minColumnWidth: themeConfig.columnWidth, columnStyle: { radius: [2, 2, 0, 0], ...columnStyle, @@ -23,7 +33,7 @@ const TinyColumn: React.FC = forwardRef( ...label, } : undefined, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/Tiny/TinyLine.tsx b/packages/charts/src/Tiny/TinyLine.tsx index 27076385d..a1009df07 100644 --- a/packages/charts/src/Tiny/TinyLine.tsx +++ b/packages/charts/src/Tiny/TinyLine.tsx @@ -1,11 +1,17 @@ import React, { forwardRef } from 'react'; import type { TinyLineConfig as AntTinyLineConfig } from '@ant-design/charts'; import { TinyLine as AntTinyLine } from '@ant-design/charts'; +import { useTheme } from '../theme'; +import type { Theme } from '../theme'; -export type TinyLineConfig = AntTinyLineConfig; +export interface TinyLineConfig extends AntTinyLineConfig { + theme?: Theme; +} const TinyLine: React.FC = forwardRef( - ({ height = 60, color, lineStyle, point, ...restConfig }, ref) => { + ({ height = 60, color, lineStyle, point, theme, ...restConfig }, ref) => { + const themeConfig = useTheme(theme); + const newConfig: TinyLineConfig = { height, color, @@ -27,7 +33,7 @@ const TinyLine: React.FC = forwardRef( }, } : undefined, - theme: 'ob', + theme: themeConfig.theme, ...restConfig, }; return ; diff --git a/packages/charts/src/Tiny/demo/area.tsx b/packages/charts/src/Tiny/demo/area.tsx index 440276846..a0316240a 100644 --- a/packages/charts/src/Tiny/demo/area.tsx +++ b/packages/charts/src/Tiny/demo/area.tsx @@ -1,7 +1,8 @@ -import { TinyArea, theme } from '@oceanbase/charts'; +import { TinyArea, useTheme } from '@oceanbase/charts'; import { Col, Row } from '@oceanbase/design'; export default () => { + const themeConfig = useTheme(); const data = [ 264, 417, 438, 887, 309, 397, 550, 575, 563, 430, 525, 592, 492, 467, 513, 546, 983, 340, 539, 243, 226, 192, @@ -11,10 +12,10 @@ export default () => { }; const config2 = { ...config1, - color: theme.semanticRed, + color: themeConfig.semanticRed, point: {}, areaStyle: { - fill: theme.semanticRedGradient, + fill: themeConfig.semanticRedGradient, }, }; return ( diff --git a/packages/charts/src/Tiny/demo/line.tsx b/packages/charts/src/Tiny/demo/line.tsx index 8485e4407..fe846bd80 100644 --- a/packages/charts/src/Tiny/demo/line.tsx +++ b/packages/charts/src/Tiny/demo/line.tsx @@ -1,7 +1,8 @@ -import { TinyLine, theme } from '@oceanbase/charts'; +import { TinyLine, useTheme } from '@oceanbase/charts'; import { Col, Row } from '@oceanbase/design'; export default () => { + const themeConfig = useTheme(); const data = [ 264, 417, 438, 887, 309, 397, 550, 575, 563, 430, 525, 592, 492, 467, 513, 546, 983, 340, 539, 243, 226, 192, @@ -11,7 +12,7 @@ export default () => { }; const config2 = { ...config1, - color: theme.semanticGreen, + color: themeConfig.semanticGreen, point: {}, }; return ( diff --git a/packages/charts/src/index.ts b/packages/charts/src/index.ts index 2c7107c35..bd998d350 100644 --- a/packages/charts/src/index.ts +++ b/packages/charts/src/index.ts @@ -46,4 +46,8 @@ export type { ProgressConfig } from './Tiny/Progress'; export { default as Score } from './Score'; export type { ScoreConfig } from './Score'; -export { theme } from './theme'; +export { default as ChartProvider } from './ChartProvider'; +export type { ChartProviderProps } from './ChartProvider'; + +export { useTheme } from './theme'; +export type { Theme, ThemeConfig } from './theme'; diff --git a/packages/charts/src/theme/dark.ts b/packages/charts/src/theme/dark.ts new file mode 100644 index 000000000..6ce26f81b --- /dev/null +++ b/packages/charts/src/theme/dark.ts @@ -0,0 +1,141 @@ +import { G2 } from '@ant-design/charts'; +import { merge } from 'lodash'; +import lightTheme from './light'; + +const { registerTheme } = G2; + +// 分类色板 +const COLORS_10 = [ + '#4D97FF', + '#55D3A9', + '#FFC061', + '#5CC3FF', + '#809CD1', + '#A880FF', + '#FF9E9E', + '#6673FF', + '#ADDB43', + '#FF80CC', +]; + +const BLACK_COLORS = { + 100: '#000000', + 95: '#0D0D0D', + 85: '#262626', + 65: '#595959', + 45: '#8C8C8C', + 25: '#BFBFBF', + 15: '#D9D9D9', + 6: '#F0F0F0', +}; + +const WHITE_COLORS = { + 100: '#FFFFFF', + 85: 'rgba(255, 255, 255, 0.85)', + 65: 'rgba(255, 255, 255, 0.65)', + 25: 'rgba(255, 255, 255, 0.25)', +}; + +// 刻度线、坐标轴线、网格线颜色 +const AXIS_LINE_COLOR = WHITE_COLORS[25]; + +const darkTheme = merge({}, lightTheme, { + theme: 'dark', + // 主题色 + defaultColor: COLORS_10[0], + // 分类色,分类个数小于 10 时使用 + colors10: COLORS_10, + // 背景色 + backgroundColor: '#141414', + // 图表辅助色 + subColor: '#424242', + // 语义色 + semanticBlue: COLORS_10[0], + semanticGreen: '#2CAA80', + semanticYellow: '#CC7A00', + semanticRed: '#B30000', + + /* 以下为自定义主题 token */ + // 语义色的渐变色,常用于面积图等区域着色 + semanticBlueGradient: 'l(270) 0:rgba(77,151,255,0) 1:rgba(77,151,255,0.8)', + semanticGreenGradient: 'l(270) 0:rgba(44,170,128,0) 1:rgba(44,170,128,0.8)', + semanticYellowGradient: 'l(270) 0:rgba(204,122,0,0) 1:rgba(204,122,0,0.8)', + semanticRedGradient: 'l(270) 0:rgba(179,0,0,0) 1:rgba(179,0,0,0.8)', + + barBackgroundColor: AXIS_LINE_COLOR, + + components: { + tooltip: { + domStyles: { + 'g2-tooltip': { + backgroundColor: '#424242', + color: WHITE_COLORS[85], + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.5)', + }, + }, + }, + annotation: { + text: { + style: { + stroke: '#ffffff', + }, + }, + }, + }, + + geometries: { + interval: { + // 适用于 Column、Bar 和 Pie 等图表 + rect: { + active: { + style: { + stroke: lightTheme.backgroundColor, + }, + }, + selected: { + style: { + stroke: lightTheme.backgroundColor, + }, + }, + }, + }, + }, + + styleSheet: { + /** 默认颜色 */ + brandColor: COLORS_10[0], + /** 坐标轴线颜色 */ + axisLineBorderColor: AXIS_LINE_COLOR, + + /** 坐标轴刻度线颜色 */ + axisTickLineBorderColor: AXIS_LINE_COLOR, + + /** 坐标轴次刻度线颜色 */ + axisSubTickLineBorderColor: AXIS_LINE_COLOR, + + /** 坐标轴刻度文本颜色 */ + axisLabelFillColor: WHITE_COLORS[65], + + /** 坐标轴网格线颜色 */ + axisGridBorderColor: AXIS_LINE_COLOR, + + /** 图例项文本颜色 */ + legendItemNameFillColor: WHITE_COLORS[85], + + // -------------------- Geometry 图形样式 -------------------- + + // -------------------- Geometry labels -------------------- + + /** label 文本颜色 */ + labelFillColor: WHITE_COLORS[85], + + // -------------------- Annotation,图形标注 -------------------- + /** text 图形标注文本颜色 */ + annotationTextFillColor: WHITE_COLORS[85], + }, +}) as typeof lightTheme; + +// 注册暗色主题 +registerTheme('dark', darkTheme); + +export default darkTheme; diff --git a/packages/charts/src/theme/index.ts b/packages/charts/src/theme/index.ts index 706fe71af..539e1e419 100644 --- a/packages/charts/src/theme/index.ts +++ b/packages/charts/src/theme/index.ts @@ -1,102 +1,20 @@ -import { G2 } from '@ant-design/charts'; - -const { registerTheme } = G2; - -// 分类色板 -const COLORS_10 = [ - '#4C96FF', - '#53D3A9', - '#FFBE5F', - '#5BC4FF', - '#809CD1', - '#A881FF', - '#FF9F9F', - '#6875FF', - '#AEDB43', - '#FF7ECB', -]; - -// 折线宽度 -const LINE_WIDTH = 1.5; - -// 刻度线、坐标轴线、网格线颜色 -const AXIS_LINE_COLOR = '#E2E8F3'; - -// 刻度线、坐标轴线、网格线宽度 -const AXIS_LINE_WIDTH = 0.5; - -export const theme = { - isDark: false, - // 主题色 - defaultColor: COLORS_10[0], - // 分类色,分类个数小于 10 时使用 - colors10: COLORS_10, - // 语义色 - semanticGreen: '#53D3A9', - semanticYellow: '#FFAC33', - semanticRed: '#FF4B4B', - - /* 以下为自定义主题 token */ - // 语义色的渐变色,常用于面积图等区域着色 - semanticGreenGradient: 'l(270) 0:rgba(10,193,133,0) 1:rgba(10,193,133,0.2)', - semanticRedGradient: 'l(270) 0:rgba(255,75,75,0) 1:rgba(255,75,75,0.2)', - // 条形宽度 - barWidth: 12, - barBackgroundColor: AXIS_LINE_COLOR, - // 柱子宽度 - columnWidth: 16, - - styleSheet: { - /** 默认颜色 */ - brandColor: COLORS_10[0], - /** 坐标轴线颜色 */ - axisLineBorderColor: AXIS_LINE_COLOR, - /** 坐标轴线粗细 */ - axisLineBorder: AXIS_LINE_WIDTH, - - /** 坐标轴刻度线颜色 */ - axisTickLineBorderColor: AXIS_LINE_COLOR, - /** 坐标轴刻度线粗细 */ - axisTickLineBorder: AXIS_LINE_WIDTH, - - /** 坐标轴次刻度线颜色 */ - axisSubTickLineBorderColor: AXIS_LINE_COLOR, - /** 坐标轴次刻度线粗细 */ - axisSubTickLineBorder: AXIS_LINE_WIDTH, - - /** 坐标轴刻度文本颜色 */ - axisLabelFillColor: '#8592AD', - - /** 坐标轴网格线颜色 */ - axisGridBorderColor: AXIS_LINE_COLOR, - /** 坐标轴网格线粗细 */ - axisGridBorder: AXIS_LINE_WIDTH, - - // -------------------- Legend 图例 -------------------- - /** 图例与图表绘图区域的偏移距离 */ - legendPadding: [16, 16, 16, 16], - /** 水平布局的图例与绘图区域偏移距离 */ - legendHorizontalPadding: [16, 0, 0, 0], - /** 垂直布局的图例与绘图区域偏移距离 */ - legendVerticalPadding: [0, 16, 0, 16], - - /** 图例项文本颜色 */ - legendItemNameFillColor: '#5C6B8A', - - // -------------------- Geometry 图形样式-------------------- - /** 线图粗细 */ - lineBorder: LINE_WIDTH, - - /** 线图 Active 状态下粗细 */ - lineActiveBorder: 2.5, - - /** 线图 selected 状态下粗细 */ - lineSelectedBorder: 2.5, - - /** 线图 inactive 状态下透明度 */ - lineInactiveBorderOpacity: 0.3, - }, -}; - -// 注册主题 -registerTheme('ob', theme); +import React, { useContext } from 'react'; +import ChartProvider from '../ChartProvider'; +import type { Theme } from '../ChartProvider'; +import lightTheme from './light'; +import darkTheme from './dark'; + +export type { Theme }; + +export type ThemeConfig = typeof lightTheme; + +export function useTheme(customizeTheme?: Theme) { + const { theme: globalTheme } = useContext(ChartProvider.ChartContext); + const theme = customizeTheme || globalTheme; + const themeMap = { + light: lightTheme, + dark: darkTheme, + }; + const themeConfig = typeof theme === 'object' ? theme : themeMap[theme] || lightTheme; + return themeConfig as ThemeConfig; +} diff --git a/packages/charts/src/theme/light.ts b/packages/charts/src/theme/light.ts new file mode 100644 index 000000000..d0fe79357 --- /dev/null +++ b/packages/charts/src/theme/light.ts @@ -0,0 +1,137 @@ +import { G2 } from '@ant-design/charts'; + +const { registerTheme } = G2; + +// 分类色板 +const COLORS_10 = [ + '#4C96FF', + '#53D3A9', + '#FFBE5F', + '#5BC4FF', + '#809CD1', + '#A881FF', + '#FF9F9F', + '#6875FF', + '#AEDB43', + '#FF7ECB', +]; + +const BLACK_COLORS = { + 100: '#000000', + 85: '#5C6B8A', + 45: '#8592AD', + 15: '#E2E8F3', +}; + +const WHITE_COLORS = { + 100: '#FFFFFF', + 95: '#F2F2F2', + 85: '#D9D9D9', + 65: '#A6A6A6', + 45: '#737373', + 25: '#404040', + 15: '#262626', + 6: '#0F0F0F', +}; + +// 折线宽度 +const LINE_WIDTH = 1.5; + +// 刻度线、坐标轴线、网格线颜色 +const AXIS_LINE_COLOR = BLACK_COLORS[15]; + +// 刻度线、坐标轴线、网格线宽度 +const AXIS_LINE_WIDTH = 0.5; + +const lightTheme = { + theme: 'light', + // 主题色 + defaultColor: COLORS_10[0], + // 分类色,分类个数小于 10 时使用 + colors10: COLORS_10, + // 背景色 + backgroundColor: '#ffffff', + // 图表辅助色 + subColor: '#F5F8FE', + // 语义色 + semanticBlue: COLORS_10[0], + semanticGreen: '#53D3A9', + semanticYellow: '#FFAC33', + semanticRed: '#FF4B4B', + + /* 以下为自定义主题 token */ + // 语义色的渐变色,常用于面积图等区域着色 + semanticBlueGradient: 'l(270) 0:rgba(76,150,255,0) 1:rgba(76,150,255,0.2)', + semanticGreenGradient: 'l(270) 0:rgba(83,211,169,0) 1:rgba(83,211,169,0.2)', + semanticYellowGradient: 'l(270) 0:rgba(255,172,51,0) 1:rgba(255,172,51,0.2)', + semanticRedGradient: 'l(270) 0:rgba(255,75,75,0) 1:rgba(255,75,75,0.2)', + // 条形宽度 + barWidth: 12, + barBackgroundColor: AXIS_LINE_COLOR, + // 柱子宽度 + columnWidth: 16, + styleSheet: { + /** 默认颜色 */ + brandColor: COLORS_10[0], + /** 坐标轴线颜色 */ + axisLineBorderColor: AXIS_LINE_COLOR, + /** 坐标轴线粗细 */ + axisLineBorder: AXIS_LINE_WIDTH, + + /** 坐标轴刻度线颜色 */ + axisTickLineBorderColor: AXIS_LINE_COLOR, + /** 坐标轴刻度线粗细 */ + axisTickLineBorder: AXIS_LINE_WIDTH, + + /** 坐标轴次刻度线颜色 */ + axisSubTickLineBorderColor: AXIS_LINE_COLOR, + /** 坐标轴次刻度线粗细 */ + axisSubTickLineBorder: AXIS_LINE_WIDTH, + + /** 坐标轴刻度文本颜色 */ + axisLabelFillColor: BLACK_COLORS[45], + + /** 坐标轴网格线颜色 */ + axisGridBorderColor: AXIS_LINE_COLOR, + /** 坐标轴网格线粗细 */ + axisGridBorder: AXIS_LINE_WIDTH, + + // -------------------- Legend 图例 -------------------- + /** 图例与图表绘图区域的偏移距离 */ + legendPadding: [16, 16, 16, 16], + /** 水平布局的图例与绘图区域偏移距离 */ + legendHorizontalPadding: [16, 0, 0, 0], + /** 垂直布局的图例与绘图区域偏移距离 */ + legendVerticalPadding: [0, 16, 0, 16], + + /** 图例项文本颜色 */ + legendItemNameFillColor: BLACK_COLORS[85], + + // -------------------- Geometry 图形样式-------------------- + + /** 线图粗细 */ + lineBorder: LINE_WIDTH, + + /** 线图 Active 状态下粗细 */ + lineActiveBorder: 2.5, + + /** 线图 selected 状态下粗细 */ + lineSelectedBorder: 2.5, + + /** 线图 inactive 状态下透明度 */ + lineInactiveBorderOpacity: 0.3, + + // -------------------- Geometry labels -------------------- + /** label 文本颜色 */ + labelFillColor: BLACK_COLORS[85], + + // -------------------- Annotation,图形标注 -------------------- + /** text 图形标注文本颜色 */ + annotationTextFillColor: BLACK_COLORS[85], + }, +}; + +// 注册浅色主题 +registerTheme('light', lightTheme); + +export default lightTheme; diff --git a/tests/setupTests.ts b/tests/setupTests.ts index b54b030f1..f58bea5d2 100644 --- a/tests/setupTests.ts +++ b/tests/setupTests.ts @@ -19,6 +19,10 @@ jest.mock('react', () => ({ jest.setTimeout(60000); +if (typeof window.URL.createObjectURL === 'undefined') { + window.URL.createObjectURL = jest.fn(); +} + /* eslint-disable global-require */ if (typeof window !== 'undefined') { // ref: https://github.com/ant-design/ant-design/issues/18774