From 64e9451e17a857cc628fff7f1cc1efc9f1ea4ec0 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Thu, 20 Jun 2019 12:11:22 -0500 Subject: [PATCH] feat(theme): allow recursive partial theme (#239) * build(tsconfig): update tsconfigs to extend from a base config The main tsconfig.json is what vscode uses to parse ts files. Adding a global type would not available in excluded test files. Extended base tsconfig to current match current build config. * feat(theme): allow recursive partial themes Allow recursive partial themes to be passed to the Setting component. Added RecursivePartial global type. Issue #201 * test(settings): add tests for partial theme to settings component * refactor: global type inclusion and story config Moved global type to be utils. Updated storybook config with custom tsconfig file. * refactor: remove enum declaration --- packages/osd-charts/.playground/tsconfig.json | 5 +- .../osd-charts/.playground/webpack.config.js | 2 +- packages/osd-charts/.storybook/tsconfig.json | 4 + .../osd-charts/.storybook/webpack.config.js | 20 +- packages/osd-charts/jest.config.json | 13 +- packages/osd-charts/package.json | 4 +- .../osd-charts/src/lib/themes/theme.test.ts | 658 +++++++++--------- packages/osd-charts/src/lib/themes/theme.ts | 94 +-- .../osd-charts/src/lib/utils/commons.test.ts | 109 ++- packages/osd-charts/src/lib/utils/commons.ts | 55 ++ .../osd-charts/src/specs/settings.test.tsx | 43 +- packages/osd-charts/src/specs/settings.tsx | 23 +- packages/osd-charts/stories/styling.tsx | 46 ++ packages/osd-charts/tsconfig.all.json | 4 - packages/osd-charts/tsconfig.jest.json | 6 + packages/osd-charts/tsconfig.json | 16 +- packages/osd-charts/tsconfig.lib.json | 8 + 17 files changed, 650 insertions(+), 460 deletions(-) create mode 100644 packages/osd-charts/.storybook/tsconfig.json delete mode 100644 packages/osd-charts/tsconfig.all.json create mode 100644 packages/osd-charts/tsconfig.jest.json create mode 100644 packages/osd-charts/tsconfig.lib.json diff --git a/packages/osd-charts/.playground/tsconfig.json b/packages/osd-charts/.playground/tsconfig.json index 9886df3705ff..58bfadadc73a 100644 --- a/packages/osd-charts/.playground/tsconfig.json +++ b/packages/osd-charts/.playground/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "downlevelIteration": true, "target": "es5" - } + }, + "exclude": [ + "../**/*.test.*" + ] } diff --git a/packages/osd-charts/.playground/webpack.config.js b/packages/osd-charts/.playground/webpack.config.js index 3fbf7b4f6ef9..a912cf118f79 100644 --- a/packages/osd-charts/.playground/webpack.config.js +++ b/packages/osd-charts/.playground/webpack.config.js @@ -12,7 +12,7 @@ module.exports = { loader: 'ts-loader', exclude: /node_modules/, options: { - configFile: './tsconfig.json', + configFile: 'tsconfig.json', }, }, { diff --git a/packages/osd-charts/.storybook/tsconfig.json b/packages/osd-charts/.storybook/tsconfig.json new file mode 100644 index 000000000000..6a7d4939df8c --- /dev/null +++ b/packages/osd-charts/.storybook/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig", + "exclude": ["../**/*.test.*"] +} diff --git a/packages/osd-charts/.storybook/webpack.config.js b/packages/osd-charts/.storybook/webpack.config.js index 8ec0f52ea374..9535e16bc1c1 100644 --- a/packages/osd-charts/.storybook/webpack.config.js +++ b/packages/osd-charts/.storybook/webpack.config.js @@ -8,15 +8,17 @@ module.exports = (baseConfig, env, config) => { config.devtool = 'source-map'; } config.module.rules.push({ - test: /\.(ts|tsx)$/, - use: [ - { - loader: require.resolve('ts-loader'), - }, - { - loader: require.resolve('react-docgen-typescript-loader'), - }, - ], + test: /\.tsx?$/, + loader: require.resolve('ts-loader'), + exclude: /node_modules/, + options: { + configFile: 'tsconfig.json', + }, + }); + config.module.rules.push({ + test: /\.tsx?$/, + loader: require.resolve('react-docgen-typescript-loader'), + exclude: /node_modules/, }); config.module.rules.push({ test: /\.tsx?$/, diff --git a/packages/osd-charts/jest.config.json b/packages/osd-charts/jest.config.json index 93ecde46c12f..e2dfabdf1433 100644 --- a/packages/osd-charts/jest.config.json +++ b/packages/osd-charts/jest.config.json @@ -1,6 +1,15 @@ { - "roots": ["/src"], + "roots": [ + "/src" + ], "preset": "ts-jest", "testEnvironment": "jest-environment-jsdom-fourteen", - "setupFilesAfterEnv": ["/scripts/setup_enzyme.ts"] + "setupFilesAfterEnv": [ + "/scripts/setup_enzyme.ts" + ], + "globals": { + "ts-jest": { + "tsConfig": "tsconfig.jest.json" + } + } } diff --git a/packages/osd-charts/package.json b/packages/osd-charts/package.json index 5d758934a06c..4e605741eb52 100644 --- a/packages/osd-charts/package.json +++ b/packages/osd-charts/package.json @@ -14,7 +14,7 @@ "scripts": { "cz": "git-cz", "build:clean": "rm -rf ./dist", - "build:ts": "tsc -p ./tsconfig.json", + "build:ts": "tsc -p ./tsconfig.lib.json", "build:sass": "node-sass src/theme_light.scss dist/theme_light.css --output-style compressed && node-sass src/theme_dark.scss dist/theme_dark.css --output-style compressed && node-sass src/theme_only_light.scss dist/theme_only_light.css --output-style compressed && node-sass src/theme_only_dark.scss dist/theme_only_dark.css --output-style compressed", "concat:sass": "node scripts/concat_sass.js", "build": "yarn build:clean && yarn build:ts && yarn build:sass && yarn concat:sass", @@ -27,7 +27,7 @@ "watch": "yarn test --watch", "semantic-release": "semantic-release", "typecheck:src": "yarn build:ts --noEmit", - "typecheck:all": "tsc -p ./tsconfig.all.json --noEmit", + "typecheck:all": "tsc -p ./tsconfig.json --noEmit", "playground": "cd .playground && webpack-dev-server" }, "files": [ diff --git a/packages/osd-charts/src/lib/themes/theme.test.ts b/packages/osd-charts/src/lib/themes/theme.test.ts index 4f29b802e7e3..03b6b6d0275a 100644 --- a/packages/osd-charts/src/lib/themes/theme.test.ts +++ b/packages/osd-charts/src/lib/themes/theme.test.ts @@ -3,25 +3,19 @@ import { DARK_THEME } from './dark_theme'; import { LIGHT_THEME } from './light_theme'; import { AreaSeriesStyle, - AxisConfig, - BarSeriesStyle, - ColorConfig, - CrosshairStyle, DEFAULT_ANNOTATION_LINE_STYLE, DEFAULT_ANNOTATION_RECT_STYLE, DEFAULT_GRID_LINE_CONFIG, - LegendStyle, LineSeriesStyle, mergeWithDefaultAnnotationLine, mergeWithDefaultAnnotationRect, mergeWithDefaultGridLineConfig, mergeWithDefaultTheme, - ScalesConfig, - SharedGeometryStyle, + PartialTheme, Theme, } from './theme'; -describe('Themes', () => { +describe('Theme', () => { let CLONED_LIGHT_THEME: Theme; let CLONED_DARK_THEME: Theme; @@ -36,367 +30,355 @@ describe('Themes', () => { expect(DARK_THEME).toEqual(CLONED_DARK_THEME); }); - it('should merge partial theme: margins', () => { - const customTheme = mergeWithDefaultTheme({ - chartMargins: { - bottom: 314571, - top: 314571, - left: 314571, - right: 314571, - }, + describe('mergeWithDefaultGridLineConfig', () => { + it('should merge partial grid line configs', () => { + const fullConfig = { + stroke: 'foo', + strokeWidth: 1, + opacity: 0, + dash: [0, 0], + }; + expect(mergeWithDefaultGridLineConfig(fullConfig)).toEqual(fullConfig); + expect(mergeWithDefaultGridLineConfig({})).toEqual(DEFAULT_GRID_LINE_CONFIG); }); - expect(customTheme.chartMargins).toBeDefined(); - expect(customTheme.chartMargins.bottom).toBe(314571); - expect(customTheme.chartMargins.left).toBe(314571); - expect(customTheme.chartMargins.right).toBe(314571); - expect(customTheme.chartMargins.top).toBe(314571); }); - it('should merge partial theme: paddings', () => { - const chartPaddings: Margins = { - bottom: 314571, - top: 314571, - left: 314571, - right: 314571, - }; - const customTheme = mergeWithDefaultTheme({ - chartPaddings, - }); - expect(customTheme.chartPaddings).toBeDefined(); - expect(customTheme.chartPaddings.bottom).toBe(314571); - expect(customTheme.chartPaddings.left).toBe(314571); - expect(customTheme.chartPaddings.right).toBe(314571); - expect(customTheme.chartPaddings.top).toBe(314571); - const customDarkTheme = mergeWithDefaultTheme( - { - chartPaddings, - }, - DARK_THEME, - ); - expect(customDarkTheme.chartPaddings).toEqual(chartPaddings); - }); + describe('mergeWithDefaultAnnotationLine', () => { + it('should merge custom and default annotation line configs', () => { + expect(mergeWithDefaultAnnotationLine()).toEqual(DEFAULT_ANNOTATION_LINE_STYLE); - it('should merge partial theme: lineSeriesStyle', () => { - const lineSeriesStyle: LineSeriesStyle = { - line: { - stroke: 'elastic_charts', - strokeWidth: 314571, - visible: true, - }, - border: { - stroke: 'elastic_charts', - strokeWidth: 314571, - visible: true, - }, - point: { - radius: 314571, - stroke: 'elastic_charts', - strokeWidth: 314571, - visible: true, - opacity: 314571, - }, - }; - const customTheme = mergeWithDefaultTheme({ - lineSeriesStyle, - }); - expect(customTheme.lineSeriesStyle).toEqual(lineSeriesStyle); - const customDarkTheme = mergeWithDefaultTheme( - { - lineSeriesStyle, - }, - DARK_THEME, - ); - expect(customDarkTheme.lineSeriesStyle).toEqual(lineSeriesStyle); - }); + const customLineConfig = { + stroke: 'foo', + strokeWidth: 50, + opacity: 1, + }; - it('should merge partial theme: areaSeriesStyle', () => { - const areaSeriesStyle: AreaSeriesStyle = { - area: { - fill: 'elastic_charts', - visible: true, - opacity: 314571, - }, - line: { - stroke: 'elastic_charts', - strokeWidth: 314571, - visible: true, - }, - border: { - stroke: 'elastic_charts', - strokeWidth: 314571, - visible: true, - }, - point: { - visible: true, - radius: 314571, - stroke: 'elastic_charts', - strokeWidth: 314571, - opacity: 314571, - }, - }; - const customTheme = mergeWithDefaultTheme({ - areaSeriesStyle, - }); - expect(customTheme.areaSeriesStyle).toEqual(areaSeriesStyle); - const customDarkTheme = mergeWithDefaultTheme( - { - areaSeriesStyle, - }, - DARK_THEME, - ); - expect(customDarkTheme.areaSeriesStyle).toEqual(areaSeriesStyle); - }); + const defaultLineConfig = { + stroke: '#000', + strokeWidth: 3, + opacity: 1, + }; - it('should merge partial theme: barSeriesStyle', () => { - const barSeriesStyle: BarSeriesStyle = { - border: { - stroke: 'elastic_charts', - strokeWidth: 314571, - visible: true, - }, - displayValue: { - fontSize: 10, + const customDetailsConfig = { + fontSize: 50, + fontFamily: 'custom-font-family', fontStyle: 'custom-font-style', - fontFamily: 'custom-font', - padding: 5, fill: 'custom-fill', - }, - }; - const customTheme = mergeWithDefaultTheme({ - barSeriesStyle, - }); - expect(customTheme.barSeriesStyle).toEqual(barSeriesStyle); - const customDarkTheme = mergeWithDefaultTheme( - { - barSeriesStyle, - }, - DARK_THEME, - ); - expect(customDarkTheme.barSeriesStyle).toEqual(barSeriesStyle); - }); + padding: 20, + }; - it('should merge partial theme: sharedStyle', () => { - const sharedStyle: SharedGeometryStyle = { - default: { - opacity: 314571, - }, - highlighted: { - opacity: 314571, - }, - unhighlighted: { - opacity: 314571, - }, - }; - const customTheme = mergeWithDefaultTheme({ - sharedStyle, - }); - expect(customTheme.sharedStyle).toEqual(sharedStyle); - const customDarkTheme = mergeWithDefaultTheme( - { - sharedStyle, - }, - DARK_THEME, - ); - expect(customDarkTheme.sharedStyle).toEqual(sharedStyle); - }); + const defaultDetailsConfig = { + fontSize: 10, + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + fontStyle: 'normal', + fill: 'gray', + padding: 0, + }; - it('should merge partial theme: scales', () => { - const scales: ScalesConfig = { - barsPadding: 314571, - histogramPadding: 0.05, - }; - const customTheme = mergeWithDefaultTheme({ - scales, - }); - expect(customTheme.scales).toEqual(scales); - const customDarkTheme = mergeWithDefaultTheme( - { - scales, - }, - DARK_THEME, - ); - expect(customDarkTheme.scales).toEqual(scales); - }); + const expectedMergedCustomLineConfig = { line: customLineConfig, details: defaultDetailsConfig }; + const mergedCustomLineConfig = mergeWithDefaultAnnotationLine({ line: customLineConfig }); + expect(mergedCustomLineConfig).toEqual(expectedMergedCustomLineConfig); - it('should merge partial theme: axes', () => { - const axes: AxisConfig = { - axisTitleStyle: { - fontSize: 314571, - fontStyle: 'elastic_charts', - fontFamily: 'elastic_charts', - padding: 314571, - fill: 'elastic_charts', - }, - axisLineStyle: { - stroke: 'elastic_charts', - strokeWidth: 314571, - }, - tickLabelStyle: { - fontSize: 314571, - fontFamily: 'elastic_charts', - fontStyle: 'elastic_charts', - fill: 'elastic_charts', - padding: 314571, - }, - tickLineStyle: { - stroke: 'elastic_charts', - strokeWidth: 314571, - }, - }; - const customTheme = mergeWithDefaultTheme({ - axes, + const expectedMergedCustomDetailsConfig = { line: defaultLineConfig, details: customDetailsConfig }; + const mergedCustomDetailsConfig = mergeWithDefaultAnnotationLine({ details: customDetailsConfig }); + expect(mergedCustomDetailsConfig).toEqual(expectedMergedCustomDetailsConfig); }); - expect(customTheme.axes).toEqual(axes); - const customDarkTheme = mergeWithDefaultTheme( - { - axes, - }, - DARK_THEME, - ); - expect(customDarkTheme.axes).toEqual(axes); }); - it('should merge partial theme: colors', () => { - const colors: ColorConfig = { - vizColors: ['elastic_charts_c1', 'elastic_charts_c2'], - defaultVizColor: 'elastic_charts', - }; - const customTheme = mergeWithDefaultTheme({ - colors, - }); - expect(customTheme.colors).toEqual(colors); - const customDarkTheme = mergeWithDefaultTheme( - { - colors, - }, - DARK_THEME, - ); - expect(customDarkTheme.colors).toEqual(colors); + describe('mergeWithDefaultAnnotationRect', () => { + it('should merge custom and default rect annotation style', () => { + expect(mergeWithDefaultAnnotationRect()).toEqual(DEFAULT_ANNOTATION_RECT_STYLE); - const vizColors: Partial = { - vizColors: ['elastic_charts_c1', 'elastic_charts_c2'], - }; - const partialUpdatedCustomTheme = mergeWithDefaultTheme({ - colors: vizColors, - }); - expect(partialUpdatedCustomTheme.colors.vizColors).toEqual(vizColors.vizColors); + const customConfig = { + stroke: 'customStroke', + fill: 'customFill', + }; + + const expectedMergedConfig = { + stroke: 'customStroke', + fill: 'customFill', + opacity: 0.5, + strokeWidth: 1, + }; - const defaultVizColor: Partial = { - defaultVizColor: 'elastic_charts', - }; - const partialUpdated2CustomTheme = mergeWithDefaultTheme({ - colors: defaultVizColor, + expect(mergeWithDefaultAnnotationRect(customConfig)).toEqual(expectedMergedConfig); }); - expect(partialUpdated2CustomTheme.colors.defaultVizColor).toEqual(defaultVizColor.defaultVizColor); }); - it('should merge partial theme: legends', () => { - const legend: LegendStyle = { - verticalWidth: 314571, - horizontalHeight: 314571, - }; - const customTheme = mergeWithDefaultTheme({ - legend, + describe('mergeWithDefaultTheme', () => { + it('should merge partial theme: margins', () => { + const customTheme = mergeWithDefaultTheme({ + chartMargins: { + bottom: 314571, + top: 314571, + left: 314571, + right: 314571, + }, + }); + expect(customTheme.chartMargins).toBeDefined(); + expect(customTheme.chartMargins.bottom).toBe(314571); + expect(customTheme.chartMargins.left).toBe(314571); + expect(customTheme.chartMargins.right).toBe(314571); + expect(customTheme.chartMargins.top).toBe(314571); }); - expect(customTheme.legend).toEqual(legend); - const customDarkTheme = mergeWithDefaultTheme( - { - legend, - }, - DARK_THEME, - ); - expect(customDarkTheme.legend).toEqual(legend); - }); - it('should merge partial theme: crosshair', () => { - const crosshair: CrosshairStyle = { - band: { - visible: false, - fill: 'elastic_charts_c1', - }, - line: { - visible: false, - stroke: 'elastic_charts_c1', - strokeWidth: 314571, - }, - }; - const customTheme = mergeWithDefaultTheme({ - crosshair, + + it('should merge partial theme: paddings', () => { + const chartPaddings: Margins = { + bottom: 314571, + top: 314571, + left: 314571, + right: 314571, + }; + const customTheme = mergeWithDefaultTheme({ + chartPaddings, + }); + expect(customTheme.chartPaddings).toBeDefined(); + expect(customTheme.chartPaddings.bottom).toBe(314571); + expect(customTheme.chartPaddings.left).toBe(314571); + expect(customTheme.chartPaddings.right).toBe(314571); + expect(customTheme.chartPaddings.top).toBe(314571); + const customDarkTheme = mergeWithDefaultTheme( + { + chartPaddings, + }, + DARK_THEME, + ); + expect(customDarkTheme.chartPaddings).toEqual(chartPaddings); }); - expect(customTheme.crosshair).toEqual(crosshair); - const customDarkTheme = mergeWithDefaultTheme( - { - crosshair, - }, - DARK_THEME, - ); - expect(customDarkTheme.crosshair).toEqual(crosshair); - }); - it('should merge partial grid line configs', () => { - const fullConfig = { - stroke: 'foo', - strokeWidth: 1, - opacity: 0, - dash: [0, 0], - }; - expect(mergeWithDefaultGridLineConfig(fullConfig)).toEqual(fullConfig); - expect(mergeWithDefaultGridLineConfig({})).toEqual(DEFAULT_GRID_LINE_CONFIG); - }); + it('should merge partial theme: lineSeriesStyle', () => { + const lineSeriesStyle: LineSeriesStyle = { + line: { + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + }, + border: { + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + }, + point: { + radius: 314571, + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + opacity: 314571, + }, + }; + const customTheme = mergeWithDefaultTheme({ + lineSeriesStyle, + }); + expect(customTheme.lineSeriesStyle).toEqual(lineSeriesStyle); + const customDarkTheme = mergeWithDefaultTheme( + { + lineSeriesStyle, + }, + DARK_THEME, + ); + expect(customDarkTheme.lineSeriesStyle).toEqual(lineSeriesStyle); + }); - it('should merge custom and default annotation line configs', () => { - expect(mergeWithDefaultAnnotationLine()).toEqual(DEFAULT_ANNOTATION_LINE_STYLE); + it('should merge partial theme: areaSeriesStyle', () => { + const areaSeriesStyle: AreaSeriesStyle = { + area: { + fill: 'elastic_charts', + visible: true, + opacity: 314571, + }, + line: { + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + }, + border: { + stroke: 'elastic_charts', + strokeWidth: 314571, + visible: true, + }, + point: { + visible: true, + radius: 314571, + stroke: 'elastic_charts', + strokeWidth: 314571, + opacity: 314571, + }, + }; + const customTheme = mergeWithDefaultTheme({ + areaSeriesStyle, + }); + expect(customTheme.areaSeriesStyle).toEqual(areaSeriesStyle); + const customDarkTheme = mergeWithDefaultTheme( + { + areaSeriesStyle, + }, + DARK_THEME, + ); + expect(customDarkTheme.areaSeriesStyle).toEqual(areaSeriesStyle); + }); - const customLineConfig = { - stroke: 'foo', - strokeWidth: 50, - opacity: 1, - }; + it('should merge partial theme: barSeriesStyle', () => { + const partialTheme: PartialTheme = { + barSeriesStyle: { + border: { + stroke: 'elastic_charts', + }, + displayValue: { + fontSize: 10, + fontStyle: 'custom-font-style', + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + barSeriesStyle: { + border: { + ...DARK_THEME.barSeriesStyle.border, + ...partialTheme!.barSeriesStyle!.border, + }, + displayValue: { + ...DARK_THEME.barSeriesStyle.displayValue, + ...partialTheme!.barSeriesStyle!.displayValue, + }, + }, + }); + }); - const defaultLineConfig = { - stroke: '#000', - strokeWidth: 3, - opacity: 1, - }; + it('should merge partial theme: sharedStyle', () => { + const partialTheme: PartialTheme = { + sharedStyle: { + highlighted: { + opacity: 100, + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + sharedStyle: { + ...DARK_THEME.sharedStyle, + highlighted: { + ...DARK_THEME.sharedStyle.highlighted, + ...partialTheme!.sharedStyle!.highlighted, + }, + }, + }); + }); - const customDetailsConfig = { - fontSize: 50, - fontFamily: 'custom-font-family', - fontStyle: 'custom-font-style', - fill: 'custom-fill', - padding: 20, - }; + it('should merge partial theme: scales', () => { + const partialTheme: PartialTheme = { + scales: { + barsPadding: 314571, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + scales: { + ...DARK_THEME.scales, + ...partialTheme!.scales, + }, + }); + }); - const defaultDetailsConfig = { - fontSize: 10, - fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, - fontStyle: 'normal', - fill: 'gray', - padding: 0, - }; + it('should merge partial theme: axes', () => { + const partialTheme: PartialTheme = { + axes: { + axisTitleStyle: { + fontStyle: 'elastic_charts', + }, + axisLineStyle: { + stroke: 'elastic_charts', + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + axes: { + ...DARK_THEME.axes, + axisTitleStyle: { + ...DARK_THEME.axes.axisTitleStyle, + ...partialTheme!.axes!.axisTitleStyle, + }, + axisLineStyle: { + ...DARK_THEME.axes.axisLineStyle, + ...partialTheme!.axes!.axisLineStyle, + }, + }, + }); + }); - const expectedMergedCustomLineConfig = { line: customLineConfig, details: defaultDetailsConfig }; - const mergedCustomLineConfig = mergeWithDefaultAnnotationLine({ line: customLineConfig }); - expect(mergedCustomLineConfig).toEqual(expectedMergedCustomLineConfig); + it('should merge partial theme: colors', () => { + const partialTheme: PartialTheme = { + colors: { + vizColors: ['elastic_charts_c1', 'elastic_charts_c2'], + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + colors: { + ...DARK_THEME.colors, + ...partialTheme!.colors, + }, + }); + }); - const expectedMergedCustomDetailsConfig = { line: defaultLineConfig, details: customDetailsConfig }; - const mergedCustomDetailsConfig = mergeWithDefaultAnnotationLine({ details: customDetailsConfig }); - expect(mergedCustomDetailsConfig).toEqual(expectedMergedCustomDetailsConfig); - }); - it('should merge custom and default rect annotation style', () => { - expect(mergeWithDefaultAnnotationRect()).toEqual(DEFAULT_ANNOTATION_RECT_STYLE); + it('should merge partial theme: legend', () => { + const partialTheme: PartialTheme = { + legend: { + horizontalHeight: 314571, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + legend: { + ...DARK_THEME.legend, + ...partialTheme!.legend, + }, + }); + }); - const customConfig = { - stroke: 'customStroke', - fill: 'customFill', - }; + it('should merge partial theme: crosshair', () => { + const partialTheme: PartialTheme = { + crosshair: { + band: { + fill: 'elastic_charts_c1', + }, + line: { + strokeWidth: 314571, + }, + }, + }; + const mergedTheme = mergeWithDefaultTheme(partialTheme, DARK_THEME); + expect(mergedTheme).toEqual({ + ...DARK_THEME, + crosshair: { + ...DARK_THEME.crosshair, + band: { + ...DARK_THEME.crosshair.band, + ...partialTheme!.crosshair!.band, + }, + line: { + ...DARK_THEME.crosshair.line, + ...partialTheme!.crosshair!.line, + }, + }, + }); + }); - const expectedMergedConfig = { - stroke: 'customStroke', - fill: 'customFill', - opacity: 0.5, - strokeWidth: 1, - }; + it('should override all values if provided', () => { + const mergedTheme = mergeWithDefaultTheme(LIGHT_THEME, DARK_THEME); + expect(mergedTheme).toEqual(LIGHT_THEME); + }); - expect(mergeWithDefaultAnnotationRect(customConfig)).toEqual(expectedMergedConfig); + it('should default to LIGHT_THEME', () => { + const partialTheme: PartialTheme = {}; + const mergedTheme = mergeWithDefaultTheme(partialTheme); + expect(mergedTheme).toEqual(LIGHT_THEME); + }); }); }); diff --git a/packages/osd-charts/src/lib/themes/theme.ts b/packages/osd-charts/src/lib/themes/theme.ts index cbd3b56e2d16..5eff91b02038 100644 --- a/packages/osd-charts/src/lib/themes/theme.ts +++ b/packages/osd-charts/src/lib/themes/theme.ts @@ -1,4 +1,5 @@ import { GeometryStyle } from '../series/rendering'; +import { mergePartial, RecursivePartial } from '../utils/commons'; import { Margins } from '../utils/dimensions'; import { LIGHT_THEME } from './light_theme'; @@ -97,6 +98,14 @@ export interface Theme { crosshair: CrosshairStyle; } +export type PartialTheme = RecursivePartial; + +export type BaseThemeType = 'light' | 'dark'; +export const BaseThemeTypes: Readonly<{ [key: string]: BaseThemeType }> = Object.freeze({ + Light: 'light', + Dark: 'dark', +}); + export type DisplayValueStyle = TextStyle & { offsetX?: number; offsetY?: number; @@ -137,20 +146,6 @@ export interface LineAnnotationStyle { export type RectAnnotationStyle = StrokeStyle & FillStyle & Opacity; -export interface PartialTheme { - chartMargins?: Margins; - chartPaddings?: Margins; - lineSeriesStyle?: LineSeriesStyle; - areaSeriesStyle?: AreaSeriesStyle; - barSeriesStyle?: BarSeriesStyle; - sharedStyle?: SharedGeometryStyle; - axes?: Partial; - scales?: Partial; - colors?: Partial; - legend?: Partial; - crosshair?: Partial; -} - export const DEFAULT_GRID_LINE_CONFIG: GridLineConfig = { stroke: 'red', strokeWidth: 1, @@ -229,74 +224,5 @@ export function mergeWithDefaultAnnotationRect(config?: Partial { test('can clamp a value to min max', () => { @@ -28,4 +28,111 @@ describe('commons utilities', () => { expect(compareByValueAsc(20, 10)).toBeGreaterThan(0); expect(compareByValueAsc(10, 10)).toBe(0); }); + + describe('mergePartial', () => { + let baseClone: TestType; + interface TestType { + string: string; + number: number; + boolean: boolean; + array1: Partial[]; + array2: number[]; + nested: Partial; + } + type PartialTestType = RecursivePartial; + const base: TestType = { + string: 'string1', + boolean: false, + number: 1, + array1: [ + { + string: 'string2', + }, + ], + array2: [1, 2, 3], + nested: { + string: 'string2', + number: 2, + }, + }; + + beforeAll(() => { + baseClone = JSON.parse(JSON.stringify(base)) as TestType; + }); + + test('should allow partial to be undefined', () => { + expect(mergePartial('test')).toBe('test'); + }); + + test('should override base value with partial', () => { + expect(mergePartial(1 as number, 2)).toBe(2); + }); + + test('should NOT return original base structure', () => { + expect(mergePartial(base)).not.toBe(base); + }); + + test('should override string value in base', () => { + const partial: PartialTestType = { string: 'test' }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + string: partial.string, + }); + }); + + test('should override boolean value in base', () => { + const partial: PartialTestType = { boolean: true }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + boolean: partial.boolean, + }); + }); + + test('should override number value in base', () => { + const partial: PartialTestType = { number: 3 }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + number: partial.number, + }); + }); + + test('should override complex array value in base', () => { + const partial: PartialTestType = { array1: [{ string: 'test' }] }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + array1: partial!.array1, + }); + }); + + test('should override simple array value in base', () => { + const partial: PartialTestType = { array2: [4, 5, 6] }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + array2: partial!.array2, + }); + }); + + test('should override nested values in base', () => { + const partial: PartialTestType = { nested: { number: 5 } }; + const newBase = mergePartial(base, partial); + expect(newBase).toEqual({ + ...newBase, + nested: { + ...newBase.nested, + number: partial!.nested!.number, + }, + }); + }); + + test('should not mutate base structure', () => { + const partial: PartialTestType = { number: 3 }; + mergePartial(base, partial); + expect(base).toEqual(baseClone); + }); + }); }); diff --git a/packages/osd-charts/src/lib/utils/commons.ts b/packages/osd-charts/src/lib/utils/commons.ts index 177f2a059a37..b3c68dfbb561 100644 --- a/packages/osd-charts/src/lib/utils/commons.ts +++ b/packages/osd-charts/src/lib/utils/commons.ts @@ -12,3 +12,58 @@ export function clamp(value: number, min: number, max: number): number { // Can remove once we upgrade to TypesScript >= 3.5 export type Omit = Pick>; + + +/** + * Replaces all properties on any type as optional, includes nested types + * + * example: + * ```ts + * interface Person { + * name: string; + * age?: number; + * spouse: Person; + * children: Person[]; + * } + * type PartialPerson = RecursivePartial; + * // results in + * interface PartialPerson { + * name?: string; + * age?: number; + * spouse?: RecursivePartial; + * children?: RecursivePartial[] + * } + * ``` + */ +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends readonly (infer U)[] // eslint-disable-line prettier/prettier + ? readonly RecursivePartial[] + : RecursivePartial +}; + +/** + * Merges values of a partial structure with a base structure. + * + * @param base structure to be duplicated, must have all props of `partial` + * @param partial structure to override values from base + * + * @returns new base structure with updated partial values + */ +export function mergePartial(base: T, partial?: RecursivePartial): T { + if (Array.isArray(base)) { + return partial ? (partial as T) : base; // No nested array merging + } else if (typeof base === 'object') { + return Object.keys(base).reduce( + (newBase, key) => { + // @ts-ignore + newBase[key] = mergePartial(base[key], partial && partial[key]); + return newBase; + }, + { ...base }, + ); + } + + return partial !== undefined ? (partial as T) : base; +} diff --git a/packages/osd-charts/src/specs/settings.test.tsx b/packages/osd-charts/src/specs/settings.test.tsx index 2ed4d5210d27..fa8733358843 100644 --- a/packages/osd-charts/src/specs/settings.test.tsx +++ b/packages/osd-charts/src/specs/settings.test.tsx @@ -5,7 +5,8 @@ import { DARK_THEME } from '../lib/themes/dark_theme'; import { LIGHT_THEME } from '../lib/themes/light_theme'; import { TooltipType } from '../lib/utils/interactions'; import { ChartStore } from '../state/chart_state'; -import { DEFAULT_TOOLTIP_SNAP, DEFAULT_TOOLTIP_TYPE, SettingsComponent } from './settings'; +import { DEFAULT_TOOLTIP_SNAP, DEFAULT_TOOLTIP_TYPE, SettingsComponent, SettingSpecProps } from './settings'; +import { PartialTheme, BaseThemeTypes } from '../lib/themes/theme'; describe('Settings spec component', () => { test('should update store on mount if spec has a chart store', () => { @@ -67,7 +68,7 @@ describe('Settings spec component', () => { expect(chartStore.debug).toBe(false); expect(chartStore.xDomain).toBeUndefined(); - const updatedProps = { + const updatedProps: SettingSpecProps = { theme: DARK_THEME, rotation: 90 as Rotation, rendering: 'svg' as Rendering, @@ -148,4 +149,42 @@ describe('Settings spec component', () => { expect(chartStore.onLegendItemPlusClickListener).toEqual(onLegendEvent); expect(chartStore.onLegendItemMinusClickListener).toEqual(onLegendEvent); }); + + test('should allow partial theme', () => { + const chartStore = new ChartStore(); + const partialTheme: PartialTheme = { + colors: { + defaultVizColor: 'aquamarine', + }, + }; + + expect(chartStore.chartTheme).toEqual(LIGHT_THEME); + + const updatedProps: SettingSpecProps = { + theme: partialTheme, + baseThemeType: BaseThemeTypes.Dark, + rotation: 90 as Rotation, + rendering: 'svg' as Rendering, + animateData: true, + showLegend: true, + tooltip: { + type: TooltipType.None, + snap: false, + }, + legendPosition: Position.Bottom, + showLegendDisplayValue: false, + debug: true, + xDomain: { min: 0, max: 10 }, + }; + + mount(); + + expect(chartStore.chartTheme).toEqual({ + ...DARK_THEME, + colors: { + ...DARK_THEME.colors, + ...partialTheme.colors, + }, + }); + }); }); diff --git a/packages/osd-charts/src/specs/settings.tsx b/packages/osd-charts/src/specs/settings.tsx index 1f677ccc6214..e20bb431e87e 100644 --- a/packages/osd-charts/src/specs/settings.tsx +++ b/packages/osd-charts/src/specs/settings.tsx @@ -1,8 +1,10 @@ import { inject } from 'mobx-react'; import { PureComponent } from 'react'; + import { DomainRange, Position, Rendering, Rotation } from '../lib/series/specs'; +import { DARK_THEME } from '../lib/themes/dark_theme'; import { LIGHT_THEME } from '../lib/themes/light_theme'; -import { Theme } from '../lib/themes/theme'; +import { BaseThemeType, mergeWithDefaultTheme, PartialTheme, Theme, BaseThemeTypes } from '../lib/themes/theme'; import { Domain } from '../lib/utils/domain'; import { TooltipType, TooltipValueFormatter } from '../lib/utils/interactions'; import { @@ -30,9 +32,10 @@ function isTooltipType(config: TooltipType | TooltipProps): config is TooltipTyp return typeof config === 'string'; } -interface SettingSpecProps { +export interface SettingSpecProps { chartStore?: ChartStore; - theme?: Theme; + theme?: Theme | PartialTheme; + baseThemeType?: BaseThemeType; rendering: Rendering; rotation: Rotation; animateData: boolean; @@ -54,10 +57,20 @@ interface SettingSpecProps { xDomain?: Domain | DomainRange; } +function getTheme(theme?: Theme | PartialTheme, baseThemeType: BaseThemeType = BaseThemeTypes.Light): Theme { + if (theme) { + const baseTheme = baseThemeType === BaseThemeTypes.Light ? LIGHT_THEME : DARK_THEME; + return mergeWithDefaultTheme(theme, baseTheme); + } + + return LIGHT_THEME; +} + function updateChartStore(props: SettingSpecProps) { const { chartStore, theme, + baseThemeType, rotation, rendering, animateData, @@ -80,7 +93,8 @@ function updateChartStore(props: SettingSpecProps) { if (!chartStore) { return; } - chartStore.chartTheme = theme || LIGHT_THEME; + + chartStore.chartTheme = getTheme(theme, baseThemeType); chartStore.chartRotation = rotation; chartStore.chartRendering = rendering; chartStore.animateData = animateData; @@ -136,6 +150,7 @@ export class SettingsComponent extends PureComponent { animateData: true, showLegend: false, debug: false, + baseThemeType: BaseThemeTypes.Light, tooltip: { type: DEFAULT_TOOLTIP_TYPE, snap: DEFAULT_TOOLTIP_SNAP, diff --git a/packages/osd-charts/stories/styling.tsx b/packages/osd-charts/stories/styling.tsx index 9ab5875eb7cf..b61a7849ffc7 100644 --- a/packages/osd-charts/stories/styling.tsx +++ b/packages/osd-charts/stories/styling.tsx @@ -22,6 +22,7 @@ import { Position, ScaleType, Settings, + BaseThemeTypes, } from '../src/'; import * as TestDatasets from '../src/lib/series/utils/test_dataset'; import { palettes } from '../src/lib/themes/colors'; @@ -365,6 +366,51 @@ storiesOf('Stylings', module) ); }) + .add('partial custom theme', () => { + const customPartialTheme: PartialTheme = { + barSeriesStyle: { + border: { + stroke: color('BarBorderStroke', 'white'), + visible: true, + }, + }, + }; + + return ( + + + + Number(d).toFixed(2)} + /> + + Number(d).toFixed(2)} + /> + + + ); + }) .add('custom series colors through spec props', () => { const barCustomSeriesColors: CustomSeriesColorsMap = new Map(); const barDataSeriesColorValues: DataSeriesColorsValues = { diff --git a/packages/osd-charts/tsconfig.all.json b/packages/osd-charts/tsconfig.all.json deleted file mode 100644 index 5624ee5a2aea..000000000000 --- a/packages/osd-charts/tsconfig.all.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig", - "include": ["stories/**/*"] -} diff --git a/packages/osd-charts/tsconfig.jest.json b/packages/osd-charts/tsconfig.jest.json new file mode 100644 index 000000000000..5cd2e875f60b --- /dev/null +++ b/packages/osd-charts/tsconfig.jest.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig", + "include": [ + "src/**/*" + ] +} diff --git a/packages/osd-charts/tsconfig.json b/packages/osd-charts/tsconfig.json index 048204d212fb..e0b83db172bf 100644 --- a/packages/osd-charts/tsconfig.json +++ b/packages/osd-charts/tsconfig.json @@ -3,27 +3,19 @@ "declaration": true, "outDir": "./dist/", "noImplicitAny": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "sourceMap": true, "preserveConstEnums": true, "strict": true, "esModuleInterop": true, "module": "CommonJS", "target": "es5", - "lib": [ - "esnext", - "dom" - ], + "lib": ["esnext", "dom"], "moduleResolution": "node", "jsx": "react", "allowJs": false, "skipLibCheck": true, "downlevelIteration": true }, - "include": [ - "src/**/*" - ], - "exclude": [ - "**/*.test.*" - ] -} \ No newline at end of file + "include": ["src/**/*", "stories/**/*"] +} diff --git a/packages/osd-charts/tsconfig.lib.json b/packages/osd-charts/tsconfig.lib.json new file mode 100644 index 000000000000..d4f45dd2c926 --- /dev/null +++ b/packages/osd-charts/tsconfig.lib.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "noUnusedLocals": true + }, + "extends": "./tsconfig", + "include": ["src/**/*"], + "exclude": ["**/*.test.*"] +}