Skip to content

Commit

Permalink
[TSVB] fix text color when using custom background color (elastic#60261)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
markov00 authored Mar 17, 2020
1 parent a755e55 commit e25430b
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 9 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,21 @@
.tvbVisTimeSeries {
overflow: hidden;
}
.tvbVisTimeSeriesDark {
.echReactiveChart_unavailable {
color: #DFE5EF;
}
.echLegendItem {
color: #DFE5EF;
}
}
.tvbVisTimeSeriesLight {
.echReactiveChart_unavailable {
color: #343741;
}
.echLegendItem {
color: #343741;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -238,14 +237,16 @@ export class TimeseriesVisualization extends Component {
}
});

const darkMode = getUISettings().get('theme:darkMode');
return (
<div className="tvbVis" style={styles.tvbVis}>
<TimeSeries
series={series}
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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ export const ScaleType = {

export const BarSeries = () => null;
export const AreaSeries = () => null;

export { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 }) => ({
Expand All @@ -57,7 +57,8 @@ const handleCursorUpdate = cursor => {
};

export const TimeSeries = ({
isDarkMode,
darkMode,
backgroundColor,
showGrid,
legend,
legendPosition,
Expand Down Expand Up @@ -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}
Expand All @@ -108,7 +114,7 @@ export const TimeSeries = ({
},
}
}
baseTheme={isDarkMode ? DARK_THEME : LIGHT_THEME}
baseTheme={theme}
tooltip={{
snap: true,
type: TooltipType.VerticalCursor,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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';
}

0 comments on commit e25430b

Please sign in to comment.