diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 96d9dcfc8c..dec952bc0b 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { Example } from '../stories/treemap/6_custom_style'; +import { Example } from '../stories/stylings/20_partition_background'; export class Playground extends React.Component { render() { diff --git a/api/charts.api.md b/api/charts.api.md index 89f164e3f7..6e66ef5c72 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -176,6 +176,11 @@ export interface AxisStyle { tickLabelPadding?: number; } +// @public +export interface BackgroundStyle { + color: string; +} + // Warning: (ae-forgotten-export) The symbol "SpecRequiredProps" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SpecOptionalProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "BarSeries" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1403,6 +1408,7 @@ export interface Theme { areaSeriesStyle: AreaSeriesStyle; // (undocumented) axes: AxisConfig; + background: BackgroundStyle; barSeriesStyle: BarSeriesStyle; bubbleSeriesStyle: BubbleSeriesStyle; chartMargins: Margins; @@ -1534,8 +1540,8 @@ export interface XYChartSeriesIdentifier extends SeriesIdentifier { // Warnings were encountered during analysis: // -// src/chart_types/partition_chart/layout/types/config_types.ts:117:5 - (ae-forgotten-export) The symbol "TimeMs" needs to be exported by the entry point index.d.ts -// src/chart_types/partition_chart/layout/types/config_types.ts:118:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts +// src/chart_types/partition_chart/layout/types/config_types.ts:120:5 - (ae-forgotten-export) The symbol "TimeMs" needs to be exported by the entry point index.d.ts +// src/chart_types/partition_chart/layout/types/config_types.ts:121:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts // src/chart_types/partition_chart/specs/index.ts:47:13 - (ae-forgotten-export) The symbol "NodeColorAccessor" needs to be exported by the entry point index.d.ts // src/commons/series_id.ts:37:3 - (ae-forgotten-export) The symbol "SeriesKey" needs to be exported by the entry point index.d.ts diff --git a/docs/0-Intro/1-Overview.mdx b/docs/0-Intro/1-Overview.mdx index 62e4be3f14..c0b1f9a7ea 100644 --- a/docs/0-Intro/1-Overview.mdx +++ b/docs/0-Intro/1-Overview.mdx @@ -1,3 +1,17 @@ +import { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src'; +import { mocks } from '../../src/mocks/hierarchical/index'; +import { config } from '../../src/chart_types/partition_chart/layout/config/config'; +import { ShapeTreeNode } from '../../src/chart_types/partition_chart/layout/types/viewmodel_types'; +import { + categoricalFillColor, + colorBrewerCategoricalStark9, + countryLookup, + indexInterpolatedFillColor, + interpolatorCET2s, + interpolatorTurbo, + productLookup, + regionLookup, +} from '../../stories/utils/utils'; import { Meta, Story } from "@storybook/addon-docs/blocks"; @@ -255,3 +269,155 @@ type PointStyleAccessor = ( ``` > Note: When overriding bar or point styles be mindful of performance and these accessor functions will be call on every bar/point is every series. Precomputing any expensive task before rendering. + +### Background Colors and Text Contrast +You can provide the `backgroundColor` of the container that the chart will be placed onto. You can set the `textContrast` to a boolean value or a number. The default `textContrast` is set to 4.5 but you can always disable this or set your own numerical amount. + +> Note: This functionality is currently available for Partition charts. Please see the partition background and partition label stories. + +```js +config: { + fillLabel: { + textInvertible: true, + textContrast: true, // can also be set to a number + } +} +``` +`textInvertible` will have to be set to true for `textContrast` to be set as well. To see an example of where this applies, please see the Partitions Background story within Stylings. Charts are included below but are static. +If you have `textInvertible` set to true, but do not have `textContrast` set to true, then the red slices, Europe, North America, and Asia, will have white text: + + + + d.exportVal} + valueFormatter={(d) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d) => d.sitc1, + nodeLabel: (d) => productLookup[d].name, + shape: { + fillColor: (d) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.7)(d.sortIndex); + }, + }, + }, + { + groupByRollup: (d) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d) => regionLookup[d].regionName, + shape: { + fillColor: (d) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.5)(d.parent.sortIndex); + }, + }, + }, + { + groupByRollup: (d) => d.dest, + nodeLabel: (d) => countryLookup[d].name, + shape: { + fillColor: (d) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.3)(d.parent.parent.sortIndex); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 0, + fontSize: 14, + }, + fontFamily: 'Arial', + fillLabel: { + valueFormatter: (d) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, + fontStyle: 'italic', + textInvertible: true, + textContrast: false, + fontWeight: 900, + valueFont: { + fontFamily: 'Menlo', + fontStyle: 'normal', + fontWeight: 100, + }, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 1, + emptySizeRatio: 0, + circlePadding: 4, + backgroundColor: 'rgba(229,229,229,1)', + }} + /> + + + + +Now if you set the `textContrast` to true as well, these slices also become black in text color: + + + + d.exportVal} + valueFormatter={(d) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d) => d.sitc1, + nodeLabel: (d) => productLookup[d].name, + shape: { + fillColor: (d) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.7)(d.sortIndex); + }, + }, + }, + { + groupByRollup: (d) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d) => regionLookup[d].regionName, + shape: { + fillColor: (d) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.5)(d.parent.sortIndex); + }, + }, + }, + { + groupByRollup: (d) => d.dest, + nodeLabel: (d) => countryLookup[d].name, + shape: { + fillColor: (d) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.3)(d.parent.parent.sortIndex); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 0, + fontSize: 14, + }, + fontFamily: 'Arial', + fillLabel: { + valueFormatter: (d) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, + fontStyle: 'italic', + textInvertible: true, + textContrast: true, + fontWeight: 900, + valueFont: { + fontFamily: 'Menlo', + fontStyle: 'normal', + fontWeight: 100, + }, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 1, + emptySizeRatio: 0, + circlePadding: 4, + backgroundColor: 'rgba(229,229,229,1)', + }} + /> + \ No newline at end of file diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png index 26b015b926..37cb8f0097 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partition-background-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partition-background-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..f79274d314 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partition-background-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partition-labels-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partition-labels-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..22271e0cfc Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-stylings-partition-labels-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-bold-link-value-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-bold-link-value-visually-looks-correct-1-snap.png index b4ca9a5fed..ca8bfae312 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-bold-link-value-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-bold-link-value-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-clockwise-no-special-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-clockwise-no-special-visually-looks-correct-1-snap.png index 1954092fb7..28235f8eee 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-clockwise-no-special-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-clockwise-no-special-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-counter-clockwise-special-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-counter-clockwise-special-visually-looks-correct-1-snap.png index d7d11391f2..dc5ca3973e 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-counter-clockwise-special-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-counter-clockwise-special-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-custom-stroke-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-custom-stroke-visually-looks-correct-1-snap.png index bfed76f1b1..0858553d29 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-custom-stroke-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-custom-stroke-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-donut-chart-with-fill-labels-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-donut-chart-with-fill-labels-visually-looks-correct-1-snap.png index 9001d659fa..ec13f4928c 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-donut-chart-with-fill-labels-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-donut-chart-with-fill-labels-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-heterogeneous-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-heterogeneous-visually-looks-correct-1-snap.png index 703bfc0778..0b5157954a 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-heterogeneous-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-heterogeneous-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-most-basic-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-most-basic-visually-looks-correct-1-snap.png index f0bed0160c..a5b2b157fe 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-most-basic-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-most-basic-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-negative-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-negative-visually-looks-correct-1-snap.png index e0be1f84b6..3074e5e877 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-negative-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-negative-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-not-a-number-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-not-a-number-visually-looks-correct-1-snap.png index e0be1f84b6..3074e5e877 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-not-a-number-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-not-a-number-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png index 08b6a6030a..d6af436ed2 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png index 36397904f9..3a8b0c5c46 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-single-sunburst-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png index ef35a7ab42..aaf5470529 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-three-layers-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-two-layers-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-two-layers-visually-looks-correct-1-snap.png index d834709356..e901db56e5 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-two-layers-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-sunburst-with-two-layers-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-visually-looks-correct-1-snap.png index f7239ecd3b..a9fdb126de 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-with-categorical-color-palette-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-with-categorical-color-palette-visually-looks-correct-1-snap.png index 6a7f226a0f..ca0cddde10 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-with-categorical-color-palette-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-value-formatted-with-categorical-color-palette-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-with-fill-labels-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-with-fill-labels-visually-looks-correct-1-snap.png index f759e09ba9..2e8fa5f711 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-with-fill-labels-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-with-fill-labels-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-groove-text-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-groove-text-visually-looks-correct-1-snap.png index b6c1fec57b..8a4650b97a 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-groove-text-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-groove-text-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-2-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-2-visually-looks-correct-1-snap.png index 82e514c4c6..071b6e6303 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-2-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-2-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-visually-looks-correct-1-snap.png index ffcf58b88d..83033e9706 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-one-layer-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png index a994082229..5f9424fc26 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-two-layers-stress-test-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-zero-values-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-zero-values-visually-looks-correct-1-snap.png index a902dd45f9..bf531cc4ef 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-zero-values-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-zero-values-visually-looks-correct-1-snap.png differ diff --git a/package.json b/package.json index f2be5cadf1..375e21ce07 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,9 @@ "@storybook/react": "5.2.8", "@storybook/source-loader": "^5.3.9", "@storybook/theming": "^5.2.8", + "@types/chroma-js": "^2.0.0", "@types/classnames": "^2.2.7", + "@types/color": "^3.0.1", "@types/core-js": "^2.5.2", "@types/d3-array": "^1.2.6", "@types/d3-collection": "^1.0.8", @@ -170,6 +172,7 @@ "webpack-dev-server": "^3.3.1" }, "dependencies": { + "chroma-js": "^2.1.0", "@popperjs/core": "^2.4.0", "classnames": "^2.2.6", "d3-array": "^1.2.4", diff --git a/src/chart_types/partition_chart/layout/config/config.ts b/src/chart_types/partition_chart/layout/config/config.ts index 4d6553f599..5077b99ec6 100644 --- a/src/chart_types/partition_chart/layout/config/config.ts +++ b/src/chart_types/partition_chart/layout/config/config.ts @@ -173,8 +173,9 @@ export const configMetadata = { fillLabel: { type: 'group', values: { - textColor: { dflt: '#000000', type: 'color' }, + textColor: { type: 'color', dflt: '#000000' }, textInvertible: { dflt: false, type: 'boolean' }, + textContrast: { dflt: false, type: 'boolean' || 'number' }, ...fontSettings, valueGetter: { dflt: sumValueGetter, @@ -221,6 +222,7 @@ export const configMetadata = { }, textColor: { dflt: '#000000', type: 'color' }, textInvertible: { dflt: false, type: 'boolean' }, + textContrast: { dflt: false, type: 'boolean' || 'number' }, textOpacity: { dflt: 1, min: 0, max: 1, type: 'number' }, minimumStemLength: { dflt: 0, diff --git a/src/chart_types/partition_chart/layout/types/config_types.ts b/src/chart_types/partition_chart/layout/types/config_types.ts index 6179d8a329..f917061542 100644 --- a/src/chart_types/partition_chart/layout/types/config_types.ts +++ b/src/chart_types/partition_chart/layout/types/config_types.ts @@ -33,9 +33,12 @@ export type PerSidePadding = PerSideDistance; export type Padding = Pixels | Partial; +export type TextContrast = boolean | number; + interface LabelConfig extends Font { textColor: Color; textInvertible: boolean; + textContrast: TextContrast; textOpacity: Ratio; valueFormatter: ValueFormatter; valueFont: PartialFont; diff --git a/src/chart_types/partition_chart/layout/types/types.ts b/src/chart_types/partition_chart/layout/types/types.ts index b5c791be8e..23077836ae 100644 --- a/src/chart_types/partition_chart/layout/types/types.ts +++ b/src/chart_types/partition_chart/layout/types/types.ts @@ -43,6 +43,8 @@ export interface Font { fontVariant: FontVariant; fontWeight: FontWeight; fontFamily: FontFamily; + textColor: string; + textOpacity: number; } export type PartialFont = Partial; diff --git a/src/chart_types/partition_chart/layout/types/viewmodel_types.ts b/src/chart_types/partition_chart/layout/types/viewmodel_types.ts index 1d47028b26..df11d5e8b7 100644 --- a/src/chart_types/partition_chart/layout/types/viewmodel_types.ts +++ b/src/chart_types/partition_chart/layout/types/viewmodel_types.ts @@ -22,11 +22,12 @@ import { Font } from './types'; import { config, ValueGetterName } from '../config/config'; import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup'; import { Color } from '../../../../utils/commons'; +import { LinkLabelsViewModelSpec } from '../viewmodel/link_text_layout'; import { VerticalAlignments } from '../viewmodel/viewmodel'; /** @internal */ export type LinkLabelVM = { - link: PointTuples; + linkLabels: PointTuples; translate: PointTuple; textAlign: CanvasTextAlign; text: string; @@ -34,8 +35,6 @@ export type LinkLabelVM = { width: Distance; valueWidth: Distance; verticalOffset: Distance; - labelFontSpec: Font; - valueFontSpec: Font; }; /** @internal */ @@ -95,19 +94,33 @@ export type ShapeViewModel = { config: Config; quadViewModel: QuadViewModel[]; rowSets: RowSet[]; - linkLabelViewModels: LinkLabelVM[]; + linkLabelViewModels: LinkLabelsViewModelSpec; outsideLinksViewModel: OutsideLinksViewModel[]; diskCenter: PointObject; pickQuads: PickFunction; outerRadius: number; }; +const defaultFont: Font = { + fontStyle: 'normal', + fontVariant: 'normal', + fontFamily: '', + fontWeight: 'normal', + textColor: 'black', + textOpacity: 1, +}; + /** @internal */ export const nullShapeViewModel = (specifiedConfig?: Config, diskCenter?: PointObject): ShapeViewModel => ({ config: specifiedConfig || config, quadViewModel: [], rowSets: [], - linkLabelViewModels: [], + linkLabelViewModels: { + linkLabels: [], + labelFontSpec: defaultFont, + valueFontSpec: defaultFont, + strokeColor: '', + }, outsideLinksViewModel: [], diskCenter: diskCenter || { x: 0, y: 0 }, pickQuads: () => [], diff --git a/src/chart_types/partition_chart/layout/utils/__mocks__/calcs.ts b/src/chart_types/partition_chart/layout/utils/__mocks__/calcs.ts new file mode 100644 index 0000000000..1c63308a69 --- /dev/null +++ b/src/chart_types/partition_chart/layout/utils/__mocks__/calcs.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ +const module = jest.requireActual('../calcs.ts'); + +export const getBackgroundWithContainerColorFromUser = jest.fn(module.getBackgroundWithContainerColorFromUser); +export const makeHighContrastColor = jest.fn(module.makeHighContrastColor); diff --git a/src/chart_types/partition_chart/layout/utils/__mocks__/d3_utils.ts b/src/chart_types/partition_chart/layout/utils/__mocks__/color_library_wrappers.ts similarity index 94% rename from src/chart_types/partition_chart/layout/utils/__mocks__/d3_utils.ts rename to src/chart_types/partition_chart/layout/utils/__mocks__/color_library_wrappers.ts index 2953037dcd..f2377d65fd 100644 --- a/src/chart_types/partition_chart/layout/utils/__mocks__/d3_utils.ts +++ b/src/chart_types/partition_chart/layout/utils/__mocks__/color_library_wrappers.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -const module = jest.requireActual('../d3_utils.ts'); +const module = jest.requireActual('../color_library_wrappers.ts'); export const defaultColor = module.defaultColor; export const transparentColor = module.transparentColor; diff --git a/src/chart_types/partition_chart/layout/utils/__mocks__/fill_text_layout.ts b/src/chart_types/partition_chart/layout/utils/__mocks__/fill_text_layout.ts new file mode 100644 index 0000000000..b9c06ef048 --- /dev/null +++ b/src/chart_types/partition_chart/layout/utils/__mocks__/fill_text_layout.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ +const module = jest.requireActual('../../viewmodel/fill_text_layout.ts'); + +export const getTextColorIfTextInvertible = jest.fn(module.getTextColorIfTextInvertible); diff --git a/src/chart_types/partition_chart/layout/utils/__mocks__/link_text_layout.ts b/src/chart_types/partition_chart/layout/utils/__mocks__/link_text_layout.ts new file mode 100644 index 0000000000..7daab9b69c --- /dev/null +++ b/src/chart_types/partition_chart/layout/utils/__mocks__/link_text_layout.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ +const module = jest.requireActual('../../link_text_layout.ts'); + +export const linkTextLayout = jest.fn(module.linkTextLayout); diff --git a/src/chart_types/partition_chart/layout/utils/calcs.test.ts b/src/chart_types/partition_chart/layout/utils/calcs.test.ts index c6ac8da0fc..7a8efef983 100644 --- a/src/chart_types/partition_chart/layout/utils/calcs.test.ts +++ b/src/chart_types/partition_chart/layout/utils/calcs.test.ts @@ -16,7 +16,76 @@ * specific language governing permissions and limitations * under the License. */ -import { integerSnap, monotonicHillClimb } from './calcs'; +import { makeHighContrastColor, combineColors, integerSnap, monotonicHillClimb } from './calcs'; + +describe('calcs', () => { + describe('test makeHighContrastColor', () => { + it('hex input - should change white text to black when background is white', () => { + const expected = '#000'; + const result = makeHighContrastColor('#fff', '#fff'); + expect(result).toBe(expected); + }); + it('rgb input - should change white text to black when background is white ', () => { + const expected = '#000'; + const result = makeHighContrastColor('rgb(255, 255, 255)', 'rgb(255, 255, 255)'); + expect(result).toBe(expected); + }); + it('rgba input - should change white text to black when background is white ', () => { + const expected = '#000'; + const result = makeHighContrastColor('rgba(255, 255, 255, 1)', 'rgba(255, 255, 255, 1)'); + expect(result).toBe(expected); + }); + it('word input - should change white text to black when background is white ', () => { + const expected = '#000'; + const result = makeHighContrastColor('white', 'white'); + expect(result).toBe(expected); + }); + // test contrast computation + it('should provide at least 4.5 contrast', () => { + const foreground = '#fff'; // white + const background = 'rgba(255, 255, 51, 0.3)'; // light yellow + const result = '#000'; // black + expect(result).toBe(makeHighContrastColor(foreground, background)); + }); + it('should use black text for hex value', () => { + const foreground = '#fff'; // white + const background = '#7874B2'; // Thailand color + const result = '#000'; // black + expect(result).toBe(makeHighContrastColor(foreground, background)); + }); + it('should switch to black text if background color is in rgba() format - Thailand', () => { + const containerBackground = 'white'; + const background = 'rgba(120, 116, 178, 0.7)'; + const resultForCombined = 'rgba(161, 158, 201, 1)'; // 0.3 'rgba(215, 213, 232, 1)'; // 0.5 - 'rgba(188, 186, 217, 1)'; //0.7 - ; + expect(combineColors(background, containerBackground)).toBe(resultForCombined); + const foreground = 'white'; + const resultForContrastedText = '#000'; //switches to black text + expect(makeHighContrastColor(foreground, resultForCombined)).toBe(resultForContrastedText); + }); + }); + describe('test the combineColors function', () => { + it('should return correct RGBA with opacity greater than 0.7', () => { + const expected = 'rgba(102, 43, 206, 1)'; + const result = combineColors('rgba(121, 47, 249, 0.8)', '#1c1c24'); + expect(result).toBe(expected); + }); + it('should return correct RGBA with opacity less than 0.7', () => { + const expected = 'rgba(226, 186, 187, 1)'; + const result = combineColors('rgba(228, 26, 28, 0.3)', 'rgba(225, 255, 255, 1)'); + expect(result).toBe(expected); + }); + it('should return correct RGBA with the input color as a word vs rgba or hex value', () => { + const expected = 'rgba(0, 0, 255, 1)'; + const result = combineColors('blue', 'black'); + expect(result).toBe(expected); + }); + it('should return the correct RGBA with hex input', () => { + const expected = 'rgba(212, 242, 210, 1)'; + const result = combineColors('#D4F2D2', '#BEB7DF'); + expect(result).toBe(expected); + }); + }); +}); describe('monotonicHillClimb', () => { const arbitraryNumber = 27; diff --git a/src/chart_types/partition_chart/layout/utils/calcs.ts b/src/chart_types/partition_chart/layout/utils/calcs.ts index 91be333a7f..f686112a14 100644 --- a/src/chart_types/partition_chart/layout/utils/calcs.ts +++ b/src/chart_types/partition_chart/layout/utils/calcs.ts @@ -17,8 +17,10 @@ * under the License. */ import { Ratio } from '../types/geometry_types'; -import { RgbTuple, stringToRGB } from './d3_utils'; +import { RgbTuple, RGBATupleToString, stringToRGB } from './color_library_wrappers'; import { Color } from '../../../../utils/commons'; +import chroma from 'chroma-js'; +import { TextContrast } from '../types/config_types'; /** @internal */ export function hueInterpolator(colors: RgbTuple[]) { @@ -48,26 +50,134 @@ export function arrayToLookup(keyFun: Function, array: Array) { return Object.assign({}, ...array.map((d) => ({ [keyFun(d)]: d }))); } -/** @internal */ -export function colorIsDark(color: Color) { - // fixme this assumes a white or very light background - const rgba = stringToRGB(color); - const { r, g, b, opacity } = rgba; - const a = rgba.hasOwnProperty('opacity') ? opacity : 1; - return r * 0.299 + g * 0.587 + b * 0.114 < a * 150; +/** If the user specifies the background of the container in which the chart will be on, we can use that color + * and make sure to provide optimal contrast + * @internal + */ +export function combineColors(foregroundColor: Color, backgroundColor: Color): Color { + const [red1, green1, blue1, alpha1] = chroma(foregroundColor).rgba(); + const [red2, green2, blue2, alpha2] = chroma(backgroundColor).rgba(); + + // For reference on alpha calculations: + // https://en.wikipedia.org/wiki/Alpha_compositing + const combinedAlpha = alpha1 + alpha2 * (1 - alpha1); + const combinedRed = Math.round((red1 * alpha1 + red2 * alpha2 * (1 - alpha1)) / combinedAlpha); + const combinedGreen = Math.round((green1 * alpha1 + green2 * alpha2 * (1 - alpha1)) / combinedAlpha); + const combinedBlue = Math.round((blue1 * alpha1 + blue2 * alpha2 * (1 - alpha1)) / combinedAlpha); + const rgba: RgbTuple = [combinedRed, combinedGreen, combinedBlue, combinedAlpha]; + return RGBATupleToString(rgba); } -/** @internal */ -export function getTextColor(shapeFillColor: Color, textColor: Color, textInvertible: boolean) { +/** + * Returns a valid color + * @param color valid color + * @internal + */ +export function validateColor(color?: string, defaultColor = 'rgba(255, 255, 255, 0)'): string { + return color === undefined || color === 'transparent' ? defaultColor : color; +} + +/** + * Return true if the color is a valid CSS color, false otherwise + * @param color a color written in string + */ +export function isColorValid(color: string) { + try { + chroma(color); + return true; + } catch { + return false; + } +} + +/** + * Adjust the text color in cases black and white can't reach ideal 4.5 ratio + * @internal + */ +export function makeHighContrastColor(foreground: Color, background: Color, ratio = 4.5): Color { + // determine the lightness factor of the background color to determine whether to lighten or darken the foreground + const lightness = chroma(background).get('hsl.l'); + let highContrastTextColor = foreground; + const isBackgroundDark = colorIsDark(background); + // determine whether white or black text is ideal contrast vs a grey that just passes the ratio + if (isBackgroundDark && chroma.deltaE('black', foreground) === 0) { + highContrastTextColor = '#fff'; + } else if (lightness > 0.5 && chroma.deltaE('white', foreground) === 0) { + highContrastTextColor = '#000'; + } + const precision = 1e8; + let contrast = getContrast(highContrastTextColor, background); + // brighten and darken the text color if not meeting the ratio + while (contrast < ratio) { + if (isBackgroundDark) { + highContrastTextColor = chroma(highContrastTextColor) + .brighten() + .toString(); + } else { + highContrastTextColor = chroma(highContrastTextColor) + .darken() + .toString(); + } + const scaledOldContrast = Math.round(contrast * precision) / precision; + contrast = getContrast(highContrastTextColor, background); + const scaledContrast = Math.round(contrast * precision) / precision; + // catch if the ideal contrast may not be possible + if (scaledOldContrast === scaledContrast) { + break; + } + } + return highContrastTextColor.toString(); +} + +/** + * show contrast amount + * @internal + */ +export function getContrast(foregroundColor: string | chroma.Color, backgroundColor: string | chroma.Color): number { + return chroma.contrast(foregroundColor, backgroundColor); +} + +/** + * determines if the color is dark based on the luminance + * @internal + */ +export function colorIsDark(color: Color): boolean { + const luminance = chroma(color).luminance(); + return luminance < 0.2; +} + +/** + * inverse color for text + * @internal + */ +export function getTextColorIfTextInvertible( + specifiedTextColorIsDark: boolean, + backgroundIsDark: boolean, + textColor: Color, + textContrast: TextContrast, + backgroundColor: Color, +) { + const inverseForContrast = specifiedTextColorIsDark === backgroundIsDark; const { r: tr, g: tg, b: tb, opacity: to } = stringToRGB(textColor); - const backgroundIsDark = colorIsDark(shapeFillColor); - const specifiedTextColorIsDark = colorIsDark(textColor); - const inverseForContrast = textInvertible && specifiedTextColorIsDark === backgroundIsDark; - return inverseForContrast - ? to === undefined - ? `rgb(${255 - tr}, ${255 - tg}, ${255 - tb})` - : `rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})` - : textColor; + if (!textContrast) { + return inverseForContrast + ? to === undefined + ? `rgb(${255 - tr}, ${255 - tg}, ${255 - tb})` + : `rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})` + : textColor; + } else if (textContrast === true && typeof textContrast !== 'number') { + return inverseForContrast + ? to === undefined + ? makeHighContrastColor(`rgb(${255 - tr}, ${255 - tg}, ${255 - tb})`, backgroundColor) + : makeHighContrastColor(`rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})`, backgroundColor) + : makeHighContrastColor(textColor, backgroundColor); + } else if (typeof textContrast === 'number') { + return inverseForContrast + ? to === undefined + ? makeHighContrastColor(`rgb(${255 - tr}, ${255 - tg}, ${255 - tb})`, backgroundColor, textContrast) + : makeHighContrastColor(`rgba(${255 - tr}, ${255 - tg}, ${255 - tb}, ${to})`, backgroundColor, textContrast) + : makeHighContrastColor(textColor, backgroundColor, textContrast); + } } /** @internal */ diff --git a/src/chart_types/partition_chart/layout/utils/d3_utils.test.ts b/src/chart_types/partition_chart/layout/utils/color_library_wrappers.test.ts similarity index 99% rename from src/chart_types/partition_chart/layout/utils/d3_utils.test.ts rename to src/chart_types/partition_chart/layout/utils/color_library_wrappers.test.ts index 144914746d..88093c95f0 100644 --- a/src/chart_types/partition_chart/layout/utils/d3_utils.test.ts +++ b/src/chart_types/partition_chart/layout/utils/color_library_wrappers.test.ts @@ -24,7 +24,7 @@ import { RgbObject, argsToRGBString, RGBtoString, -} from './d3_utils'; +} from './color_library_wrappers'; describe('d3 Utils', () => { describe('stringToRGB', () => { diff --git a/src/chart_types/partition_chart/layout/utils/d3_utils.ts b/src/chart_types/partition_chart/layout/utils/color_library_wrappers.ts similarity index 86% rename from src/chart_types/partition_chart/layout/utils/d3_utils.ts rename to src/chart_types/partition_chart/layout/utils/color_library_wrappers.ts index 84002fc871..b2cb0c0762 100644 --- a/src/chart_types/partition_chart/layout/utils/d3_utils.ts +++ b/src/chart_types/partition_chart/layout/utils/color_library_wrappers.ts @@ -17,6 +17,8 @@ * under the License. */ import { rgb as d3Rgb, RGBColor as D3RGBColor } from 'd3-color'; +import { Color } from '../../../../utils/commons'; +import chroma from 'chroma-js'; type RGB = number; type A = number; @@ -106,3 +108,23 @@ export function RGBtoString(rgb: RgbObject): string { const { r, g, b, opacity } = rgb; return argsToRGBString(r, g, b, opacity); } + +/** @internal */ +export function RGBATupleToString(rgba: RgbTuple): string { + if (rgba.length === 4) { + return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`; + } + return `rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`; +} + +/** convert rgb to hex + * @internal */ +export function RGBAToHex(rgba: Color) { + return chroma(rgba).hex(); +} + +/** convert hex to rgb + * @internal */ +export function HexToRGB(hex: string) { + return chroma(hex).rgba(); +} diff --git a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts index 0366fb4674..85f35f9d4a 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { getRectangleRowGeometry } from './fill_text_layout'; +import { getRectangleRowGeometry, getFillTextColor } from './fill_text_layout'; describe('Test that getRectangleRowGeometry works with:', () => { const container = { x0: 0, y0: 0, x1: 200, y1: 100 }; @@ -272,3 +272,27 @@ describe('Test that getRectangleRowGeometry works with:', () => { }); }); }); +describe('Test getTextColor function', () => { + test('getTextColor works with textContrast greater than default ratio', () => { + const textColor = 'black'; + const textInvertible = true; + const textContrast = 6; + const fillColor = 'rgba(55, 126, 184, 0.7)'; + const containerBackgroundColor = 'white'; + const expectedAdjustedTextColor = 'black'; + expect(getFillTextColor(textColor, textInvertible, textContrast, fillColor, containerBackgroundColor)).toEqual( + expectedAdjustedTextColor, + ); + }); + test('getTextColor works with textContrast not defined', () => { + const textColor = 'black'; + const textInvertible = true; + const textContrast = false; + const fillColor = 'rgba(55, 126, 184, 0.7)'; + const containerBackgroundColor = 'white'; + const expectedAdjustedTextColor = 'black'; + expect(getFillTextColor(textColor, textInvertible, textContrast, fillColor, containerBackgroundColor)).toEqual( + expectedAdjustedTextColor, + ); + }); +}); diff --git a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts index 5870542c2e..5da3d83be6 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts @@ -21,13 +21,13 @@ import { Coordinate, Distance, Pixels, - PointTuple, Radian, Radius, Ratio, RingSectorConstruction, + PointTuple, } from '../types/geometry_types'; -import { Config, Padding } from '../types/config_types'; +import { Config, Padding, TextContrast } from '../types/config_types'; import { logarithm, TAU, trueBearingToStandardPositionAngle } from '../utils/math'; import { QuadViewModel, @@ -41,8 +41,16 @@ import { import { Box, Font, PartialFont, TextMeasure } from '../types/types'; import { conjunctiveConstraint } from '../circline_geometry'; import { Layer } from '../../specs/index'; -import { integerSnap, getTextColor, monotonicHillClimb } from '../utils/calcs'; -import { ValueFormatter } from '../../../../utils/commons'; +import { + combineColors, + makeHighContrastColor, + colorIsDark, + getTextColorIfTextInvertible, + integerSnap, + monotonicHillClimb, +} from '../utils/calcs'; +import { ValueFormatter, Color } from '../../../../utils/commons'; +import { RGBATupleToString } from '../utils/color_library_wrappers'; import { RectangleConstruction, VerticalAlignments } from './viewmodel'; const INFINITY_RADIUS = 1e4; // far enough for a sub-2px precision on a 4k screen, good enough for text bounds; 64 bit floats still work well with it @@ -294,6 +302,50 @@ function getWordSpacing(fontSize: number) { return fontSize / 4; } +/** + * Determine the color for the text hinging on the parameters of textInvertible and textContrast + * @internal + */ +export function getFillTextColor( + textColor: Color, + textInvertible: boolean, + textContrast: TextContrast, + sliceFillColor: string, + containerBackgroundColor?: Color, +) { + let adjustedTextColor = textColor; + const containerBackgroundColorFromUser = + containerBackgroundColor === undefined || containerBackgroundColor === 'transparent' + ? 'rgba(255, 255, 255, 0)' + : containerBackgroundColor; + + const containerBackground = combineColors(sliceFillColor, containerBackgroundColorFromUser); + const formattedContainerBackground = + typeof containerBackground !== 'string' ? RGBATupleToString(containerBackground) : containerBackground; + + const textShouldBeInvertedAndTextContrastIsFalse = textInvertible && !textContrast; + const textShouldBeInvertedAndTextContrastIsSetToTrue = textInvertible && typeof textContrast !== 'number'; + const textContrastIsSetToANumberValue = typeof textContrast === 'number'; + const textShouldNotBeInvertedButTextContrastIsDefined = textContrast && !textInvertible; + + // change the contrast for the inverted slices + if (textShouldBeInvertedAndTextContrastIsFalse || textShouldBeInvertedAndTextContrastIsSetToTrue) { + const backgroundIsDark = colorIsDark(combineColors(sliceFillColor, containerBackgroundColorFromUser)); + const specifiedTextColorIsDark = colorIsDark(textColor); + // @ts-ignore + adjustedTextColor = getTextColorIfTextInvertible( + backgroundIsDark, + specifiedTextColorIsDark, + textColor, + textContrast, + formattedContainerBackground, + ); + // if textContrast is a number then take that into account or if textInvertible is set to false + } else if (textContrastIsSetToANumberValue || textShouldNotBeInvertedButTextContrastIsDefined) { + return makeHighContrastColor(adjustedTextColor, formattedContainerBackground); + } + return adjustedTextColor; +} type GetShapeRowGeometry = ( container: C, cx: Distance, @@ -315,6 +367,7 @@ function fill( shapeConstructor: ShapeConstructor, getShapeRowGeometry: GetShapeRowGeometry, getRotation: GetRotation, + containerBackgroundColor?: Color, ) { return function( config: Config, @@ -347,6 +400,8 @@ function fill( fontWeight, valueFormatter, padding, + textContrast, + textOpacity, } = Object.assign( { fontFamily: configFontFamily, fontWeight: 'normal', padding: 2 }, fillLabel, @@ -354,8 +409,13 @@ function fill( layer.fillLabel, layer.shape, ); - - const fillTextColor = getTextColor(node.fillColor, textColor, textInvertible); + const fillTextColor = getFillTextColor( + textColor, + textInvertible, + textContrast, + node.fillColor, + containerBackgroundColor, + ); const valueFont = Object.assign( { fontFamily: configFontFamily, fontWeight: 'normal' }, @@ -370,6 +430,8 @@ function fill( fontVariant, fontWeight, fontFamily, + textColor, + textOpacity, }; const allBoxes = getAllBoxes(rawTextGetter, valueGetter, valueFormatter, sizeInvariantFont, valueFont, node); const [cx, cy] = textFillOrigin; @@ -570,8 +632,9 @@ export function fillTextLayout( shapeConstructor: ShapeConstructor, getShapeRowGeometry: GetShapeRowGeometry, getRotation: GetRotation, + containerBackgroundColor?: Color, ) { - const specificFiller = fill(shapeConstructor, getShapeRowGeometry, getRotation); + const specificFiller = fill(shapeConstructor, getShapeRowGeometry, getRotation, containerBackgroundColor); return function( measure: TextMeasure, rawTextGetter: RawTextGetter, diff --git a/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts b/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts index 7e0b459484..d4ddb96727 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts @@ -21,15 +21,24 @@ import { Config } from '../types/config_types'; import { TAU, trueBearingToStandardPositionAngle } from '../utils/math'; import { LinkLabelVM, RawTextGetter, ShapeTreeNode, ValueGetterFunction } from '../types/viewmodel_types'; import { meanAngle } from '../geometry'; -import { Box, Font, TextAlign, TextMeasure } from '../types/types'; -import { ValueFormatter } from '../../../../utils/commons'; +import { ValueFormatter, Color } from '../../../../utils/commons'; +import { makeHighContrastColor, validateColor } from '../utils/calcs'; import { Point } from '../../../../utils/point'; +import { Box, Font, TextAlign, TextMeasure } from '../types/types'; import { integerSnap, monotonicHillClimb } from '../utils/calcs'; function cutToLength(s: string, maxLength: number) { return s.length <= maxLength ? s : `${s.substr(0, maxLength - 1)}…`; // ellipsis is one char } +/**@internal */ +export interface LinkLabelsViewModelSpec { + linkLabels: LinkLabelVM[]; + labelFontSpec: Font; + valueFontSpec: Font; + strokeColor: Color; +} + /** @internal */ export function linkTextLayout( rectWidth: Distance, @@ -44,12 +53,39 @@ export function linkTextLayout( valueFormatter: ValueFormatter, maxTextLength: number, diskCenter: Point, -): LinkLabelVM[] { - const { linkLabel } = config; + containerBackgroundColor?: Color, +): LinkLabelsViewModelSpec { + const { linkLabel, sectorLineStroke } = config; const maxDepth = nodesWithoutRoom.reduce((p: number, n: ShapeTreeNode) => Math.max(p, n.depth), 0); const yRelativeIncrement = Math.sin(linkLabel.stemAngle) * linkLabel.minimumStemLength; const rowPitch = linkLabel.fontSize + linkLabel.spacing; - return nodesWithoutRoom + // determine the ideal contrast color for the link labels + const validBackgroundColor = validateColor(containerBackgroundColor); + const contrastTextColor = containerBackgroundColor + ? makeHighContrastColor(linkLabel.textColor, validBackgroundColor) + : linkLabel.textColor; + const strokeColor = containerBackgroundColor + ? makeHighContrastColor(sectorLineStroke, validBackgroundColor) + : sectorLineStroke; + const labelFontSpec = { + fontStyle: 'normal', + fontVariant: 'normal', + fontFamily: config.fontFamily, + fontWeight: 'normal', + ...linkLabel, + textColor: contrastTextColor, + }; + const valueFontSpec = { + fontStyle: 'normal', + fontVariant: 'normal', + fontFamily: config.fontFamily, + fontWeight: 'normal', + ...linkLabel, + ...linkLabel.valueFont, + textColor: contrastTextColor, + }; + + const linkLabels: LinkLabelVM[] = nodesWithoutRoom .filter((n: ShapeTreeNode) => n.depth === maxDepth) // only the outermost ring can have links .sort((n1: ShapeTreeNode, n2: ShapeTreeNode) => Math.abs(n2.x0 - n2.x1) - Math.abs(n1.x0 - n1.x1)) .slice(0, linkLabel.maxCount) // largest linkLabel.MaxCount slices @@ -82,6 +118,7 @@ export function linkTextLayout( const rawText = rawTextGetter(node); const labelText = cutToLength(rawText, maxTextLength); const valueText = valueFormatter(valueGetter(node)); + const labelFontSpec: Font = { fontStyle: 'normal', fontVariant: 'normal', @@ -113,7 +150,7 @@ export function linkTextLayout( text: labelText, }) : { text: '', width: 0, verticalOffset: 0 }; - const link: PointTuples = [ + const linkLabels: PointTuples = [ [x0, y0], [stemFromX, stemFromY], [stemToX, stemToY], @@ -122,7 +159,7 @@ export function linkTextLayout( const translate: PointTuple = [translateX, stemToY]; const textAlign: TextAlign = rightSide ? 'left' : 'right'; return { - link, + linkLabels, translate, textAlign, text, @@ -134,18 +171,19 @@ export function linkTextLayout( valueFontSpec, }; }) - .filter((l: LinkLabelVM) => l.text !== ''); // cull linked labels whose text was truncated to nothing -} + .filter(({ text }) => text !== ''); // cull linked labels whose text was truncated to nothing; + return { linkLabels, valueFontSpec, labelFontSpec, strokeColor }; -function fitText(measure: TextMeasure, desiredText: string, allottedWidth: number, fontSize: number, box: Box) { - const desiredLength = desiredText.length; - const response = (v: number) => measure(fontSize, [{ ...box, text: box.text.substr(0, v) }])[0].width; - const visibleLength = monotonicHillClimb(response, desiredLength, allottedWidth, integerSnap); - const text = visibleLength < 2 && desiredLength >= 2 ? '' : cutToLength(box.text, visibleLength); - const { width, emHeightAscent, emHeightDescent } = measure(fontSize, [{ ...box, text }])[0]; - return { - width, - verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle` - text, - }; + function fitText(measure: TextMeasure, desiredText: string, allottedWidth: number, fontSize: number, box: Box) { + const desiredLength = desiredText.length; + const response = (v: number) => measure(fontSize, [{ ...box, text: box.text.substr(0, v) }])[0].width; + const visibleLength = monotonicHillClimb(response, desiredLength, allottedWidth, integerSnap); + const text = visibleLength < 2 && desiredLength >= 2 ? '' : cutToLength(box.text, visibleLength); + const { width, emHeightAscent, emHeightDescent } = measure(fontSize, [{ ...box, text }])[0]; + return { + width, + verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle` + text, + }; + } } diff --git a/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts b/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts index 66f2790ea9..c869f544c8 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts @@ -24,7 +24,7 @@ import { Distance, Pixels, PointTuple, Radius } from '../types/geometry_types'; import { meanAngle } from '../geometry'; import { getTopPadding, treemap } from '../utils/treemap'; import { sunburst } from '../utils/sunburst'; -import { argsToRGBString, stringToRGB } from '../utils/d3_utils'; +import { argsToRGBString, stringToRGB } from '../utils/color_library_wrappers'; import { nullShapeViewModel, OutsideLinksViewModel, @@ -56,7 +56,7 @@ import { sortIndexAccessor, HierarchyOfArrays, } from '../utils/group_by_rollup'; -import { StrokeStyle, ValueFormatter } from '../../../../utils/commons'; +import { StrokeStyle, ValueFormatter, Color } from '../../../../utils/commons'; import { percentValueGetter } from '../config/config'; import { $Values } from 'utility-types'; @@ -192,6 +192,7 @@ export function shapeViewModel( valueGetter: ValueGetterFunction, tree: HierarchyOfArrays, topGroove: Pixels, + containerBackgroundColor?: Color, ): ShapeViewModel { const { width, @@ -275,11 +276,17 @@ export function shapeViewModel( const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter; const getRowSets = treemapLayout - ? fillTextLayout(rectangleConstruction(treeHeight, topGroove), getRectangleRowGeometry, () => 0) + ? fillTextLayout( + rectangleConstruction(treeHeight, topGroove), + getRectangleRowGeometry, + () => 0, + containerBackgroundColor, + ) : fillTextLayout( ringSectorConstruction(config, innerRadius, ringThickness), getSectorRowGeometry, inSectorRotation(config.horizontalTextEnforcer, config.horizontalTextAngleThreshold), + containerBackgroundColor, ); const rowSets: RowSet[] = getRowSets( @@ -310,9 +317,7 @@ export function shapeViewModel( // successful text render if found, and has some row(s) return !(foundInFillText && foundInFillText.rows.length !== 0); }); - const maxLinkedLabelTextLength = config.linkLabel.maxTextLength; - const linkLabelViewModels = linkTextLayout( width, height, @@ -326,6 +331,7 @@ export function shapeViewModel( valueFormatter, maxLinkedLabelTextLength, diskCenter, + containerBackgroundColor, ); const pickQuads: PickFunction = (x, y) => { diff --git a/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts b/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts index a6729f7b87..c1556a9d5e 100644 --- a/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts +++ b/src/chart_types/partition_chart/renderer/canvas/canvas_renderers.ts @@ -30,6 +30,7 @@ import { TAU } from '../../layout/utils/math'; import { PartitionLayout } from '../../layout/types/config_types'; import { cssFontShorthand } from '../../layout/utils/measure'; import { clearCanvas, renderLayers, withContext } from '../../../../renderers/canvas'; +import { LinkLabelsViewModelSpec } from '../../layout/viewmodel/link_text_layout'; // the burnout avoidance in the center of the pie const LINE_WIDTH_MULT = 10; // border can be a maximum 1/LINE_WIDTH_MULT - th of the sector angle, otherwise the border would dominate @@ -186,41 +187,36 @@ function renderLinkLabels( ctx: CanvasRenderingContext2D, linkLabelFontSize: Pixels, linkLabelLineWidth: Pixels, - linkLabelTextColor: string, - viewModels: LinkLabelVM[], + { linkLabels, labelFontSpec, valueFontSpec, strokeColor }: LinkLabelsViewModelSpec, ) { + const labelColor = addOpacity(labelFontSpec.textColor, labelFontSpec.textOpacity); + const valueColor = addOpacity(valueFontSpec.textColor, valueFontSpec.textOpacity); const labelValueGap = linkLabelFontSize / 2; // one en space withContext(ctx, (ctx) => { ctx.lineWidth = linkLabelLineWidth; - ctx.strokeStyle = linkLabelTextColor; - ctx.fillStyle = linkLabelTextColor; - viewModels.forEach( - ({ - link, - translate, - textAlign, - text, - valueText, - width, - labelFontSpec, - valueFontSpec, - valueWidth, - }: LinkLabelVM) => { - ctx.beginPath(); - ctx.moveTo(...link[0]); - link.slice(1).forEach((point) => ctx.lineTo(...point)); - ctx.stroke(); - withContext(ctx, (ctx) => { - ctx.translate(...translate); - ctx.scale(1, -1); // flip for text rendering not to be upside down - ctx.textAlign = textAlign; - ctx.font = `${labelFontSpec.fontStyle} ${labelFontSpec.fontVariant} ${labelFontSpec.fontWeight} ${linkLabelFontSize}px ${labelFontSpec.fontFamily}`; - ctx.fillText(text, textAlign === 'right' ? -valueWidth - labelValueGap : 0, 0); - ctx.font = `${valueFontSpec.fontStyle} ${valueFontSpec.fontVariant} ${valueFontSpec.fontWeight} ${linkLabelFontSize}px ${valueFontSpec.fontFamily}`; - ctx.fillText(valueText, textAlign === 'left' ? width + labelValueGap : 0, 0); - }); - }, - ); + linkLabels.forEach(({ linkLabels, translate, textAlign, text, valueText, width, valueWidth }: LinkLabelVM) => { + // label lines + ctx.beginPath(); + ctx.moveTo(...linkLabels[0]); + linkLabels.slice(1).forEach((point) => ctx.lineTo(...point)); + ctx.strokeStyle = strokeColor; + ctx.stroke(); + withContext(ctx, (ctx) => { + ctx.translate(...translate); + ctx.scale(1, -1); // flip for text rendering not to be upside down + ctx.textAlign = textAlign; + // label text + ctx.strokeStyle = labelColor; + ctx.fillStyle = labelColor; + ctx.font = `${labelFontSpec.fontStyle} ${labelFontSpec.fontVariant} ${labelFontSpec.fontWeight} ${linkLabelFontSize}px ${labelFontSpec.fontFamily}`; + ctx.fillText(text, textAlign === 'right' ? -valueWidth - labelValueGap : 0, 0); + // value text + ctx.strokeStyle = valueColor; + ctx.fillStyle = valueColor; + ctx.font = `${valueFontSpec.fontStyle} ${valueFontSpec.fontVariant} ${valueFontSpec.fontWeight} ${linkLabelFontSize}px ${valueFontSpec.fontFamily}`; + ctx.fillText(valueText, textAlign === 'left' ? width + labelValueGap : 0, 0); + }); + }); }); } @@ -230,9 +226,9 @@ export function renderPartitionCanvas2d( dpr: number, { config, quadViewModel, rowSets, outsideLinksViewModel, linkLabelViewModels, diskCenter }: ShapeViewModel, ) { - const { sectorLineWidth, sectorLineStroke, linkLabel /*, backgroundColor*/ } = config; + const { sectorLineWidth, sectorLineStroke, linkLabel } = config; - const linkLabelTextColor = addOpacity(linkLabel.textColor, linkLabel.textOpacity); + const linkLineColor = addOpacity(linkLabel.textColor, linkLabel.textOpacity); withContext(ctx, (ctx) => { // set some defaults for the overall rendering @@ -263,7 +259,7 @@ export function renderPartitionCanvas2d( // The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way. renderLayers(ctx, [ // clear the canvas - (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000 /*, backgroundColor*/), + (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000), // bottom layer: sectors (pie slices, ring sectors etc.) (ctx: CanvasRenderingContext2D) => @@ -276,11 +272,11 @@ export function renderPartitionCanvas2d( // the link lines for the outside-fill text (ctx: CanvasRenderingContext2D) => - renderFillOutsideLinks(ctx, outsideLinksViewModel, linkLabelTextColor, linkLabel.lineWidth), + renderFillOutsideLinks(ctx, outsideLinksViewModel, linkLineColor, linkLabel.lineWidth), // all the text and link lines for single-row outside texts (ctx: CanvasRenderingContext2D) => - renderLinkLabels(ctx, linkLabel.fontSize, linkLabel.lineWidth, linkLabelTextColor, linkLabelViewModels), + renderLinkLabels(ctx, linkLabel.fontSize, linkLabel.lineWidth, linkLabelViewModels), ]); }); } diff --git a/src/chart_types/partition_chart/state/selectors/geometries.ts b/src/chart_types/partition_chart/state/selectors/geometries.ts index 69766e7901..b443137d9f 100644 --- a/src/chart_types/partition_chart/state/selectors/geometries.ts +++ b/src/chart_types/partition_chart/state/selectors/geometries.ts @@ -26,14 +26,18 @@ import { PartitionSpec } from '../../specs/index'; import { SpecTypes } from '../../../../specs/settings'; import { getTree } from './tree'; import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { isColorValid } from '../../layout/utils/calcs'; const getSpecs = (state: GlobalChartState) => state.specs; /** @internal */ export const partitionGeometries = createCachedSelector( - [getSpecs, getChartContainerDimensionsSelector, getTree], - (specs, parentDimensions, tree): ShapeViewModel => { + [getSpecs, getChartContainerDimensionsSelector, getTree, getChartThemeSelector], + (specs, parentDimensions, tree, theme): ShapeViewModel => { const pieSpecs = getSpecsFromStore(specs, ChartTypes.Partition, SpecTypes.Series); - return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions, tree) : nullShapeViewModel(); + const { color } = theme.background; + const bgColor: string | undefined = isColorValid(color) ? color : undefined; + return pieSpecs.length === 1 ? render(pieSpecs[0], parentDimensions, tree, bgColor) : nullShapeViewModel(); }, )((state) => state.chartId); diff --git a/src/chart_types/partition_chart/state/selectors/scenegraph.ts b/src/chart_types/partition_chart/state/selectors/scenegraph.ts index 1c09f39a5a..0567d98f5f 100644 --- a/src/chart_types/partition_chart/state/selectors/scenegraph.ts +++ b/src/chart_types/partition_chart/state/selectors/scenegraph.ts @@ -28,7 +28,7 @@ import { } from '../../layout/types/viewmodel_types'; import { DEPTH_KEY, HierarchyOfArrays } from '../../layout/utils/group_by_rollup'; import { PartitionSpec, Layer } from '../../specs/index'; -import { identity, mergePartial, RecursivePartial } from '../../../../utils/commons'; +import { identity, mergePartial, RecursivePartial, Color } from '../../../../utils/commons'; import { config as defaultConfig, VALUE_GETTERS } from '../../layout/config/config'; import { Config } from '../../layout/types/config_types'; @@ -49,6 +49,7 @@ export function render( partitionSpec: PartitionSpec, parentDimensions: Dimensions, tree: HierarchyOfArrays, + containerBackgroundColor?: Color, ): ShapeViewModel { const { width, height } = parentDimensions; const { layers, topGroove, config: specConfig } = partitionSpec; @@ -70,5 +71,6 @@ export function render( valueGetter, tree, topGroove, + containerBackgroundColor, ); } diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts index 3cf7152c73..fc0af7b44c 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts @@ -17,7 +17,7 @@ * under the License. */ import { Stroke, Line } from '../../../../../geoms/types'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { AnnotationLineProps } from '../../../annotations/line/types'; import { LineAnnotationStyle } from '../../../../../utils/themes/theme'; import { renderMultiLine } from '../primitives/line'; diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts index 914a54cc03..c16310fba1 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts @@ -20,7 +20,7 @@ import { renderRect } from '../primitives/rect'; import { Rect, Fill, Stroke } from '../../../../../geoms/types'; import { AnnotationRectProps } from '../../../annotations/rect/types'; import { RectAnnotationStyle } from '../../../../../utils/themes/theme'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { withContext } from '../../../../../renderers/canvas'; /** @internal */ diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts index 3c5e73878a..246bd475a8 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -21,7 +21,7 @@ import { AxisProps } from '.'; import { Position } from '../../../../../utils/commons'; import { TickStyle } from '../../../../../utils/themes/theme'; import { renderLine, MIN_STROKE_WIDTH } from '../primitives/line'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; /** @internal */ export function renderTick(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts index 0e8fea23b4..2cd16b5aea 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts @@ -81,6 +81,8 @@ export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, p fontStyle: labelStyle.fontStyle ? (labelStyle.fontStyle as FontStyle) : 'normal', fontVariant: 'normal', fontWeight: 'normal', + textColor: labelStyle.fill, + textOpacity: 1, }; withContext(ctx, (ctx) => { const textOffsetX = tickLabelRotation === 0 ? 0 : offsetX; diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts index 1e0358c6ed..1c6d1dd305 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts @@ -61,6 +61,8 @@ function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { fontVariant: 'normal', fontStyle: titleStyle.fontStyle ? (titleStyle.fontStyle as FontStyle) : 'normal', fontWeight: 'normal', + textColor: titleStyle.fill, + textOpacity: 1, }; renderText( ctx, @@ -99,6 +101,8 @@ function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) fontVariant: 'normal', fontStyle: titleStyle.fontStyle ? (titleStyle.fontStyle as FontStyle) : 'normal', fontWeight: 'normal', + textColor: titleStyle.fill, + textOpacity: 1, }; renderText( ctx, diff --git a/src/chart_types/xy_chart/renderer/canvas/grids.ts b/src/chart_types/xy_chart/renderer/canvas/grids.ts index 38f52e1e2d..7a54956ac1 100644 --- a/src/chart_types/xy_chart/renderer/canvas/grids.ts +++ b/src/chart_types/xy_chart/renderer/canvas/grids.ts @@ -24,7 +24,7 @@ import { AxisSpec } from '../../../../chart_types/xy_chart/utils/specs'; import { getSpecsById } from '../../state/utils'; import { renderMultiLine, MIN_STROKE_WIDTH } from './primitives/line'; import { Line, Stroke } from '../../../../geoms/types'; -import { stringToRGB } from '../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../partition_chart/layout/utils/color_library_wrappers'; import { withContext } from '../../../../renderers/canvas'; interface GridProps { diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts index 8a45768ce4..de946ab61c 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/arc.ts @@ -18,7 +18,7 @@ import { withContext } from '../../../../../renderers/canvas'; import { Circle, Stroke, Fill, Arc } from '../../../../../geoms/types'; -import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { MIN_STROKE_WIDTH } from './line'; /** @internal */ diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts index cf6e2899f7..4be6c6442e 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/line.ts @@ -17,7 +17,7 @@ * under the License. */ import { Stroke, Line } from '../../../../../geoms/types'; -import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { withContext } from '../../../../../renderers/canvas'; /** diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts index cbb773b8c7..6c93b69327 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/path.ts @@ -18,7 +18,7 @@ import { ClippedRanges } from '../../../../../utils/geometry'; import { withContext, withClipRanges } from '../../../../../renderers/canvas'; -import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { Rect, Stroke, Fill } from '../../../../../geoms/types'; import { MIN_STROKE_WIDTH } from './line'; diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts index 35a54abf5f..ac6e14e218 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts @@ -17,7 +17,7 @@ * under the License. */ import { Rect, Fill, Stroke } from '../../../../../geoms/types'; -import { RGBtoString } from '../../../../partition_chart/layout/utils/d3_utils'; +import { RGBtoString } from '../../../../partition_chart/layout/utils/color_library_wrappers'; /** @internal */ export function renderRect( diff --git a/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/src/chart_types/xy_chart/renderer/canvas/renderers.ts index 1e6d701613..c11becb0cb 100644 --- a/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -26,7 +26,7 @@ import { ReactiveChartStateProps } from './xy_chart'; import { renderAnnotations } from './annotations'; import { renderBarValues } from './values/bar'; import { renderDebugRect } from './utils/debug'; -import { stringToRGB } from '../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../partition_chart/layout/utils/color_library_wrappers'; import { Rect } from '../../../../geoms/types'; import { renderBubbles } from './bubbles'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts b/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts index 869760d07f..66a7d0bb1e 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/area.test.ts @@ -20,9 +20,9 @@ import { MockStyles } from '../../../../../mocks'; import { buildAreaStyles } from './area'; import { Fill } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; -jest.mock('../../../../partition_chart/layout/utils/d3_utils'); +jest.mock('../../../../partition_chart/layout/utils/color_library_wrappers'); jest.mock('../../../../../utils/commons'); const COLOR = 'aquamarine'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/area.ts b/src/chart_types/xy_chart/renderer/canvas/styles/area.ts index bf09e43fea..b1ed8afaec 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/area.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/area.ts @@ -17,7 +17,7 @@ * under the License. */ import { GeometryStateStyle, AreaStyle } from '../../../../../utils/themes/theme'; -import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { Fill } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts b/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts index a66da315ce..565bb75907 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/bar.test.ts @@ -20,9 +20,9 @@ import { MockStyles } from '../../../../../mocks'; import { buildBarStyles } from './bar'; import { Fill, Stroke } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; -jest.mock('../../../../partition_chart/layout/utils/d3_utils'); +jest.mock('../../../../partition_chart/layout/utils/color_library_wrappers'); jest.mock('../../../../../utils/commons'); const COLOR = 'aquamarine'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts b/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts index 7b951eb0ae..bb3e24efd9 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/bar.ts @@ -17,7 +17,7 @@ * under the License. */ import { GeometryStateStyle, RectStyle, RectBorderStyle } from '../../../../../utils/themes/theme'; -import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { Stroke, Fill } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts b/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts index 347fcd7099..a56a7edb34 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/line.test.ts @@ -20,9 +20,9 @@ import { MockStyles } from '../../../../../mocks'; import { buildLineStyles } from './line'; import { Stroke } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; -jest.mock('../../../../partition_chart/layout/utils/d3_utils'); +jest.mock('../../../../partition_chart/layout/utils/color_library_wrappers'); jest.mock('../../../../../utils/commons'); const COLOR = 'aquamarine'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/line.ts b/src/chart_types/xy_chart/renderer/canvas/styles/line.ts index c86dc2e8d8..b88d8637c0 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/line.ts @@ -17,7 +17,7 @@ * under the License. */ import { GeometryStateStyle, LineStyle } from '../../../../../utils/themes/theme'; -import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { Stroke } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/point.test.ts b/src/chart_types/xy_chart/renderer/canvas/styles/point.test.ts index c7f2fd32b8..f89b8424c0 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/point.test.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/point.test.ts @@ -20,10 +20,10 @@ import { MockStyles } from '../../../../../mocks'; import { buildPointStyles } from './point'; import { Fill, Stroke } from '../../../../../geoms/types'; import { getColorFromVariant } from '../../../../../utils/commons'; -import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { PointStyle } from '../../../../../utils/themes/theme'; -jest.mock('../../../../partition_chart/layout/utils/d3_utils'); +jest.mock('../../../../partition_chart/layout/utils/color_library_wrappers'); jest.mock('../../../../../utils/commons'); const COLOR = 'aquamarine'; diff --git a/src/chart_types/xy_chart/renderer/canvas/styles/point.ts b/src/chart_types/xy_chart/renderer/canvas/styles/point.ts index 9fc49382de..c900799cc5 100644 --- a/src/chart_types/xy_chart/renderer/canvas/styles/point.ts +++ b/src/chart_types/xy_chart/renderer/canvas/styles/point.ts @@ -17,7 +17,7 @@ * under the License. */ import { PointStyle, GeometryStateStyle } from '../../../../../utils/themes/theme'; -import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/d3_utils'; +import { stringToRGB, OpacityFn } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { Fill, Stroke } from '../../../../../geoms/types'; import { mergePartial, getColorFromVariant } from '../../../../../utils/commons'; diff --git a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts index 4ed16dc53d..7ac452b70a 100644 --- a/src/chart_types/xy_chart/renderer/canvas/values/bar.ts +++ b/src/chart_types/xy_chart/renderer/canvas/values/bar.ts @@ -55,6 +55,8 @@ export function renderBarValues(ctx: CanvasRenderingContext2D, props: BarValuesP fontStyle: fontStyle ? (fontStyle as FontStyle) : 'normal', fontVariant: 'normal', fontWeight: 'normal', + textColor: 'black', + textOpacity: 1, }; const { x, y, align, baseline, rect } = positionText( diff --git a/src/components/_container.scss b/src/components/_container.scss index 30f0eaf93a..739e1771fe 100644 --- a/src/components/_container.scss +++ b/src/components/_container.scss @@ -1,7 +1,7 @@ .echChart { + position: relative; display: flex; height: 100%; - position: relative; &--column { flex-direction: column; diff --git a/src/components/_global.scss b/src/components/_global.scss index b57fed5fa0..59d48a5a83 100644 --- a/src/components/_global.scss +++ b/src/components/_global.scss @@ -10,3 +10,11 @@ height: 0; position: absolute; } + +.echChartBackground { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} diff --git a/src/components/chart.tsx b/src/components/chart.tsx index b949301efd..fded2da8dd 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { CSSProperties, createRef } from 'react'; +import React, { createRef } from 'react'; import classNames from 'classnames'; import { Provider } from 'react-redux'; import { createStore, Store, Unsubscribe } from 'redux'; @@ -34,6 +34,7 @@ import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; import { onExternalPointerEvent } from '../state/actions/events'; import { PointerEvent } from '../specs'; import { getInternalIsInitializedSelector } from '../state/selectors/get_internal_is_intialized'; +import { ChartBackground } from './chart_background'; interface ChartProps { /** The type of rendered @@ -49,16 +50,6 @@ interface ChartState { legendPosition: Position; } -function getContainerStyle(size: any): CSSProperties { - if (size) { - return { - position: 'relative', - ...getChartSize(size), - }; - } - return {}; -} - export class Chart extends React.Component { static defaultProps: ChartProps = { renderer: 'canvas', @@ -84,7 +75,6 @@ export class Chart extends React.Component { this.state = { legendPosition: Position.Right, }; - this.unsubscribeToStore = this.chartStore.subscribe(() => { const state = this.chartStore.getState(); if (!getInternalIsInitializedSelector(state)) { @@ -157,14 +147,16 @@ export class Chart extends React.Component { render() { const { size, className } = this.props; - const containerStyle = getContainerStyle(size); + const containerSizeStyle = getChartSize(size); const horizontal = isHorizontalAxis(this.state.legendPosition); const chartClassNames = classNames('echChart', className, { 'echChart--column': horizontal, }); + return ( -
+
+ diff --git a/src/components/chart_background.tsx b/src/components/chart_background.tsx new file mode 100644 index 0000000000..082b20e104 --- /dev/null +++ b/src/components/chart_background.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ +import React from 'react'; +import { connect } from 'react-redux'; +import { getChartThemeSelector } from '../state/selectors/get_chart_theme'; +import { GlobalChartState } from '../state/chart_state'; +import { getInternalIsInitializedSelector } from '../state/selectors/get_internal_is_intialized'; + +interface ChartBackgroundProps { + backgroundColor: string; +} + +export class ChartBackgroundComponent extends React.Component { + static displayName = 'ChartBackground'; + + constructor(props: ChartBackgroundProps) { + super(props); + } + + render() { + const { backgroundColor } = this.props; + return
; + } +} + +const mapStateToProps = (state: GlobalChartState): ChartBackgroundProps => { + if (!getInternalIsInitializedSelector(state)) { + return { + backgroundColor: 'transparent', + }; + } + return { + backgroundColor: getChartThemeSelector(state).background.color, + }; +}; + +/** @internal */ +export const ChartBackground = connect(mapStateToProps)(ChartBackgroundComponent); diff --git a/src/components/legend/_legend.scss b/src/components/legend/_legend.scss index fcb5d4671c..d66e8b9c39 100644 --- a/src/components/legend/_legend.scss +++ b/src/components/legend/_legend.scss @@ -35,6 +35,7 @@ &--debug { background: red; + position: relative; } .echLegendListContainer { diff --git a/src/geoms/types.ts b/src/geoms/types.ts index db30c9929f..fa1bed611f 100644 --- a/src/geoms/types.ts +++ b/src/geoms/types.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { RgbObject } from '../chart_types/partition_chart/layout/utils/d3_utils'; +import { RgbObject } from '../chart_types/partition_chart/layout/utils/color_library_wrappers'; import { Radian } from '../chart_types/partition_chart/layout/types/geometry_types'; export interface Text { text: string; diff --git a/src/mocks/hierarchical/palettes.ts b/src/mocks/hierarchical/palettes.ts index 62c478476e..4e40d2f9e2 100644 --- a/src/mocks/hierarchical/palettes.ts +++ b/src/mocks/hierarchical/palettes.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { RgbTuple } from '../../chart_types/partition_chart/layout/utils/d3_utils'; +import { RgbTuple } from '../../chart_types/partition_chart/layout/utils/color_library_wrappers'; // prettier-ignore const CET2s: RgbTuple[] = [[46, 34, 235], [49, 32, 237], [52, 30, 238], [56, 29, 239], [59, 28, 240], [63, 27, 241], [66, 27, 242], [70, 27, 242], [73, 27, 243], [77, 28, 244], [80, 29, 244], [84, 30, 245], [87, 31, 245], [91, 32, 246], [94, 33, 246], [97, 35, 246], [100, 36, 247], [103, 38, 247], [106, 39, 248], [109, 41, 248], [112, 42, 248], [115, 44, 249], [118, 45, 249], [121, 47, 249], [123, 48, 250], [126, 49, 250], [129, 51, 250], [132, 52, 251], [135, 53, 251], [137, 54, 251], [140, 56, 251], [143, 57, 251], [146, 58, 252], [149, 59, 252], [152, 60, 252], [155, 60, 252], [158, 61, 252], [162, 62, 252], [165, 63, 252], [168, 63, 252], [171, 64, 252], [175, 65, 252], [178, 65, 252], [181, 66, 252], [185, 66, 252], [188, 66, 252], [191, 67, 252], [195, 67, 252], [198, 68, 252], [201, 68, 251], [204, 69, 251], [207, 69, 251], [211, 70, 251], [214, 70, 251], [217, 71, 250], [219, 72, 250], [222, 73, 250], [225, 74, 249], [227, 75, 249], [230, 76, 248], [232, 78, 247], [234, 79, 246], [236, 81, 245], [238, 83, 244], [240, 85, 243], [242, 88, 241], [243, 90, 240], [244, 93, 238], [245, 96, 236], [246, 99, 234], [247, 102, 232], [248, 105, 230], [249, 108, 227], [249, 111, 225], [250, 114, 223], [250, 117, 220], [251, 120, 217], [251, 123, 215], [252, 127, 212], [252, 130, 210], [252, 133, 207], [252, 136, 204], [252, 139, 201], [253, 141, 199], [253, 144, 196], [253, 147, 193], [253, 150, 190], [253, 153, 188], [253, 156, 185], [253, 158, 182], [253, 161, 179], [253, 164, 177], [253, 166, 174], [253, 169, 171], [253, 171, 168], [253, 174, 165], [252, 176, 162], [252, 179, 160], [252, 181, 157], [252, 184, 154], [252, 186, 151], [253, 188, 148], [253, 191, 145], [253, 193, 142], [253, 195, 139], [253, 198, 136], [253, 200, 133], [253, 202, 130], [253, 204, 127], [253, 207, 124], [253, 209, 120], [253, 211, 117], [253, 213, 114], [253, 215, 110], [253, 217, 107], [253, 219, 104], [253, 221, 100], [252, 223, 96], [252, 225, 93], [252, 227, 89], [251, 229, 85], [250, 231, 81], [250, 232, 77], [249, 234, 73], [248, 235, 69], [246, 236, 65], [245, 237, 61], [243, 238, 57], [242, 239, 54], [240, 239, 50], [238, 239, 46], [235, 239, 43], [233, 239, 40], [231, 239, 37], [228, 239, 35], [225, 238, 33], [223, 238, 31], [220, 237, 29], [217, 236, 27], [214, 235, 26], [211, 234, 25], [209, 233, 24], [206, 232, 24], [203, 231, 23], [200, 230, 22], [197, 229, 22], [194, 228, 21], [191, 227, 21], [188, 226, 21], [185, 225, 20], [182, 224, 20], [179, 223, 20], [176, 221, 19], [173, 220, 19], [170, 219, 19], [167, 218, 18], [164, 217, 18], [161, 216, 17], [158, 215, 17], [154, 214, 17], [151, 213, 16], [148, 211, 16], [145, 210, 16], [142, 209, 15], [139, 208, 15], [136, 207, 15], [132, 206, 14], [129, 205, 14], [126, 204, 14], [122, 202, 13], [119, 201, 13], [116, 200, 13], [112, 199, 13], [109, 198, 12], [105, 197, 12], [102, 196, 12], [98, 194, 12], [94, 193, 12], [91, 192, 12], [87, 191, 12], [83, 190, 13], [79, 188, 14], [76, 187, 15], [72, 186, 16], [68, 185, 18], [65, 183, 20], [62, 182, 22], [59, 181, 25], [56, 179, 27], [54, 178, 30], [52, 176, 34], [51, 175, 37], [50, 173, 40], [50, 172, 44], [50, 170, 48], [51, 168, 51], [52, 167, 55], [53, 165, 59], [54, 163, 63], [56, 161, 67], [57, 160, 71], [59, 158, 74], [60, 156, 78], [62, 154, 82], [63, 152, 86], [64, 150, 90], [66, 148, 93], [67, 147, 97], [67, 145, 101], [68, 143, 104], [69, 141, 108], [69, 139, 111], [69, 137, 115], [70, 135, 118], [70, 133, 122], [69, 131, 125], [69, 129, 129], [69, 128, 132], [68, 126, 135], [67, 124, 139], [67, 122, 142], [66, 120, 145], [64, 118, 149], [63, 116, 152], [62, 114, 155], [60, 112, 158], [59, 110, 162], [57, 108, 165], [56, 106, 168], [54, 104, 171], [53, 102, 174], [51, 100, 177], [50, 98, 180], [48, 96, 183], [47, 93, 185], [46, 91, 188], [45, 89, 191], [44, 86, 193], [43, 84, 196], [42, 81, 199], [41, 79, 201], [40, 76, 204], [40, 73, 206], [39, 70, 209], [38, 68, 211], [38, 65, 213], [37, 62, 216], [37, 59, 218], [37, 56, 220], [37, 53, 222], [37, 50, 224], [37, 47, 227], [38, 44, 228], [40, 41, 230], [42, 39, 232], [44, 36, 234],]; diff --git a/src/state/selectors/get_settings_specs.ts b/src/state/selectors/get_settings_specs.ts index 20d5510f15..0cbefd8466 100644 --- a/src/state/selectors/get_settings_specs.ts +++ b/src/state/selectors/get_settings_specs.ts @@ -20,7 +20,7 @@ import createCachedSelector from 're-reselect'; import { GlobalChartState } from '../chart_state'; import { ChartTypes } from '../../chart_types'; import { getSpecsFromStore } from '../utils'; -import { SettingsSpec, SpecTypes } from '../../specs/settings'; +import { SettingsSpec, SpecTypes, DEFAULT_SETTINGS_SPEC } from '../../specs/settings'; import { getChartIdSelector } from './get_chart_id'; const getSpecs = (state: GlobalChartState) => state.specs; @@ -30,9 +30,9 @@ export const getSettingsSpecSelector = createCachedSelector( [getSpecs], (specs): SettingsSpec => { const settingsSpecs = getSpecsFromStore(specs, ChartTypes.Global, SpecTypes.Settings); - if (settingsSpecs.length > 1) { - throw new Error('Multiple settings specs are configured on the same chart'); + if (settingsSpecs.length === 1) { + return settingsSpecs[0]; } - return settingsSpecs[0]; + return DEFAULT_SETTINGS_SPEC; }, )(getChartIdSelector); diff --git a/src/utils/chart_size.ts b/src/utils/chart_size.ts index 8589401563..1503582018 100644 --- a/src/utils/chart_size.ts +++ b/src/utils/chart_size.ts @@ -25,7 +25,10 @@ export interface ChartSizeObject { export type ChartSize = number | string | ChartSizeArray | ChartSizeObject; /** @internal */ -export function getChartSize(size: ChartSize) { +export function getChartSize(size?: ChartSize): ChartSizeObject { + if (size === undefined) { + return {}; + } if (Array.isArray(size)) { return { width: size[0] === undefined ? '100%' : size[0], diff --git a/src/utils/themes/dark_theme.ts b/src/utils/themes/dark_theme.ts index 14f7331ba0..834e846cde 100644 --- a/src/utils/themes/dark_theme.ts +++ b/src/utils/themes/dark_theme.ts @@ -163,4 +163,7 @@ export const DARK_THEME: Theme = { visible: true, }, }, + background: { + color: '#1D1E24', // $euiColorEmptyShade + }, }; diff --git a/src/utils/themes/light_theme.ts b/src/utils/themes/light_theme.ts index 53e452b578..b434763791 100644 --- a/src/utils/themes/light_theme.ts +++ b/src/utils/themes/light_theme.ts @@ -162,4 +162,7 @@ export const LIGHT_THEME: Theme = { visible: true, }, }, + background: { + color: '#FFFFFF', // $euiColorEmptyShade: #FFF !default; + }, }; diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index 1353796792..e888da1ccd 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -123,6 +123,17 @@ export interface ColorConfig { vizColors: Color[]; defaultVizColor: Color; } +/** + * The background style applied to the chart. + * This is used to coordinate adequate contrast of the text in partition and treemap charts. + * @public + */ +export interface BackgroundStyle { + /** + * The background color + */ + color: string; +} export interface LegendStyle { /** * Max width used for left/right legend @@ -197,6 +208,11 @@ export interface Theme { * value from 1 to 100 */ markSizeRatio?: number; + /** + * The background allows the consumer to provide a color of the background container of the chart. + * This can then be used to calculate the contrast of the text for partition charts. + */ + background: BackgroundStyle; } export type PartialTheme = RecursivePartial; diff --git a/stories/bar/1_basic.tsx b/stories/bar/1_basic.tsx index c5b752d0ea..84d5b54dea 100644 --- a/stories/bar/1_basic.tsx +++ b/stories/bar/1_basic.tsx @@ -19,7 +19,7 @@ import { boolean } from '@storybook/addon-knobs'; import React from 'react'; -import { BarSeries, Chart, ScaleType } from '../../src'; +import { BarSeries, Chart, ScaleType, Settings, DARK_THEME, LIGHT_THEME } from '../../src'; export const Example = () => { const darkmode = boolean('darkmode', false); @@ -36,6 +36,7 @@ export const Example = () => { const specId = toggleSpec ? 'bars1' : 'bars2'; return ( + { const darkmode = boolean('darkmode', false); const className = darkmode ? 'story-chart-dark' : 'story-chart'; - const defaultTheme = darkmode ? DARK_THEME : LIGHT_THEME; return ( - + Number(d).toFixed(2)} /> diff --git a/stories/interactions/14_crosshair_time.tsx b/stories/interactions/14_crosshair_time.tsx index 3189d96d53..5624bb6588 100644 --- a/stories/interactions/14_crosshair_time.tsx +++ b/stories/interactions/14_crosshair_time.tsx @@ -67,7 +67,12 @@ export const Example = () => { return ( - + { + const partialColorTheme: PartialTheme = { + background: { + color: color('Color of the background container', 'rgba(255, 255, 255, 1)'), + }, + }; + const invertTextColors = boolean('invert colors for lightness/darkness', true); + const toggleTextContrast = boolean('set text contrast to true or false', true); + return ( + + + d.exportVal as number} + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d: Datum) => d.sitc1, + nodeLabel: (d: any) => productLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.7)(d.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d: any) => regionLookup[d].regionName, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.5)(d.parent.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => d.dest, + nodeLabel: (d: any) => countryLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.3)(d.parent.parent.sortIndex); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 0, + fontSize: 14, + }, + fontFamily: 'Arial', + fillLabel: { + valueFormatter: (d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, + fontStyle: 'italic', + textInvertible: invertTextColors, + textContrast: toggleTextContrast, + fontWeight: 900, + valueFont: { + fontFamily: 'Menlo', + fontStyle: 'normal', + fontWeight: 100, + }, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 1, + emptySizeRatio: 0, + circlePadding: 4, + backgroundColor: 'rgba(229,229,229,1)', + }} + /> + + ); +}; diff --git a/stories/stylings/21_partition_labels.tsx b/stories/stylings/21_partition_labels.tsx new file mode 100644 index 0000000000..65d625290b --- /dev/null +++ b/stories/stylings/21_partition_labels.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { Chart, Datum, Partition, Settings } from '../../src'; +import { mocks } from '../../src/mocks/hierarchical/index'; +import { config } from '../../src/chart_types/partition_chart/layout/config/config'; +import React from 'react'; +import { indexInterpolatedFillColor, interpolatorCET2s, productLookup } from '../utils/utils'; +import { color } from '@storybook/addon-knobs'; + +export const Example = () => { + const partialCustomTheme = { + background: { + color: color('Change background container color', '#1c1c24'), + }, + }; + return ( + + + d.exportVal as number} + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d: Datum) => d.sitc1, + nodeLabel: (d: Datum) => productLookup[d].name, + fillLabel: { textInvertible: true, textContrast: true }, + shape: { + fillColor: indexInterpolatedFillColor(interpolatorCET2s), + }, + }, + ]} + /> + + ); +}; diff --git a/stories/stylings/5_partial_custom_theme.tsx b/stories/stylings/5_partial_custom_theme.tsx index e61d8ca536..62bec73a02 100644 --- a/stories/stylings/5_partial_custom_theme.tsx +++ b/stories/stylings/5_partial_custom_theme.tsx @@ -33,6 +33,9 @@ export const Example = () => { visible: true, }, }, + background: { + color: color('Change background container color', 'white'), + }, }; return ( diff --git a/stories/stylings/stylings.stories.tsx b/stories/stylings/stylings.stories.tsx index 0b72de1a7b..51c3503e7e 100644 --- a/stories/stylings/stylings.stories.tsx +++ b/stories/stylings/stylings.stories.tsx @@ -34,7 +34,6 @@ export { Example as partialCustomThemeWithBaseTheme } from './6_partial_and_base export { Example as multipleCustomPartialThemes } from './7_multiple_custom'; export { Example as customSeriesColorsViaColorsArray } from './8_custom_series_colors_array'; export { Example as customSeriesColorsViaAccessorFunction } from './9_custom_series_colors_function'; - export { Example as customSeriesStylesBars } from './10_custom_bars'; export { Example as customSeriesStylesLines } from './11_custom_lines'; export { Example as customSeriesStylesArea } from './12_custom_area'; @@ -46,3 +45,5 @@ export { Example as styleAccessorOverrides } from './16_style_accessor'; export { Example as barSeriesColorVariant } from './17_bar_series_color_variant'; export { Example as lineSeriesColorVariant } from './18_line_series_color_variant'; export { Example as areaSeriesColorVariant } from './19_area_series_color_variant'; +export { Example as partitionBackground } from './20_partition_background'; +export { Example as partitionLabels } from './21_partition_labels'; diff --git a/stories/sunburst/29_custom_stroke.tsx b/stories/sunburst/29_custom_stroke.tsx index d9c9df95b0..fb8177f1bd 100644 --- a/stories/sunburst/29_custom_stroke.tsx +++ b/stories/sunburst/29_custom_stroke.tsx @@ -16,35 +16,44 @@ * specific language governing permissions and limitations * under the License. */ -import { Chart, Datum, Partition, PartitionLayout } from '../../src'; +import { Chart, Datum, Partition, PartitionLayout, Settings, DARK_THEME } from '../../src'; import { mocks } from '../../src/mocks/hierarchical/index'; import { config } from '../../src/chart_types/partition_chart/layout/config/config'; import React from 'react'; import { countryLookup, indexInterpolatedFillColor, interpolatorCET2s } from '../utils/utils'; +import { color } from '@storybook/addon-knobs'; -export const Example = () => ( - - d.exportVal as number} - valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} - layers={[ - { - groupByRollup: (d: Datum) => d.origin, - nodeLabel: (d: Datum) => countryLookup[d].name, - fillLabel: { textInvertible: true }, - shape: { - fillColor: indexInterpolatedFillColor(interpolatorCET2s), +export const Example = () => { + const partialCustomTheme = { + background: { + color: color('Change background container color', '#1c1c24'), + }, + }; + return ( + + + d.exportVal as number} + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d: Datum) => d.origin, + nodeLabel: (d: Datum) => countryLookup[d].name, + fillLabel: { textInvertible: true }, + shape: { + fillColor: indexInterpolatedFillColor(interpolatorCET2s), + }, }, - }, - ]} - config={{ - partitionLayout: PartitionLayout.sunburst, - linkLabel: { maxCount: 15, textColor: 'white' }, - sectorLineStroke: 'rgb(26, 27, 32)', // same as the dark theme - sectorLineWidth: 1.2, - }} - /> - -); + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { maxCount: 15, textColor: 'white' }, + sectorLineStroke: 'rgb(26, 27, 32)', // same as the dark theme + sectorLineWidth: 1.2, + }} + /> + + ); +}; diff --git a/stories/treemap/5_multicolor.tsx b/stories/treemap/5_multicolor.tsx index aed0c30a30..c8596974a7 100644 --- a/stories/treemap/5_multicolor.tsx +++ b/stories/treemap/5_multicolor.tsx @@ -68,7 +68,7 @@ export const Example = () => ( nodeLabel: (d: any) => countryLookup[d].name, fillLabel: { valueFormatter: (d: number) => `${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, - textColor: 'rgb(60,60,60,1)', + textColor: 'rgba(60,60,60,1)', textInvertible: false, fontWeight: 100, fontStyle: 'normal', diff --git a/stories/treemap/6_custom_style.tsx b/stories/treemap/6_custom_style.tsx index 88ddd3901b..6d78fba020 100644 --- a/stories/treemap/6_custom_style.tsx +++ b/stories/treemap/6_custom_style.tsx @@ -65,7 +65,7 @@ export const Example = () => ( nodeLabel: (d: any) => countryLookup[d].name, fillLabel: { valueFormatter: (d: number) => `${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`, - textColor: 'rgb(60,60,60,1)', + textColor: 'rgba(60,60,60,1)', textInvertible: false, fontWeight: 600, fontStyle: 'normal', diff --git a/yarn.lock b/yarn.lock index 7dd4062db0..6153a91fae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4798,16 +4798,35 @@ dependencies: "@types/node" "*" +"@types/chroma-js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" + integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== + "@types/classnames@^2.2.7": version "2.2.9" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b" integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A== -"@types/color-name@^1.1.1": +"@types/color-convert@*": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d" + integrity sha512-OKGEfULrvSL2VRbkl/gnjjgbbF7ycIlpSsX7Nkab4MOWi5XxmgBYvuiQ7lcCFY5cPDz7MUNaKgxte2VRmtr4Fg== + dependencies: + "@types/color-name" "*" + +"@types/color-name@*", "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/color@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.1.tgz#2900490ed04da8116c5058cd5dba3572d5a25071" + integrity sha512-oeUWVaAwI+xINDUx+3F2vJkl/vVB03VChFF/Gl3iQCdbcakjuoJyMOba+3BXRtnBhxZ7uBYqQBi9EpLnvSoztA== + dependencies: + "@types/color-convert" "*" + "@types/core-js@^2.5.2": version "2.5.2" resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f" @@ -7238,6 +7257,13 @@ chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.3: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chroma-js@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.1.0.tgz#c0be48a21fe797ef8965608c1c4f911ef2da49d5" + integrity sha512-uiRdh4ZZy+UTPSrAdp8hqEdVb1EllLtTHOt5TMaOjJUvi+O54/83Fc5K2ld1P+TJX+dw5B+8/sCgzI6eaur/lg== + dependencies: + cross-env "^6.0.3" + chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" @@ -8049,6 +8075,13 @@ create-react-context@^0.3.0: gud "^1.0.0" warning "^4.0.3" +cross-env@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941" + integrity sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag== + dependencies: + cross-spawn "^7.0.0" + cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"