[TSVB] fix text color when using custom background color (#60261) (#60364)

When the user apply a background color manually from the UI,
this commit adapt the current colors to have a better contrast with
the chosen background color irrespective of the used dark/light theme
This commit is contained in:
Marco Vettorello 2020-03-17 16:10:59 +01:00 committed by GitHub
parent c846272c85
commit e701e14c95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 220 additions and 9 deletions

View file

@ -309,6 +309,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",

View file

@ -7,4 +7,21 @@
.tvbVisTimeSeries {
overflow: hidden;
}
.tvbVisTimeSeriesDark {
.echReactiveChart_unavailable {
color: #DFE5EF;
}
.echLegendItem {
color: #DFE5EF;
}
}
.tvbVisTimeSeriesLight {
.echReactiveChart_unavailable {
color: #343741;
}
.echLegendItem {
color: #343741;
}
}
}

View file

@ -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 (
<div className="tvbVis" style={styles.tvbVis}>
<TimeSeries
@ -245,7 +245,8 @@ export class TimeseriesVisualization extends Component {
yAxis={yAxis}
onBrush={onBrush}
enableHistogramMode={enableHistogramMode}
isDarkMode={isBackgroundDark(model.background_color)}
backgroundColor={model.background_color}
darkMode={darkMode}
showGrid={Boolean(model.show_grid)}
legend={Boolean(model.show_legend)}
legendPosition={model.legend_position}

View file

@ -40,3 +40,5 @@ export const ScaleType = {
export const BarSeries = () => null;
export const AreaSeries = () => null;
export { LIGHT_THEME, DARK_THEME } from '@elastic/charts';

View file

@ -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 (
<Chart ref={chartRef} renderer="canvas" className="tvbVisTimeSeries">
<Chart ref={chartRef} renderer="canvas" className={classes}>
<Settings
showLegend={legend}
legendPosition={legendPosition}
@ -108,7 +114,7 @@ export const TimeSeries = ({
},
}
}
baseTheme={isDarkMode ? DARK_THEME : LIGHT_THEME}
baseTheme={theme}
tooltip={{
snap: true,
type: TooltipType.VerticalCursor,
@ -240,7 +246,8 @@ TimeSeries.defaultProps = {
};
TimeSeries.propTypes = {
isDarkMode: PropTypes.bool,
darkMode: PropTypes.bool,
backgroundColor: PropTypes.string,
showGrid: PropTypes.bool,
legend: PropTypes.bool,
legendPosition: PropTypes.string,

View file

@ -0,0 +1,44 @@
/*
* 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 { getTheme } from './theme';
import { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
describe('TSVB theme', () => {
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');
});
});

View file

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