diff --git a/package.json b/package.json
index b3dcfb2aa3b0a..261b3ad74d9b7 100644
--- a/package.json
+++ b/package.json
@@ -314,6 +314,7 @@
"@types/cheerio": "^0.22.10",
"@types/chromedriver": "^2.38.0",
"@types/classnames": "^2.2.9",
+ "@types/color": "^3.0.0",
"@types/d3": "^3.5.43",
"@types/dedent": "^0.7.0",
"@types/deep-freeze-strict": "^1.1.0",
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss
index 90c2007b1c94a..3db09bace079f 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/_vis_types.scss
@@ -7,4 +7,21 @@
.tvbVisTimeSeries {
overflow: hidden;
}
+ .tvbVisTimeSeriesDark {
+ .echReactiveChart_unavailable {
+ color: #DFE5EF;
+ }
+ .echLegendItem {
+ color: #DFE5EF;
+ }
+ }
+ .tvbVisTimeSeriesLight {
+ .echReactiveChart_unavailable {
+ color: #343741;
+ }
+ .echLegendItem {
+ color: #343741;
+ }
+ }
}
+
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js
index 954d3d174bb8c..356ba08ac2427 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_types/timeseries/vis.js
@@ -33,9 +33,8 @@ import { getAxisLabelString } from '../../lib/get_axis_label_string';
import { getInterval } from '../../lib/get_interval';
import { areFieldsDifferent } from '../../lib/charts';
import { createXaxisFormatter } from '../../lib/create_xaxis_formatter';
-import { isBackgroundDark } from '../../../lib/set_is_reversed';
import { STACKED_OPTIONS } from '../../../visualizations/constants';
-import { getCoreStart } from '../../../services';
+import { getCoreStart, getUISettings } from '../../../services';
export class TimeseriesVisualization extends Component {
static propTypes = {
@@ -238,6 +237,7 @@ export class TimeseriesVisualization extends Component {
}
});
+ const darkMode = getUISettings().get('theme:darkMode');
return (
null;
export const AreaSeries = () => null;
+
+export { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js
index 986111b462b35..75554a476bdea 100644
--- a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/index.js
@@ -19,14 +19,13 @@
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
+import classNames from 'classnames';
import {
Axis,
Chart,
Position,
Settings,
- DARK_THEME,
- LIGHT_THEME,
AnnotationDomainTypes,
LineAnnotation,
TooltipType,
@@ -40,6 +39,7 @@ import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constan
import { AreaSeriesDecorator } from './decorators/area_decorator';
import { BarSeriesDecorator } from './decorators/bar_decorator';
import { getStackAccessors } from './utils/stack_format';
+import { getTheme, getChartClasses } from './utils/theme';
const generateAnnotationData = (values, formatter) =>
values.map(({ key, docs }) => ({
@@ -57,7 +57,8 @@ const handleCursorUpdate = cursor => {
};
export const TimeSeries = ({
- isDarkMode,
+ darkMode,
+ backgroundColor,
showGrid,
legend,
legendPosition,
@@ -89,8 +90,13 @@ export const TimeSeries = ({
const timeZone = timezoneProvider(uiSettings)();
const hasBarChart = series.some(({ bars }) => bars.show);
+ // compute the theme based on the bg color
+ const theme = getTheme(darkMode, backgroundColor);
+ // apply legend style change if bgColor is configured
+ const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor));
+
return (
-
+
{
+ it('should return the basic themes if no bg color is specified', () => {
+ // use original dark/light theme
+ expect(getTheme(false)).toEqual(LIGHT_THEME);
+ expect(getTheme(true)).toEqual(DARK_THEME);
+
+ // discard any wrong/missing bg color
+ expect(getTheme(true, null)).toEqual(DARK_THEME);
+ expect(getTheme(true, '')).toEqual(DARK_THEME);
+ expect(getTheme(true, undefined)).toEqual(DARK_THEME);
+ });
+ it('should return a highcontrast color theme for a different background', () => {
+ // red use a near full-black color
+ expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)');
+
+ // violet increased the text color to full white for higer contrast
+ expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)');
+
+ // light yellow, prefer the LIGHT_THEME fill color because already with a good contrast
+ expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333');
+ });
+});
diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts
new file mode 100644
index 0000000000000..a25d5e1ce1d35
--- /dev/null
+++ b/src/legacy/core_plugins/vis_type_timeseries/public/visualizations/views/timeseries/utils/theme.ts
@@ -0,0 +1,139 @@
+/*
+ * 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 colorJS from 'color';
+import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts';
+
+function computeRelativeLuminosity(rgb: string) {
+ return colorJS(rgb).luminosity();
+}
+
+function computeContrast(rgb1: string, rgb2: string) {
+ return colorJS(rgb1).contrast(colorJS(rgb2));
+}
+
+function getAAARelativeLum(bgColor: string, fgColor: string, ratio = 7) {
+ const relLum1 = computeRelativeLuminosity(bgColor);
+ const relLum2 = computeRelativeLuminosity(fgColor);
+ if (relLum1 > relLum2) {
+ // relLum1 is brighter, relLum2 is darker
+ return (relLum1 + 0.05 - ratio * 0.05) / ratio;
+ } else {
+ // relLum1 is darker, relLum2 is brighter
+ return Math.min(ratio * (relLum1 + 0.05) - 0.05, 1);
+ }
+}
+
+function getGrayFromRelLum(relLum: number) {
+ if (relLum <= 0.0031308) {
+ return relLum * 12.92;
+ } else {
+ return (1.0 + 0.055) * Math.pow(relLum, 1.0 / 2.4) - 0.055;
+ }
+}
+
+function getGrayRGBfromGray(gray: number) {
+ const g = Math.round(gray * 255);
+ return `rgb(${g},${g},${g})`;
+}
+
+function getAAAGray(bgColor: string, fgColor: string, ratio = 7) {
+ const relLum = getAAARelativeLum(bgColor, fgColor, ratio);
+ const gray = getGrayFromRelLum(relLum);
+ return getGrayRGBfromGray(gray);
+}
+
+function findBestContrastColor(
+ bgColor: string,
+ lightFgColor: string,
+ darkFgColor: string,
+ ratio = 4.5
+) {
+ const lc = computeContrast(bgColor, lightFgColor);
+ const dc = computeContrast(bgColor, darkFgColor);
+ if (lc >= dc) {
+ if (lc >= ratio) {
+ return lightFgColor;
+ }
+ return getAAAGray(bgColor, lightFgColor, ratio);
+ }
+ if (dc >= ratio) {
+ return darkFgColor;
+ }
+ return getAAAGray(bgColor, darkFgColor, ratio);
+}
+
+function isValidColor(color: string | null | undefined): color is string {
+ if (typeof color !== 'string') {
+ return false;
+ }
+ if (color.length === 0) {
+ return false;
+ }
+ try {
+ colorJS(color);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function getTheme(darkMode: boolean, bgColor?: string | null): Theme {
+ if (!isValidColor(bgColor)) {
+ return darkMode ? DARK_THEME : LIGHT_THEME;
+ }
+
+ const bgLuminosity = computeRelativeLuminosity(bgColor);
+ const mainTheme = bgLuminosity <= 0.179 ? DARK_THEME : LIGHT_THEME;
+ const color = findBestContrastColor(
+ bgColor,
+ LIGHT_THEME.axes.axisTitleStyle.fill,
+ DARK_THEME.axes.axisTitleStyle.fill
+ );
+ return {
+ ...mainTheme,
+ axes: {
+ ...mainTheme.axes,
+ axisTitleStyle: {
+ ...mainTheme.axes.axisTitleStyle,
+ fill: color,
+ },
+ tickLabelStyle: {
+ ...mainTheme.axes.tickLabelStyle,
+ fill: color,
+ },
+ axisLineStyle: {
+ ...mainTheme.axes.axisLineStyle,
+ stroke: color,
+ },
+ tickLineStyle: {
+ ...mainTheme.axes.tickLineStyle,
+ stroke: color,
+ },
+ },
+ };
+}
+
+export function getChartClasses(bgColor?: string) {
+ // keep the original theme color if no bg color is specified
+ if (typeof bgColor !== 'string') {
+ return;
+ }
+ const bgLuminosity = computeRelativeLuminosity(bgColor);
+ return bgLuminosity <= 0.179 ? 'tvbVisTimeSeriesDark' : 'tvbVisTimeSeriesLight';
+}