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:
parent
c846272c85
commit
e701e14c95
|
@ -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",
|
||||
|
|
|
@ -7,4 +7,21 @@
|
|||
.tvbVisTimeSeries {
|
||||
overflow: hidden;
|
||||
}
|
||||
.tvbVisTimeSeriesDark {
|
||||
.echReactiveChart_unavailable {
|
||||
color: #DFE5EF;
|
||||
}
|
||||
.echLegendItem {
|
||||
color: #DFE5EF;
|
||||
}
|
||||
}
|
||||
.tvbVisTimeSeriesLight {
|
||||
.echReactiveChart_unavailable {
|
||||
color: #343741;
|
||||
}
|
||||
.echLegendItem {
|
||||
color: #343741;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -40,3 +40,5 @@ export const ScaleType = {
|
|||
|
||||
export const BarSeries = () => null;
|
||||
export const AreaSeries = () => null;
|
||||
|
||||
export { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
}
|
Loading…
Reference in a new issue