From 766f1ad34858b2871cd48154874007a0ea9034fc Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 19 Feb 2019 10:02:51 +0100 Subject: [PATCH] feat: add dark theme (#44) * feat(theme): add dark theme The Dark Theme is now available. The AxisConfig has now a cleaner styling, splitted by axis or tick styles. Changing a theme is now easier and it's not necessary tied to a darkmode flag. New theme can be added and used on the charts. fix #35 BREAKING CHANGE: The `Theme.AxisConfig` type has a different signature. It now contains `axisTitleStyle`, `axisLineStyle`, `tickLabelStyle` and `tickLineStyle` defined as `TextStyle` or `StrokeStyle` elements. The `Theme` interface is changed in a more flat structure. `darkMode` prop from `Setting` is removed. `theme` prop in `Setting` is now a `Theme` type object, not a `PartialTheme`. You can use `mergeWithDefaultTheme` function to merge an existing theme with a partial one. --- .storybook/config.ts | 12 +- .storybook/{style.css => style.scss} | 8 + .storybook/theme_service.ts | 17 + src/components/_legend.scss | 4 +- src/components/legend.tsx | 8 +- .../react_canvas/area_geometries.tsx | 60 ++-- src/components/react_canvas/axis.tsx | 107 +++--- .../react_canvas/bar_geometries.tsx | 26 +- .../react_canvas/line_geometries.tsx | 37 ++- .../react_canvas/reactive_chart.tsx | 38 ++- src/components/svg/axis.tsx | 22 +- src/index.ts | 9 +- src/lib/axes/axis_utils.test.ts | 9 +- src/lib/axes/axis_utils.ts | 90 ++--- src/lib/series/rendering.ts | 13 +- src/lib/themes/dark_theme.ts | 99 ++++++ src/lib/themes/light_theme.ts | 99 ++++++ src/lib/themes/theme.ts | 312 +++++++----------- src/lib/themes/theme_commons.ts | 29 ++ src/lib/utils/dimensions.test.ts | 20 +- src/lib/utils/dimensions.ts | 7 +- src/specs/settings.tsx | 7 +- src/state/chart_state.ts | 5 +- stories/axis.tsx | 35 +- stories/bar_chart.tsx | 16 +- stories/grid.tsx | 4 +- stories/interactions.tsx | 5 +- stories/line_chart.tsx | 2 +- stories/mixed.tsx | 3 +- stories/styling.tsx | 287 ++++++++++++++-- stories/utils.ts | 0 yarn.lock | 5 + 32 files changed, 922 insertions(+), 473 deletions(-) rename .storybook/{style.css => style.scss} (67%) create mode 100644 .storybook/theme_service.ts create mode 100644 src/lib/themes/dark_theme.ts create mode 100644 src/lib/themes/light_theme.ts create mode 100644 src/lib/themes/theme_commons.ts create mode 100644 stories/utils.ts diff --git a/.storybook/config.ts b/.storybook/config.ts index b3a58de16f..53121afe9a 100644 --- a/.storybook/config.ts +++ b/.storybook/config.ts @@ -1,10 +1,12 @@ -import '@elastic/eui/dist/eui_theme_light.css'; import { withInfo } from '@storybook/addon-info'; import { withKnobs } from '@storybook/addon-knobs'; import { withOptions } from '@storybook/addon-options'; import { addDecorator, configure } from '@storybook/react'; import '../src/index.scss'; -import './style.css'; +import './style.scss'; +import { switchTheme } from './theme_service'; + +switchTheme('light'); addDecorator( withOptions({ @@ -19,6 +21,12 @@ addDecorator( withInfo({ inline: true, source: false, + styles: { + infoBody: { + marginTop: 0, + marginBottom: 0, + }, + }, }), ); diff --git a/.storybook/style.css b/.storybook/style.scss similarity index 67% rename from .storybook/style.css rename to .storybook/style.scss index 4270e11309..7a0eccb27e 100644 --- a/.storybook/style.css +++ b/.storybook/style.scss @@ -1,6 +1,11 @@ +@import '../node_modules/@elastic/eui/src/themes/eui/eui_colors_dark.scss'; + .story-chart { background: white; } +.story-chart-dark { + background: $euiColorEmptyShade; +} #root { background-color: blanchedalmond; } @@ -21,3 +26,6 @@ border: 1px solid gray; padding: 5px; } +.Pane.vertical.Pane1 { + background: red; +} diff --git a/.storybook/theme_service.ts b/.storybook/theme_service.ts new file mode 100644 index 0000000000..70c40abc87 --- /dev/null +++ b/.storybook/theme_service.ts @@ -0,0 +1,17 @@ +// @ts-ignore +import themeDark from '!!style-loader/useable!css-loader!@elastic/eui/dist/eui_theme_dark.css'; +// @ts-ignore +import themeLight from '!!style-loader/useable!css-loader!@elastic/eui/dist/eui_theme_light.css'; + +export function switchTheme(theme: string) { + switch (theme) { + case 'light': + themeDark.unuse(); + themeLight.use(); + return; + case 'dark': + themeLight.unuse(); + themeDark.use(); + return; + } +} diff --git a/src/components/_legend.scss b/src/components/_legend.scss index cc49a933a3..2cb4c6fe89 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -1,5 +1,5 @@ -$elasticChartsLegendMaxWidth: $euiSize * 10 + $euiSize; -$elasticChartsLegendMaxHeight: $euiSize * 4 + $euiSize; +$elasticChartsLegendMaxWidth: $euiSize * 10; +$elasticChartsLegendMaxHeight: $euiSize * 4; .elasticChartsLegend { position: absolute; diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 7211f12cf6..6f44774edc 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -48,13 +48,13 @@ class LegendComponent extends React.Component { let paddingStyle; if (isVertical(legendPosition)) { paddingStyle = { - paddingTop: chartTheme.chart.margins.top, - paddingBottom: chartTheme.chart.margins.bottom, + paddingTop: chartTheme.chartMargins.top, + paddingBottom: chartTheme.chartMargins.bottom, }; } else { paddingStyle = { - paddingLeft: chartTheme.chart.margins.left, - paddingRight: chartTheme.chart.margins.right, + paddingLeft: chartTheme.chartMargins.left, + paddingRight: chartTheme.chartMargins.right, }; } return ( diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx index b0c0a7aef3..5d5f9533b2 100644 --- a/src/components/react_canvas/area_geometries.tsx +++ b/src/components/react_canvas/area_geometries.tsx @@ -4,8 +4,13 @@ import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { AreaGeometry, GeometryValue, getGeometryStyle, PointGeometry } from '../../lib/series/rendering'; -import { AreaSeriesStyle } from '../../lib/themes/theme'; +import { + AreaGeometry, + GeometryValue, + getGeometryStyle, + PointGeometry, +} from '../../lib/series/rendering'; +import { AreaSeriesStyle, SharedGeometryStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; interface AreaGeometriesDataProps { @@ -13,6 +18,7 @@ interface AreaGeometriesDataProps { areas: AreaGeometry[]; num?: number; style: AreaSeriesStyle; + sharedStyle: SharedGeometryStyle; onElementClick?: ElementClickListener; onElementOver: ((tooltip: TooltipData) => void) & IAction; onElementOut: (() => void) & IAction; @@ -24,7 +30,7 @@ interface AreaGeometriesDataState { export class AreaGeometries extends React.PureComponent< AreaGeometriesDataProps, AreaGeometriesDataState - > { +> { static defaultProps: Partial = { animated: false, num: 1, @@ -86,31 +92,31 @@ export class AreaGeometries extends React.PureComponent< [] as JSX.Element[], ); } - private renderPoints = (points: PointGeometry[], i: number): JSX.Element[] => { - const { style } = this.props; + private renderPoints = (areaPoints: PointGeometry[], i: number): JSX.Element[] => { + const { radius, stroke, strokeWidth } = this.props.style.point; const { overPoint } = this.state; - return points.map((point, index) => { - const { x, y, color, value, transform } = point; + return areaPoints.map((areaPoint, index) => { + const { x, y, color, value, transform } = areaPoint; return ( { - const { areas } = this.props; + const { areas, sharedStyle } = this.props; return areas.map((glyph, i) => { const { area, color, transform, geometryId } = glyph; - const geometryStyle = getGeometryStyle(geometryId, this.props.highlightedLegendItem); + const geometryStyle = getGeometryStyle( + geometryId, + this.props.highlightedLegendItem, + sharedStyle, + ); if (this.props.animated) { return ( @@ -152,8 +162,6 @@ export class AreaGeometries extends React.PureComponent< fill={color} listening={false} {...geometryStyle} - // areaCap="round" - // areaJoin="round" /> )} @@ -161,15 +169,7 @@ export class AreaGeometries extends React.PureComponent< ); } else { return ( - + ); } }); diff --git a/src/components/react_canvas/axis.tsx b/src/components/react_canvas/axis.tsx index e2233e8a03..c912b1d29e 100644 --- a/src/components/react_canvas/axis.tsx +++ b/src/components/react_canvas/axis.tsx @@ -1,9 +1,14 @@ import React from 'react'; import { Group, Line, Rect, Text } from 'react-konva'; import { - AxisTick, AxisTicksDimensions, centerRotationOrigin, - getHorizontalAxisTickLineProps, getTickLabelProps, - getVerticalAxisTickLineProps, isHorizontal, isVertical, + AxisTick, + AxisTicksDimensions, + centerRotationOrigin, + getHorizontalAxisTickLineProps, + getTickLabelProps, + getVerticalAxisTickLineProps, + isHorizontal, + isVertical, } from '../../lib/axes/axis_utils'; import { AxisSpec, Position } from '../../lib/series/specs'; import { Theme } from '../../lib/themes/theme'; @@ -24,15 +29,9 @@ export class Axis extends React.PureComponent { return this.renderAxis(); } renderTickLabel = (tick: AxisTick, i: number) => { + const { padding, ...labelStyle } = this.props.chartTheme.axes.tickLabelStyle; const { - axes: { tickFontFamily, tickFontSize, tickFontStyle }, - } = this.props.chartTheme; - const { - axisSpec: { - tickSize, - tickPadding, - position, - }, + axisSpec: { tickSize, tickPadding, position }, axisTicksDimensions, debug, } = this.props; @@ -49,7 +48,10 @@ export class Axis extends React.PureComponent { ); const { maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions; - const centeredRectProps = centerRotationOrigin(axisTicksDimensions, { x: tickLabelProps.x, y: tickLabelProps.y }); + const centeredRectProps = centerRotationOrigin(axisTicksDimensions, { + x: tickLabelProps.x, + y: tickLabelProps.y, + }); const textProps = { width: maxLabelTextWidth, @@ -62,14 +64,7 @@ export class Axis extends React.PureComponent { return ( {debug && } - + ); } @@ -78,23 +73,22 @@ export class Axis extends React.PureComponent { const { axisSpec: { tickSize, tickPadding, position }, axisTicksDimensions: { maxLabelBboxHeight }, + chartTheme: { + axes: { tickLineStyle }, + }, } = this.props; - const lineProps = isVertical(position) ? - getVerticalAxisTickLineProps( - position, - tickPadding, - tickSize, - tick.position, - ) : getHorizontalAxisTickLineProps( - position, - tickPadding, - tickSize, - tick.position, - maxLabelBboxHeight, - ); + const lineProps = isVertical(position) + ? getVerticalAxisTickLineProps(position, tickPadding, tickSize, tick.position) + : getHorizontalAxisTickLineProps( + position, + tickPadding, + tickSize, + tick.position, + maxLabelBboxHeight, + ); - return ; + return ; } private renderAxis = () => { const { ticks, axisPosition } = this.props; @@ -114,6 +108,9 @@ export class Axis extends React.PureComponent { axisSpec: { tickSize, tickPadding, position }, axisPosition, axisTicksDimensions, + chartTheme: { + axes: { axisLineStyle }, + }, } = this.props; const lineProps: number[] = []; if (isVertical(position)) { @@ -125,11 +122,15 @@ export class Axis extends React.PureComponent { lineProps[0] = 0; lineProps[2] = axisPosition.width; lineProps[1] = - position === Position.Top ? axisTicksDimensions.maxLabelBboxHeight + tickSize + tickPadding : 0; + position === Position.Top + ? axisTicksDimensions.maxLabelBboxHeight + tickSize + tickPadding + : 0; lineProps[3] = - position === Position.Top ? axisTicksDimensions.maxLabelBboxHeight + tickSize + tickPadding : 0; + position === Position.Top + ? axisTicksDimensions.maxLabelBboxHeight + tickSize + tickPadding + : 0; } - return ; + return ; } private renderAxisTitle() { const { @@ -149,18 +150,19 @@ export class Axis extends React.PureComponent { axisSpec: { title, position, tickSize, tickPadding }, axisTicksDimensions: { maxLabelBboxWidth }, chartTheme: { - axes: { titleFontFamily, titleFontSize, titleFontStyle, titlePadding }, + axes: { axisTitleStyle }, }, debug, } = this.props; if (!title) { return null; } + const { padding, ...titleStyle } = axisTitleStyle; const top = height; const left = position === Position.Left - ? -(maxLabelBboxWidth + titleFontSize + titlePadding) - : tickSize + tickPadding + maxLabelBboxWidth + titlePadding; + ? -(maxLabelBboxWidth + titleStyle.fontSize + padding) + : tickSize + tickPadding + maxLabelBboxWidth + padding; return ( @@ -169,7 +171,7 @@ export class Axis extends React.PureComponent { x={left} y={top} width={height} - height={titleFontSize} + height={titleStyle.fontSize} fill="violet" stroke="black" strokeWidth={1} @@ -181,12 +183,9 @@ export class Axis extends React.PureComponent { x={left} y={top} text={title} - fill="gray" width={height} rotation={-90} - fontFamily={titleFontFamily} - fontStyle={titleFontStyle} - fontSize={titleFontSize} + {...titleStyle} /> ); @@ -197,7 +196,9 @@ export class Axis extends React.PureComponent { axisSpec: { title, position, tickSize, tickPadding }, axisTicksDimensions: { maxLabelBboxHeight }, chartTheme: { - axes: { titleFontSize, titlePadding }, + axes: { + axisTitleStyle: { padding, ...titleStyle }, + }, }, debug, } = this.props; @@ -206,9 +207,10 @@ export class Axis extends React.PureComponent { return; } - const top = position === Position.Top ? - -maxLabelBboxHeight - titlePadding : - maxLabelBboxHeight + tickPadding + tickSize + titlePadding; + const top = + position === Position.Top + ? -maxLabelBboxHeight - padding + : maxLabelBboxHeight + tickPadding + tickSize + padding; const left = 0; return ( @@ -218,7 +220,7 @@ export class Axis extends React.PureComponent { x={left} y={top} width={width} - height={titleFontSize} + height={titleStyle.fontSize} stroke="black" strokeWidth={1} fill="violet" @@ -230,11 +232,8 @@ export class Axis extends React.PureComponent { y={top} width={width} height={height} - verticalAlign="middle" text={title} - fill="gray" - fontStyle="bold" - fontSize={titleFontSize} + {...titleStyle} /> ); diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index 66afadc51f..40c9eb2ac8 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -5,11 +5,14 @@ import { Group, Rect } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; import { BarGeometry, GeometryValue, getGeometryStyle } from '../../lib/series/rendering'; +import { BarSeriesStyle, SharedGeometryStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; interface BarGeometriesDataProps { animated?: boolean; bars: BarGeometry[]; + style: BarSeriesStyle; + sharedStyle: SharedGeometryStyle; onElementClick?: ElementClickListener; onElementOver: ((tooltip: TooltipData) => void) & IAction; onElementOut: (() => void) & IAction; @@ -21,7 +24,7 @@ interface BarGeometriesDataState { export class BarGeometries extends React.PureComponent< BarGeometriesDataProps, BarGeometriesDataState - > { +> { static defaultProps: Partial = { animated: false, }; @@ -75,6 +78,10 @@ export class BarGeometries extends React.PureComponent< private renderBarGeoms = (bars: BarGeometry[]): JSX.Element[] => { const { overBar } = this.state; + const { + style: { border }, + sharedStyle, + } = this.props; return bars.map((bar, i) => { const { x, y, width, height, color, value } = bar; @@ -86,8 +93,15 @@ export class BarGeometries extends React.PureComponent< hasHighlight, }; - const geometryStyle = getGeometryStyle(bar.geometryId, this.props.highlightedLegendItem, individualHighlight); + const geometryStyle = getGeometryStyle( + bar.geometryId, + this.props.highlightedLegendItem, + sharedStyle, + individualHighlight, + ); + // min 5px bars with white border + const borderEnabled = border.visible && width > border.strokeWidth * 7; if (this.props.animated) { return ( @@ -100,7 +114,9 @@ export class BarGeometries extends React.PureComponent< width={width} height={props.height} fill={color} - strokeWidth={0} + strokeWidth={border.strokeWidth} + stroke={border.stroke} + strokeEnabled={borderEnabled} perfectDrawEnabled={true} onMouseOver={this.onOverBar(bar)} onMouseLeave={this.onOutBar} @@ -120,7 +136,9 @@ export class BarGeometries extends React.PureComponent< width={width} height={height} fill={color} - strokeWidth={0} + strokeWidth={border.strokeWidth} + stroke={border.stroke} + strokeEnabled={borderEnabled} perfectDrawEnabled={false} onMouseOver={this.onOverBar(bar)} onMouseLeave={this.onOutBar} diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index 8058c6f1c9..9f408e29cd 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -4,14 +4,20 @@ import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { GeometryValue, getGeometryStyle, LineGeometry, PointGeometry } from '../../lib/series/rendering'; -import { LineSeriesStyle } from '../../lib/themes/theme'; +import { + GeometryValue, + getGeometryStyle, + LineGeometry, + PointGeometry, +} from '../../lib/series/rendering'; +import { LineSeriesStyle, SharedGeometryStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; interface LineGeometriesDataProps { animated?: boolean; lines: LineGeometry[]; style: LineSeriesStyle; + sharedStyle: SharedGeometryStyle; onElementClick?: ElementClickListener; onElementOver: ((tooltip: TooltipData) => void) & IAction; onElementOut: (() => void) & IAction; @@ -23,7 +29,7 @@ interface LineGeometriesDataState { export class LineGeometries extends React.PureComponent< LineGeometriesDataProps, LineGeometriesDataState - > { +> { static defaultProps: Partial = { animated: false, }; @@ -85,7 +91,7 @@ export class LineGeometries extends React.PureComponent< ); } private renderPoints = (points: PointGeometry[], i: number): JSX.Element[] => { - const { style } = this.props; + const { radius, stroke, strokeWidth } = this.props.style.point; const { overPoint } = this.state; return points.map((point, index) => { @@ -95,7 +101,7 @@ export class LineGeometries extends React.PureComponent< { - const { style, lines } = this.props; + const { style, lines, sharedStyle } = this.props; + const { strokeWidth } = style.line; return lines.map((glyph, i) => { const { line, color, transform, geometryId } = glyph; - const geometryStyle = getGeometryStyle(geometryId, this.props.highlightedLegendItem); + const geometryStyle = getGeometryStyle( + geometryId, + this.props.highlightedLegendItem, + sharedStyle, + ); if (this.props.animated) { return ( @@ -147,7 +158,7 @@ export class LineGeometries extends React.PureComponent< { const { geometries, canDataBeAnimated, + chartTheme, onOverElement, onOutElement, onElementClickListener, @@ -77,6 +78,8 @@ class Chart extends React.Component { { { { } renderGrids = () => { - const { - axesGridLinesPositions, - axesSpecs, - chartDimensions, - debug, - } = this.props.chartStore!; + const { axesGridLinesPositions, axesSpecs, chartDimensions, debug } = this.props.chartStore!; const gridComponents: JSX.Element[] = []; axesGridLinesPositions.forEach((axisGridLinesPositions, axisId) => { @@ -273,15 +273,15 @@ class Chart extends React.Component { const clippings = debug ? {} : { - clipX: 0, - clipY: 0, - clipWidth: [90, -90].includes(chartRotation) - ? chartDimensions.height - : chartDimensions.width, - clipHeight: [90, -90].includes(chartRotation) - ? chartDimensions.width - : chartDimensions.height, - }; + clipX: 0, + clipY: 0, + clipWidth: [90, -90].includes(chartRotation) + ? chartDimensions.height + : chartDimensions.width, + clipHeight: [90, -90].includes(chartRotation) + ? chartDimensions.width + : chartDimensions.height, + }; let brushProps = {}; const isBrushEnabled = this.props.chartStore!.isBrushEnabled(); @@ -320,7 +320,9 @@ class Chart extends React.Component { }} {...brushProps} > - {this.renderGrids()} + + {this.renderGrids()} + { className="euiSeriesChartAxis_tickLabel" key={`tick-${i}`} {...textProps} - // textAnchor={textProps.textAnchor} - // dominantBaseline={textProps.dominantBaseline} - // transform={transform} + // textAnchor={textProps.textAnchor} + // dominantBaseline={textProps.dominantBaseline} + // transform={transform} > {tick.label} @@ -124,16 +124,14 @@ export class Axis extends React.PureComponent { axisPosition: { height }, axisSpec: { title, position, tickSize, tickPadding }, axisTicksDimensions: { maxLabelBboxWidth }, - chartTheme: { - chart: { margins }, - }, + chartTheme: { chartMargins }, } = this.props; const top = height / 2; const left = position === Position.Left - ? -(maxLabelBboxWidth + margins.left / 2) - : tickSize + tickPadding + maxLabelBboxWidth + +margins.right / 2; + ? -(maxLabelBboxWidth + chartMargins.left / 2) + : tickSize + tickPadding + maxLabelBboxWidth + +chartMargins.right / 2; const translate = `translate(${left} ${top}) rotate(-90)`; return ( @@ -148,15 +146,13 @@ export class Axis extends React.PureComponent { axisPosition: { width }, axisSpec: { title, position, tickSize, tickPadding }, axisTicksDimensions: { maxLabelBboxHeight }, - chartTheme: { - chart: { margins }, - }, + chartTheme: { chartMargins }, } = this.props; const top = position === Position.Top - ? -margins.top / 2 - : maxLabelBboxHeight + tickPadding + tickSize + margins.bottom / 2; + ? -chartMargins.top / 2 + : maxLabelBboxHeight + tickPadding + tickSize + chartMargins.bottom / 2; const left = width / 2; const translate = `translate(${left} ${top} )`; return ( diff --git a/src/index.ts b/src/index.ts index b6f2440843..3dd8d6cbf2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,5 +2,10 @@ export * from './specs'; export { Chart } from './components/chart'; export { getAxisId, getGroupId, getSpecId } from './lib/utils/ids'; export { ScaleType } from './lib/utils/scales/scales'; - -export { Position, Rotation } from './lib/series/specs'; +export { Position, Rendering, Rotation } from './lib/series/specs'; +export * from './lib/themes/theme'; +export { LIGHT_THEME } from './lib/themes/light_theme'; +export { DARK_THEME } from './lib/themes/dark_theme'; +export { CurveType } from './lib/series/curves'; +export { niceTimeFormatter } from './utils/data/formatters'; +export { DataGenerator } from './utils/data_generators/data_generator'; diff --git a/src/lib/axes/axis_utils.test.ts b/src/lib/axes/axis_utils.test.ts index b0889c439d..57f178722d 100644 --- a/src/lib/axes/axis_utils.test.ts +++ b/src/lib/axes/axis_utils.test.ts @@ -1,7 +1,7 @@ import { XDomain } from '../series/domains/x_domain'; import { YDomain } from '../series/domains/y_domain'; import { Position } from '../series/specs'; -import { DEFAULT_THEME } from '../themes/theme'; +import { LIGHT_THEME } from '../themes/light_theme'; import { getAxisId, getGroupId } from '../utils/ids'; import { ScaleType } from '../utils/scales/scales'; import { @@ -94,7 +94,7 @@ describe('Axis computational utils', () => { isBandScale: false, }; - const { axes } = DEFAULT_THEME; + const { axes } = LIGHT_THEME; test('should compute axis dimensions', () => { const bboxCalculator = new SvgTextBBoxCalculator(); @@ -457,10 +457,7 @@ describe('Axis computational utils', () => { const chartWidth = 100; const chartHeight = 200; - const verticalAxisGridLinePositions = getVerticalAxisGridLineProps( - tickPosition, - chartWidth, - ); + const verticalAxisGridLinePositions = getVerticalAxisGridLineProps(tickPosition, chartWidth); expect(verticalAxisGridLinePositions).toEqual([0, 10, 100, 10]); diff --git a/src/lib/axes/axis_utils.ts b/src/lib/axes/axis_utils.ts index 5228db2dde..0d9c37aa6c 100644 --- a/src/lib/axes/axis_utils.ts +++ b/src/lib/axes/axis_utils.ts @@ -1,4 +1,3 @@ - import { XDomain } from '../series/domains/x_domain'; import { YDomain } from '../series/domains/y_domain'; import { computeXScale, computeYScales } from '../series/scales'; @@ -105,7 +104,7 @@ export function getScaleForAxisSpec( export function computeRotatedLabelDimensions(unrotatedDims: BBox, degreesRotation: number): BBox { const { width, height } = unrotatedDims; - const radians = degreesRotation * Math.PI / 180; + const radians = (degreesRotation * Math.PI) / 180; const rotatedHeight = Math.abs(width * Math.sin(radians)) + Math.abs(height * Math.cos(radians)); const rotatedWidth = Math.abs(width * Math.cos(radians)) + Math.abs(height * Math.sin(radians)); @@ -126,11 +125,18 @@ function computeTickDimensions( const tickValues = scale.ticks(); const tickLabels = tickValues.map(tickFormat); - const { tickFontSize, tickFontFamily } = axisConfig; + const { + tickLabelStyle: { fontFamily, fontSize }, + } = axisConfig; - const { maxLabelBboxWidth, maxLabelBboxHeight, maxLabelTextWidth, maxLabelTextHeight } = tickLabels - .reduce((acc: { [key: string]: number }, tickLabel: string) => { - const bbox = bboxCalculator.compute(tickLabel, tickFontSize, tickFontFamily).getOrElse({ + const { + maxLabelBboxWidth, + maxLabelBboxHeight, + maxLabelTextWidth, + maxLabelTextHeight, + } = tickLabels.reduce( + (acc: { [key: string]: number }, tickLabel: string) => { + const bbox = bboxCalculator.compute(tickLabel, fontSize, fontFamily).getOrElse({ width: 0, height: 0, }); @@ -153,7 +159,9 @@ function computeTickDimensions( maxLabelTextWidth: prevLabelWidth > labelWidth ? prevLabelWidth : labelWidth, maxLabelTextHeight: prevLabelHeight > labelHeight ? prevLabelHeight : labelHeight, }; - }, { maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 }); + }, + { maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 }, + ); return { tickValues, @@ -173,14 +181,19 @@ function computeTickDimensions( */ export function centerRotationOrigin( axisTicksDimensions: { - maxLabelBboxWidth: number, - maxLabelBboxHeight: number, - maxLabelTextWidth: number, - maxLabelTextHeight: number, + maxLabelBboxWidth: number; + maxLabelBboxHeight: number; + maxLabelTextWidth: number; + maxLabelTextHeight: number; }, - coordinates: { x: number, y: number }): { x: number, y: number, offsetX: number, offsetY: number } { - - const { maxLabelBboxWidth, maxLabelBboxHeight, maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions; + coordinates: { x: number; y: number }, +): { x: number; y: number; offsetX: number; offsetY: number } { + const { + maxLabelBboxWidth, + maxLabelBboxHeight, + maxLabelTextWidth, + maxLabelTextHeight, + } = axisTicksDimensions; const offsetX = maxLabelTextWidth / 2; const offsetY = maxLabelTextHeight / 2; @@ -222,7 +235,7 @@ export function getTickLabelProps( } return { - x: isAxisLeft ? - (maxLabelBboxWidth) : tickSize + tickPadding, + x: isAxisLeft ? -maxLabelBboxWidth : tickSize + tickPadding, y: tickPosition - maxLabelBboxHeight / 2, align, verticalAlign, @@ -369,7 +382,9 @@ export function getVisibleTicks( const { showOverlappingTicks, showOverlappingLabels } = axisSpec; const { maxLabelBboxHeight, maxLabelBboxWidth } = axisDim; const { width, height } = chartDimensions; - const requiredSpace = isVertical(axisSpec.position) ? maxLabelBboxHeight / 2 : maxLabelBboxWidth / 2; + const requiredSpace = isVertical(axisSpec.position) + ? maxLabelBboxHeight / 2 + : maxLabelBboxWidth / 2; let previousOccupiedSpace = 0; const visibleTicks = []; @@ -438,19 +453,23 @@ export function getAxisPosition( if (isVertical(position)) { if (position === Position.Left) { - leftIncrement = maxLabelBboxWidth + tickSize + tickPadding + chartMargins.left + axisTitleHeight; + leftIncrement = + maxLabelBboxWidth + tickSize + tickPadding + chartMargins.left + axisTitleHeight; dimensions.left = maxLabelBboxWidth + cumLeftSum + chartMargins.left + axisTitleHeight; } else { - rightIncrement = maxLabelBboxWidth + tickSize + tickPadding + chartMargins.right + axisTitleHeight; + rightIncrement = + maxLabelBboxWidth + tickSize + tickPadding + chartMargins.right + axisTitleHeight; dimensions.left = left + width + cumRightSum; } dimensions.width = maxLabelBboxWidth; } else { if (position === Position.Top) { - topIncrement = maxLabelBboxHeight + tickSize + tickPadding + chartMargins.top + axisTitleHeight; + topIncrement = + maxLabelBboxHeight + tickSize + tickPadding + chartMargins.top + axisTitleHeight; dimensions.top = cumTopSum + chartMargins.top + axisTitleHeight; } else { - bottomIncrement = maxLabelBboxHeight + tickSize + tickPadding + chartMargins.bottom + axisTitleHeight; + bottomIncrement = + maxLabelBboxHeight + tickSize + tickPadding + chartMargins.bottom + axisTitleHeight; dimensions.top = top + height + cumBottomSum; } dimensions.height = maxLabelBboxHeight; @@ -471,7 +490,7 @@ export function getAxisTicksPositions( totalGroupsCount: number, legendPosition?: Position, ) { - const chartConfig = chartTheme.chart; + const { chartPaddings, chartMargins } = chartTheme; const legendStyle = chartTheme.legend; const axisPositions: Map = new Map(); const axisVisibleTicks: Map = new Map(); @@ -479,9 +498,9 @@ export function getAxisTicksPositions( const axisGridLinesPositions: Map = new Map(); let cumTopSum = 0; - let cumBottomSum = chartConfig.paddings.bottom; + let cumBottomSum = chartPaddings.bottom; let cumLeftSum = 0; - let cumRightSum = chartConfig.paddings.right; + let cumRightSum = chartPaddings.right; if (showLegend) { switch (legendPosition) { case Position.Left: @@ -538,18 +557,20 @@ export function getAxisTicksPositions( if (axisSpec.showGridLines) { const isVerticalAxis = isVertical(axisSpec.position); - const gridLines = visibleTicks.map((tick: AxisTick): AxisLinePosition => { - return computeAxisGridLinePositions(isVerticalAxis, tick.position, chartDimensions); - }); + const gridLines = visibleTicks.map( + (tick: AxisTick): AxisLinePosition => { + return computeAxisGridLinePositions(isVerticalAxis, tick.position, chartDimensions); + }, + ); axisGridLinesPositions.set(id, gridLines); } - const { titleFontSize, titlePadding } = chartTheme.axes; - const axisTitleHeight = titleFontSize + titlePadding; + const { fontSize, padding } = chartTheme.axes.axisTitleStyle; + const axisTitleHeight = (fontSize || 10) + (padding || 0); const axisPosition = getAxisPosition( chartDimensions, - chartConfig.margins, + chartMargins, axisTitleHeight, axisSpec, axisDim, @@ -580,14 +601,9 @@ function computeAxisGridLinePositions( tickPosition: number, chartDimensions: Dimensions, ): AxisLinePosition { - const positions = isVerticalAxis ? - getVerticalAxisGridLineProps( - tickPosition, - chartDimensions.width, - ) : getHorizontalAxisGridLineProps( - tickPosition, - chartDimensions.height, - ); + const positions = isVerticalAxis + ? getVerticalAxisGridLineProps(tickPosition, chartDimensions.width) + : getHorizontalAxisGridLineProps(tickPosition, chartDimensions.height); return positions; } diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index 2967fb132b..0b231f712a 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -1,5 +1,5 @@ import { area, line } from 'd3-shape'; -import { DEFAULT_THEME } from '../themes/theme'; +import { SharedGeometryStyle } from '../themes/theme'; import { SpecId } from '../utils/ids'; import { Scale, ScaleType } from '../utils/scales/scales'; import { CurveType, getCurveFactory } from './curves'; @@ -198,23 +198,22 @@ export function renderArea( export function getGeometryStyle( geometryId: GeometryId, highlightedLegendItem: LegendItem | null, + sharedStyle: SharedGeometryStyle, individualHighlight?: { [key: string]: boolean }, ): GeometryStyle { - const { shared } = DEFAULT_THEME.chart.styles; - if (highlightedLegendItem != null) { const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); - return isPartOfHighlightedSeries ? shared.highlighted : shared.unhighlighted; + return isPartOfHighlightedSeries ? sharedStyle.highlighted : sharedStyle.unhighlighted; } if (individualHighlight) { const { hasHighlight, hasGeometryHover } = individualHighlight; if (!hasGeometryHover) { - return shared.highlighted; + return sharedStyle.highlighted; } - return hasHighlight ? shared.highlighted : shared.unhighlighted; + return hasHighlight ? sharedStyle.highlighted : sharedStyle.unhighlighted; } - return shared.default; + return sharedStyle.default; } diff --git a/src/lib/themes/dark_theme.ts b/src/lib/themes/dark_theme.ts new file mode 100644 index 0000000000..b24d05e9dc --- /dev/null +++ b/src/lib/themes/dark_theme.ts @@ -0,0 +1,99 @@ +import { palettes } from '@elastic/eui'; +import { Theme } from './theme'; + +import { + DEFAULT_CHART_MARGINS, + DEFAULT_CHART_PADDING, + DEFAULT_GEOMETRY_STYLES, + DEFAULT_MISSING_COLOR, +} from './theme_commons'; + +export const DARK_THEME: Theme = { + chartPaddings: DEFAULT_CHART_PADDING, + chartMargins: DEFAULT_CHART_MARGINS, + lineSeriesStyle: { + line: { + stroke: DEFAULT_MISSING_COLOR, + strokeWidth: 1, + visible: true, + }, + border: { + stroke: 'white', + strokeWidth: 2, + visible: false, + }, + point: { + visible: false, + radius: 5, + stroke: 'white', + strokeWidth: 1, + }, + }, + areaSeriesStyle: { + area: { + fill: DEFAULT_MISSING_COLOR, + visible: true, + }, + line: { + stroke: DEFAULT_MISSING_COLOR, + strokeWidth: 1, + visible: true, + }, + border: { + stroke: 'white', + strokeWidth: 2, + visible: false, + }, + point: { + visible: false, + radius: 4, + stroke: 'white', + strokeWidth: 1, + }, + }, + barSeriesStyle: { + border: { + stroke: 'white', + strokeWidth: 2, + visible: false, + }, + }, + sharedStyle: DEFAULT_GEOMETRY_STYLES, + scales: { + ordinal: { + padding: 0.25, + }, + }, + axes: { + axisTitleStyle: { + fontSize: 12, + fontStyle: 'bold', + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + padding: 5, + fill: 'white', + }, + axisLineStyle: { + stroke: 'white', + strokeWidth: 1, + }, + tickLabelStyle: { + fontSize: 10, + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + fontStyle: 'normal', + fill: 'white', + padding: 0, + }, + tickLineStyle: { + stroke: 'white', + strokeWidth: 1, + }, + }, + colors: { + vizColors: palettes.euiPaletteColorBlind.colors, + defaultVizColor: DEFAULT_MISSING_COLOR, + }, + legend: { + verticalWidth: 150, + horizontalHeight: 50, + }, +}; diff --git a/src/lib/themes/light_theme.ts b/src/lib/themes/light_theme.ts new file mode 100644 index 0000000000..bb0c523536 --- /dev/null +++ b/src/lib/themes/light_theme.ts @@ -0,0 +1,99 @@ +import { palettes } from '@elastic/eui'; +import { Theme } from './theme'; + +import { + DEFAULT_CHART_MARGINS, + DEFAULT_CHART_PADDING, + DEFAULT_GEOMETRY_STYLES, + DEFAULT_MISSING_COLOR, +} from './theme_commons'; + +export const LIGHT_THEME: Theme = { + chartPaddings: DEFAULT_CHART_PADDING, + chartMargins: DEFAULT_CHART_MARGINS, + lineSeriesStyle: { + line: { + stroke: DEFAULT_MISSING_COLOR, + strokeWidth: 1, + visible: true, + }, + border: { + stroke: 'gray', + strokeWidth: 2, + visible: false, + }, + point: { + visible: false, + radius: 5, + stroke: 'white', + strokeWidth: 1, + }, + }, + areaSeriesStyle: { + area: { + fill: DEFAULT_MISSING_COLOR, + visible: true, + }, + line: { + stroke: DEFAULT_MISSING_COLOR, + strokeWidth: 1, + visible: true, + }, + border: { + stroke: 'gray', + strokeWidth: 2, + visible: false, + }, + point: { + visible: false, + radius: 5, + stroke: 'white', + strokeWidth: 1, + }, + }, + barSeriesStyle: { + border: { + stroke: 'white', + strokeWidth: 1, + visible: false, + }, + }, + sharedStyle: DEFAULT_GEOMETRY_STYLES, + scales: { + ordinal: { + padding: 0.25, + }, + }, + axes: { + axisTitleStyle: { + fontSize: 12, + fontStyle: 'bold', + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + padding: 5, + fill: 'gray', + }, + axisLineStyle: { + stroke: 'gray', + strokeWidth: 1, + }, + tickLabelStyle: { + fontSize: 10, + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + fontStyle: 'normal', + fill: 'gray', + padding: 0, + }, + tickLineStyle: { + stroke: 'gray', + strokeWidth: 1, + }, + }, + colors: { + vizColors: palettes.euiPaletteColorBlind.colors, + defaultVizColor: DEFAULT_MISSING_COLOR, + }, + legend: { + verticalWidth: 150, + horizontalHeight: 50, + }, +}; diff --git a/src/lib/themes/theme.ts b/src/lib/themes/theme.ts index 34bad0574c..56cbc918f8 100644 --- a/src/lib/themes/theme.ts +++ b/src/lib/themes/theme.ts @@ -1,28 +1,44 @@ import { GeometryStyle } from '../series/rendering'; import { Margins } from '../utils/dimensions'; +import { LIGHT_THEME } from './light_theme'; -export interface ChartConfig { - /* Space btw parent DOM element and first available element of the chart (axis - * if exists, else the chart itself) - */ - margins: Margins; +interface Visible { + visible: boolean; +} +interface Radius { + radius: number; +} +export interface TextStyle { + fontSize: number; + fontFamily: string; + fontStyle?: string; + fill: string; + padding: number; +} +export interface GeometryStyle { + stroke: string; + strokeWidth: number; + fill?: string; + opacity?: number; +} - /* Space btw the chart geometries and axis; if no axis, pads space btw chart & container */ - paddings: Margins; - styles: { - lineSeries: LineSeriesStyle; - areaSeries: AreaSeriesStyle; - shared: { [key: string]: GeometryStyle }; - }; +export interface SharedGeometryStyle { + [key: string]: GeometryStyle; +} + +export interface StrokeStyle { + stroke: string; + strokeWidth: number; +} +export interface FillStyle { + fill: string; } + export interface AxisConfig { - tickFontSize: number; - tickFontFamily: string; - tickFontStyle: string; - titleFontSize: number; - titleFontFamily: string; - titleFontStyle: string; - titlePadding: number; + axisTitleStyle: TextStyle; + axisLineStyle: StrokeStyle; + tickLabelStyle: TextStyle; + tickLineStyle: StrokeStyle; } export interface GridLineConfig { stroke?: string; @@ -39,51 +55,52 @@ export interface ColorConfig { vizColors: string[]; defaultVizColor: string; } -export interface InteractionConfig { - dimmingOpacity: number; -} export interface LegendStyle { verticalWidth: number; horizontalHeight: number; } export interface Theme { - chart: ChartConfig; + /** + * Space btw parent DOM element and first available element of the chart (axis if exists, else the chart itself) + */ + chartMargins: Margins; + /** + * Space btw the chart geometries and axis; if no axis, pads space btw chart & container + */ + chartPaddings: Margins; + lineSeriesStyle: LineSeriesStyle; + areaSeriesStyle: AreaSeriesStyle; + barSeriesStyle: BarSeriesStyle; + sharedStyle: SharedGeometryStyle; axes: AxisConfig; scales: ScalesConfig; colors: ColorConfig; - interactions: InteractionConfig; legend: LegendStyle; } +export interface BarSeriesStyle { + border: StrokeStyle & Visible; +} export interface LineSeriesStyle { - hideLine: boolean; - lineWidth: number; - hideBorder: boolean; - borderStrokeColor: string; - borderWidth: number; - hideDataPoints: boolean; - dataPointsRadius: number; - dataPointsStroke: string; - dataPointsStrokeWidth: number; + line: StrokeStyle & Visible; + border: StrokeStyle & Visible; + point: StrokeStyle & Visible & Radius; } export interface AreaSeriesStyle { - hideArea: boolean; - hideLine: boolean; - lineStrokeColor: string; - lineWidth: number; - hideBorder: boolean; - borderStrokeColor: string; - borderWidth: number; - hideDataPoints: boolean; - dataPointsRadius: number; - dataPointsStroke: string; - dataPointsStrokeWidth: number; + area: FillStyle & Visible; + line: StrokeStyle & Visible; + border: StrokeStyle & Visible; + point: StrokeStyle & Visible & Radius; } export interface PartialTheme { - chart?: Partial; + chartMargins?: Margins; + chartPaddings?: Margins; + lineSeriesStyle?: LineSeriesStyle; + areaSeriesStyle?: AreaSeriesStyle; + barSeriesStyle?: BarSeriesStyle; + sharedStyle?: SharedGeometryStyle; axes?: Partial; scales?: Partial; colors?: Partial; - interactions?: Partial; legend?: Partial; } @@ -93,181 +110,74 @@ export const DEFAULT_GRID_LINE_CONFIG: GridLineConfig = { opacity: 1, }; -export const GEOMETRY_STYLES: { [key: string]: GeometryStyle } = { - default: { - opacity: 1, - }, - highlighted: { - opacity: 1, - }, - unhighlighted: { - opacity: 0.25, - }, -}; - -export const DEFAULT_THEME: Theme = { - chart: { - paddings: { - left: 5, - right: 5, - top: 5, - bottom: 5, - }, - margins: { - left: 30, - right: 30, - top: 30, - bottom: 30, - }, - styles: { - lineSeries: { - hideLine: false, - lineWidth: 1, - hideBorder: true, - borderWidth: 2, - borderStrokeColor: 'gray', - hideDataPoints: true, - dataPointsRadius: 5, - dataPointsStroke: 'white', - dataPointsStrokeWidth: 1, - }, - areaSeries: { - hideArea: false, - hideLine: true, - lineWidth: 1, - lineStrokeColor: 'white', - hideBorder: true, - borderWidth: 2, - borderStrokeColor: 'gray', - hideDataPoints: true, - dataPointsRadius: 4, - dataPointsStroke: 'white', - dataPointsStrokeWidth: 1, - }, - shared: GEOMETRY_STYLES, - }, - }, - scales: { - ordinal: { - padding: 0.25, - }, - }, - axes: { - tickFontSize: 10, - tickFontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, - tickFontStyle: 'normal', - titleFontSize: 12, - titleFontStyle: 'bold', - titleFontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, - titlePadding: 5, - }, - colors: { - vizColors: [ - '#00B3A4', - '#3185FC', - '#DB1374', - '#490092', - '#FEB6DB', - '#E6C220', - '#F98510', - '#BFA180', - '#461A0A', - '#920000', - ], - defaultVizColor: 'red', - }, - interactions: { - dimmingOpacity: 0.1, - }, - legend: { - verticalWidth: 150, - horizontalHeight: 50, - }, -}; - -export function mergeWithDefaultTheme(theme: PartialTheme): Theme { - const chart: ChartConfig = { - ...DEFAULT_THEME.chart, +export function mergeWithDefaultTheme( + theme: PartialTheme, + defaultTheme: Theme = LIGHT_THEME, +): Theme { + const customTheme: Theme = { + ...defaultTheme, }; - if (theme.chart) { - chart.margins = { - ...DEFAULT_THEME.chart.margins, - ...theme.chart.margins, + if (theme.chartMargins) { + customTheme.chartMargins = { + ...defaultTheme.chartMargins, + ...theme.chartMargins, }; - chart.paddings = { - ...DEFAULT_THEME.chart.paddings, - ...theme.chart.paddings, + } + if (theme.chartPaddings) { + customTheme.chartPaddings = { + ...defaultTheme.chartPaddings, + ...theme.chartPaddings, + }; + } + if (theme.areaSeriesStyle) { + customTheme.areaSeriesStyle = { + ...defaultTheme.areaSeriesStyle, + ...theme.areaSeriesStyle, + }; + } + if (theme.lineSeriesStyle) { + customTheme.lineSeriesStyle = { + ...defaultTheme.lineSeriesStyle, + ...theme.lineSeriesStyle, + }; + } + if (theme.barSeriesStyle) { + customTheme.barSeriesStyle = { + ...defaultTheme.barSeriesStyle, + ...theme.barSeriesStyle, + }; + } + if (theme.sharedStyle) { + customTheme.sharedStyle = { + ...defaultTheme.sharedStyle, + ...theme.sharedStyle, }; - if (theme.chart.styles) { - if (theme.chart.styles.areaSeries) { - chart.styles.areaSeries = { - ...DEFAULT_THEME.chart.styles.areaSeries, - ...theme.chart.styles.areaSeries, - }; - } - if (theme.chart.styles.lineSeries) { - chart.styles.lineSeries = { - ...DEFAULT_THEME.chart.styles.lineSeries, - ...theme.chart.styles.lineSeries, - }; - } - } } - const scales: ScalesConfig = { - ...DEFAULT_THEME.scales, - }; if (theme.scales) { - scales.ordinal = { - ...DEFAULT_THEME.scales.ordinal, + customTheme.scales.ordinal = { + ...defaultTheme.scales.ordinal, ...theme.scales.ordinal, }; } - let axes: AxisConfig = { - ...DEFAULT_THEME.axes, - }; if (theme.axes) { - axes = { - ...DEFAULT_THEME.axes, + customTheme.axes = { + ...defaultTheme.axes, ...theme.axes, }; } - const colors: ColorConfig = { - ...DEFAULT_THEME.colors, - }; if (theme.colors) { if (theme.colors.defaultVizColor) { - colors.defaultVizColor = theme.colors.defaultVizColor; + customTheme.colors.defaultVizColor = theme.colors.defaultVizColor; } if (theme.colors.vizColors) { - colors.vizColors = theme.colors.vizColors; + customTheme.colors.vizColors = theme.colors.vizColors; } } - - let interactions: InteractionConfig = { - ...DEFAULT_THEME.interactions, - }; - if (theme.interactions) { - interactions = { - ...DEFAULT_THEME.interactions, - ...theme.interactions, - }; - } - - let legend: LegendStyle = { - ...DEFAULT_THEME.legend, - }; if (theme.legend) { - legend = { - ...DEFAULT_THEME.legend, + customTheme.legend = { + ...defaultTheme.legend, ...theme.legend, }; } - return { - chart, - scales, - axes, - colors, - interactions, - legend, - }; + return customTheme; } diff --git a/src/lib/themes/theme_commons.ts b/src/lib/themes/theme_commons.ts new file mode 100644 index 0000000000..8638ec4003 --- /dev/null +++ b/src/lib/themes/theme_commons.ts @@ -0,0 +1,29 @@ +import { Margins } from '../utils/dimensions'; +import { SharedGeometryStyle } from './theme'; + +export const DEFAULT_MISSING_COLOR = 'red'; + +export const DEFAULT_CHART_PADDING: Margins = { + left: 0, + right: 0, + top: 0, + bottom: 0, +}; +export const DEFAULT_CHART_MARGINS: Margins = { + left: 10, + right: 10, + top: 10, + bottom: 10, +}; + +export const DEFAULT_GEOMETRY_STYLES: SharedGeometryStyle = { + default: { + opacity: 1, + }, + highlighted: { + opacity: 1, + }, + unhighlighted: { + opacity: 0.25, + }, +}; diff --git a/src/lib/utils/dimensions.test.ts b/src/lib/utils/dimensions.test.ts index 6827a45b5c..137f80ca42 100644 --- a/src/lib/utils/dimensions.test.ts +++ b/src/lib/utils/dimensions.test.ts @@ -1,6 +1,7 @@ import { AxisTicksDimensions } from '../axes/axis_utils'; import { AxisSpec, Position } from '../series/specs'; -import { DEFAULT_THEME, LegendStyle } from '../themes/theme'; +import { LIGHT_THEME } from '../themes/light_theme'; +import { LegendStyle } from '../themes/theme'; import { computeChartDimensions, Margins } from './dimensions'; import { AxisId, getAxisId, getGroupId } from './ids'; import { ScaleType } from './scales/scales'; @@ -53,21 +54,18 @@ describe('Computed chart dimensions', () => { horizontalHeight: 10, }; const showLegend = false; - + const defaultTheme = LIGHT_THEME; const chartTheme = { - ...DEFAULT_THEME, - chart: { - ...DEFAULT_THEME.chart, - margins: chartMargins, - paddings: chartPaddings, - }, + ...defaultTheme, + chartMargins, + chartPaddings, axes: { - ...DEFAULT_THEME.axes, - titleFontSize: 10, - titlePadding: 10, + ...defaultTheme.axes, }, ...legend, }; + chartTheme.axes.axisTitleStyle.fontSize = 10; + chartTheme.axes.axisTitleStyle.padding = 10; test('should be equal to parent dimension with no axis minus margins', () => { const axisDims = new Map(); const axisSpecs = new Map(); diff --git a/src/lib/utils/dimensions.ts b/src/lib/utils/dimensions.ts index 992fc22d67..e73bc2383e 100644 --- a/src/lib/utils/dimensions.ts +++ b/src/lib/utils/dimensions.ts @@ -31,12 +31,11 @@ export function computeChartDimensions( showLegend: boolean, legendPosition?: Position, ): Dimensions { - const chartMargins = chartTheme.chart.margins; - const chartPaddings = chartTheme.chart.paddings; + const { chartMargins, chartPaddings } = chartTheme; const legendStyle = chartTheme.legend; - const { titleFontSize, titlePadding } = chartTheme.axes; + const { axisTitleStyle } = chartTheme.axes; - const axisTitleHeight = titleFontSize + titlePadding; + const axisTitleHeight = axisTitleStyle.fontSize + axisTitleStyle.padding; let vLeftAxisSpecWidth = 0; let vRightAxisSpecWidth = 0; diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 19bc95098e..a4411d28f9 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -1,7 +1,8 @@ import { inject } from 'mobx-react'; import { PureComponent } from 'react'; import { Position, Rendering, Rotation } from '../lib/series/specs'; -import { DEFAULT_THEME, mergeWithDefaultTheme, PartialTheme } from '../lib/themes/theme'; +import { LIGHT_THEME } from '../lib/themes/light_theme'; +import { Theme } from '../lib/themes/theme'; import { BrushEndListener, ChartStore, @@ -12,7 +13,7 @@ import { interface SettingSpecProps { chartStore?: ChartStore; - theme?: PartialTheme; + theme?: Theme; rendering: Rendering; rotation: Rotation; animateData: boolean; @@ -47,7 +48,7 @@ function updateChartStore(props: SettingSpecProps) { if (!chartStore) { return; } - chartStore.chartTheme = theme ? mergeWithDefaultTheme(theme) : DEFAULT_THEME; + chartStore.chartTheme = theme || LIGHT_THEME; chartStore.chartRotation = rotation; chartStore.chartRendering = rendering; chartStore.animateData = animateData; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 3cb563f377..0380e82f8b 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -35,7 +35,8 @@ import { Rotation, } from '../lib/series/specs'; import { formatTooltip } from '../lib/series/tooltip'; -import { DEFAULT_THEME, Theme } from '../lib/themes/theme'; +import { LIGHT_THEME } from '../lib/themes/light_theme'; +import { Theme } from '../lib/themes/theme'; import { computeChartDimensions, Dimensions } from '../lib/utils/dimensions'; import { AxisId, GroupId, SpecId } from '../lib/utils/ids'; import { Scale, ScaleType } from '../lib/utils/scales/scales'; @@ -109,7 +110,7 @@ export class ChartStore { chartRotation: Rotation = 0; // updated from jsx chartRendering: Rendering = 'canvas'; // updated from jsx - chartTheme: Theme = DEFAULT_THEME; // updated from jsx + chartTheme: Theme = LIGHT_THEME; // updated from jsx axesSpecs: Map = new Map(); // readed from jsx axesTicksDimensions: Map = new Map(); // computed axesPositions: Map = new Map(); // computed diff --git a/stories/axis.tsx b/stories/axis.tsx index 4422f6d892..a2199e49f2 100644 --- a/stories/axis.tsx +++ b/stories/axis.tsx @@ -6,16 +6,18 @@ import { Axis, BarSeries, Chart, + DataGenerator, getAxisId, getGroupId, getSpecId, + LIGHT_THEME, + LineSeries, + mergeWithDefaultTheme, + PartialTheme, Position, ScaleType, Settings, } from '../src/'; -import { PartialTheme } from '../src/lib/themes/theme'; -import { LineSeries } from '../src/specs'; -import { DataGenerator } from '../src/utils/data_generators/data_generator'; function createThemeAction(title: string, min: number, max: number, value: number) { return number( @@ -186,27 +188,26 @@ storiesOf('Axis', module) }) .add('with multi axis', () => { const theme: PartialTheme = { - chart: { - margins: { - left: createThemeAction('margin left', 0, 50, 0), - right: createThemeAction('margin right', 0, 50, 0), - top: createThemeAction('margin top', 0, 50, 0), - bottom: createThemeAction('margin bottom', 0, 50, 0), - }, - paddings: { - left: createThemeAction('padding left', 0, 50, 0), - right: createThemeAction('padding right', 0, 50, 0), - top: createThemeAction('padding top', 0, 50, 0), - bottom: createThemeAction('padding bottom', 0, 50, 0), - }, + chartMargins: { + left: createThemeAction('margin left', 0, 50, 0), + right: createThemeAction('margin right', 0, 50, 0), + top: createThemeAction('margin top', 0, 50, 0), + bottom: createThemeAction('margin bottom', 0, 50, 0), + }, + chartPaddings: { + left: createThemeAction('padding left', 0, 50, 0), + right: createThemeAction('padding right', 0, 50, 0), + top: createThemeAction('padding top', 0, 50, 0), + bottom: createThemeAction('padding bottom', 0, 50, 0), }, }; + const customTheme = mergeWithDefaultTheme(theme, LIGHT_THEME); const seriesGroup1 = 'group1'; const seriesGroup2 = 'group2'; return ( - + {renderAxisWithOptions(Position.Top, seriesGroup1, false)} {renderAxisWithOptions(Position.Top, seriesGroup2, true)} {renderAxisWithOptions(Position.Left, seriesGroup1, false)} diff --git a/stories/bar_chart.tsx b/stories/bar_chart.tsx index 76d6b3bfba..be5af27000 100644 --- a/stories/bar_chart.tsx +++ b/stories/bar_chart.tsx @@ -5,20 +5,24 @@ import { Axis, BarSeries, Chart, + DARK_THEME, + DataGenerator, getAxisId, getSpecId, + LIGHT_THEME, + niceTimeFormatter, Position, ScaleType, Settings, } from '../src/'; import * as TestDatasets from '../src/lib/series/utils/test_dataset'; -import { niceTimeFormatter } from '../src/utils/data/formatters'; -import { DataGenerator } from '../src/utils/data_generators/data_generator'; storiesOf('Bar Chart', module) .add('basic', () => { + const darkmode = boolean('darkmode', false); + const className = darkmode ? 'story-chart-dark' : 'story-chart'; return ( - + { + const darkmode = boolean('darkmode', false); + const className = darkmode ? 'story-chart-dark' : 'story-chart'; + const defaultTheme = darkmode ? DARK_THEME : LIGHT_THEME; return ( - + + { diff --git a/stories/mixed.tsx b/stories/mixed.tsx index f523c70be1..bca1bc7707 100644 --- a/stories/mixed.tsx +++ b/stories/mixed.tsx @@ -1,16 +1,17 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { + AreaSeries, Axis, BarSeries, Chart, getAxisId, getSpecId, + LineSeries, Position, ScaleType, Settings, } from '../src/'; -import { AreaSeries, LineSeries } from '../src/specs'; storiesOf('Mixed Charts', module) .add('bar and lines', () => { diff --git a/stories/styling.tsx b/stories/styling.tsx index 247658b5cb..17e8cdad82 100644 --- a/stories/styling.tsx +++ b/stories/styling.tsx @@ -1,49 +1,74 @@ -import { boolean, number, select } from '@storybook/addon-knobs'; +import { palettes } from '@elastic/eui'; +import { boolean, color, number, select } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import React from 'react'; +import { switchTheme } from '../.storybook/theme_service'; import { + AreaSeries, Axis, BarSeries, Chart, + CurveType, + DARK_THEME, + DataGenerator, getAxisId, getSpecId, + LIGHT_THEME, + LineSeries, + mergeWithDefaultTheme, + PartialTheme, Position, ScaleType, Settings, } from '../src/'; -import { PartialTheme } from '../src/lib/themes/theme'; +import { DEFAULT_MISSING_COLOR } from '../src/lib/themes/theme_commons'; -function createThemeAction(title: string, min: number, max: number, value: number) { - return number(title, value, { - range: true, - min, - max, - step: 1, - }); +function range( + title: string, + min: number, + max: number, + value: number, + groupId?: string, + step: number = 1, +) { + return number( + title, + value, + { + range: true, + min, + max, + step, + }, + groupId, + ); } storiesOf('Stylings', module) .add('margins and paddings', () => { const theme: PartialTheme = { - chart: { - margins: { - left: createThemeAction('margin left', 0, 50, 10), - right: createThemeAction('margin right', 0, 50, 10), - top: createThemeAction('margin top', 0, 50, 10), - bottom: createThemeAction('margin bottom', 0, 50, 10), - }, - paddings: { - left: createThemeAction('padding left', 0, 50, 10), - right: createThemeAction('padding right', 0, 50, 10), - top: createThemeAction('padding top', 0, 50, 10), - bottom: createThemeAction('padding bottom', 0, 50, 10), - }, + chartMargins: { + left: range('margin left', 0, 50, 10), + right: range('margin right', 0, 50, 10), + top: range('margin top', 0, 50, 10), + bottom: range('margin bottom', 0, 50, 10), + }, + chartPaddings: { + left: range('padding left', 0, 50, 10), + right: range('padding right', 0, 50, 10), + top: range('padding top', 0, 50, 10), + bottom: range('padding bottom', 0, 50, 10), }, }; - + const customTheme = mergeWithDefaultTheme(theme, LIGHT_THEME); return ( - + ); }) - .add('axis (TOFIX)', () => { + .add('axis', () => { const theme: PartialTheme = { axes: { - tickFontSize: createThemeAction('tickFontSize', 0, 40, 10), - tickFontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, - tickFontStyle: 'normal', - titleFontSize: createThemeAction('titleFontSize', 0, 40, 12), - titleFontStyle: 'bold', - titleFontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, - titlePadding: createThemeAction('titlePadding', 0, 40, 5), + axisTitleStyle: { + fill: color('titleFill', '#333', 'Axis Title'), + fontSize: range('titleFontSize', 0, 40, 12, 'Axis Title'), + fontStyle: 'bold', + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + padding: range('titlePadding', 0, 40, 5, 'Axis Title'), + }, + axisLineStyle: { + stroke: color('axisLinecolor', '#333', 'Axis Line'), + strokeWidth: range('axisLineWidth', 0, 5, 1, 'Axis Line'), + }, + tickLabelStyle: { + fill: color('tickFill', '#333', 'Tick Label'), + fontSize: range('tickFontSize', 0, 40, 10, 'Tick Label'), + fontFamily: `'Open Sans', Helvetica, Arial, sans-serif`, + fontStyle: 'normal', + padding: 0, + }, + tickLineStyle: { + stroke: color('tickLineColor', '#333', 'Tick Line'), + strokeWidth: range('tickLineWidth', 0, 5, 1, 'Tick Line'), + }, }, }; + const customTheme = mergeWithDefaultTheme(theme, LIGHT_THEME); return ( @@ -127,4 +168,184 @@ storiesOf('Stylings', module) /> ); + }) + .add('theme/style', () => { + const theme: PartialTheme = { + chartMargins: { + left: range('margin left', 0, 50, 10, 'Margins'), + right: range('margin right', 0, 50, 10, 'Margins'), + top: range('margin top', 0, 50, 10, 'Margins'), + bottom: range('margin bottom', 0, 50, 10, 'Margins'), + }, + chartPaddings: { + left: range('padding left', 0, 50, 10, 'Paddings'), + right: range('padding right', 0, 50, 10, 'Paddings'), + top: range('padding top', 0, 50, 10, 'Paddings'), + bottom: range('padding bottom', 0, 50, 10, 'Paddings'), + }, + lineSeriesStyle: { + line: { + stroke: DEFAULT_MISSING_COLOR, + strokeWidth: range('lStrokeWidth', 0, 10, 1, 'line'), + visible: true, + // not already customizeable + // visible: boolean('lVisible', true, 'line'), + }, + border: { + stroke: 'gray', + strokeWidth: 2, + visible: false, + // not already customizeable + // stroke: color('lBorderStroke', 'gray', 'line'), + // strokeWidth: range('lBorderStrokeWidth', 0, 10, 2, 'line'), + // visible: boolean('lBorderVisible', false, 'line'), + }, + point: { + visible: true, + // not already customizeable + // visible: boolean('lPointVisible', true, 'line'), + radius: range('lPointRadius', 0, 20, 5, 'line'), + stroke: color('lPointStroke', 'white', 'line'), + strokeWidth: range('lPointStrokeWidth', 0, 20, 1, 'line'), + }, + }, + areaSeriesStyle: { + area: { + // not already customizeable + fill: DEFAULT_MISSING_COLOR, + visible: true, + }, + line: { + stroke: DEFAULT_MISSING_COLOR, + strokeWidth: range('aStrokeWidth', 0, 10, 1, 'area'), + visible: true, + // not already customizeable + // visible: boolean('aVisible', true, 'area'), + }, + border: { + stroke: 'gray', + strokeWidth: 2, + visible: false, + // not already customizeable + // stroke: color('aBorderStroke', 'gray', 'area'), + // strokeWidth: range('aBorderStrokeWidth', 0, 10, 2, 'area'), + // visible: boolean('aBorderVisible', false, 'area'), + }, + point: { + visible: true, + // not already customizeable + // visible: boolean('aPointVisible', true, 'area'), + radius: range('aPointRadius', 0, 20, 5, 'area'), + stroke: color('aPointStroke', 'white', 'area'), + strokeWidth: range('aPointStrokeWidth', 0, 20, 1, 'area'), + }, + }, + barSeriesStyle: { + border: { + stroke: color('bBorderStroke', 'white', 'bar'), + strokeWidth: range('bStrokeWidth', 0, 10, 1, 'bar'), + visible: boolean('bBorderVisible', true, 'bar'), + }, + }, + sharedStyle: { + default: { + opacity: range('sOpacity', 0, 1, 1, 'Shared', 0.05), + }, + highlighted: { + opacity: range('sHighlighted', 0, 1, 1, 'Shared', 0.05), + }, + unhighlighted: { + opacity: range('sUnhighlighted', 0, 1, 0.25, 'Shared', 0.05), + }, + }, + colors: { + vizColors: select( + 'vizColors', + { + colorBlind: palettes.euiPaletteColorBlind.colors, + darkBackground: palettes.euiPaletteForDarkBackground.colors, + lightBackground: palettes.euiPaletteForLightBackground.colors, + forStatus: palettes.euiPaletteForStatus.colors, + }, + palettes.euiPaletteColorBlind.colors, + 'Colors', + ), + defaultVizColor: DEFAULT_MISSING_COLOR, + }, + }; + + const darkmode = boolean('darkmode', false, 'Colors'); + const className = darkmode ? 'story-chart-dark' : 'story-chart'; + const defaultTheme = darkmode ? DARK_THEME : LIGHT_THEME; + const customTheme = mergeWithDefaultTheme(theme, defaultTheme); + switchTheme(darkmode ? 'dark' : 'light'); + const dg = new DataGenerator(); + const data1 = dg.generateGroupedSeries(40, 4); + const data2 = dg.generateSimpleSeries(40); + const data3 = dg.generateSimpleSeries(40); + return ( + + + + Number(d).toFixed(2)} + /> + + Number(d).toFixed(2)} + /> + + + + + ); }); diff --git a/stories/utils.ts b/stories/utils.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/yarn.lock b/yarn.lock index 095fc6ac18..6f0232a693 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11420,6 +11420,11 @@ react-is@~16.3.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" integrity sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q== +react-is@~16.3.0: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" + integrity sha512-ybEM7YOr4yBgFd6w8dJqwxegqZGJNBZl6U27HnGKuTZmDvVrD5quWOK/wAnMywiZzW+Qsk+l4X2c70+thp/A8Q== + react-konva@^16.7.1: version "16.7.1" resolved "https://registry.yarnpkg.com/react-konva/-/react-konva-16.7.1.tgz#6ddcd7f9dd4f8015064eeb5fa8133b8dd1984407"