From 8e4ccb8a6c5a24c898378b491b572313f3bfcf6d Mon Sep 17 00:00:00 2001 From: nickofthyme Date: Mon, 13 Jul 2020 09:30:38 -0500 Subject: [PATCH] feat(axis): improve axis styles and flexibility - add visibility to ticks, labels, line and overall axis - add style overrides for each axis that override theme axis styles - improve padding controls for axis titles, labels and ticks - add tick label offsets to tweek final position of tick labels --- .eslintrc.js | 2 +- api/charts.api.md | 12 +- .../xy_chart/renderer/canvas/axes/index.ts | 60 ++-- .../xy_chart/renderer/canvas/axes/line.ts | 19 +- .../xy_chart/renderer/canvas/axes/tick.ts | 13 +- .../renderer/canvas/axes/tick_label.ts | 73 +++-- .../xy_chart/renderer/canvas/axes/title.ts | 57 ++-- .../xy_chart/renderer/canvas/grids.ts | 18 +- .../renderer/canvas/primitives/text.ts | 10 +- .../xy_chart/renderer/canvas/renderers.ts | 6 +- .../xy_chart/renderer/canvas/xy_chart.tsx | 6 +- src/chart_types/xy_chart/specs/axis.tsx | 5 - .../selectors/compute_axis_visible_ticks.ts | 4 + .../selectors/compute_chart_dimensions.ts | 5 +- .../state/selectors/get_axis_styles.ts | 44 +++ src/chart_types/xy_chart/tooltip/tooltip.ts | 7 +- .../xy_chart/utils/axis_utils.test.ts | 12 +- src/chart_types/xy_chart/utils/axis_utils.ts | 280 ++++++++++++------ .../xy_chart/utils/dimensions.test.ts | 4 +- src/chart_types/xy_chart/utils/dimensions.ts | 28 +- src/chart_types/xy_chart/utils/specs.ts | 18 +- src/specs/constants.ts | 2 +- src/utils/commons.ts | 69 +++++ src/utils/dimensions.ts | 29 ++ src/utils/domain.ts | 14 +- src/utils/themes/dark_theme.ts | 25 +- src/utils/themes/light_theme.ts | 25 +- src/utils/themes/theme.test.ts | 18 +- src/utils/themes/theme.ts | 67 ++++- stories/axes/2_tick_label_rotation.tsx | 118 +++++--- stories/grids/1_basic.tsx | 14 +- stories/grids/2_multiple_axes.tsx | 4 +- stories/legend/2_legend_bottom.tsx | 6 +- stories/stylings/15_tick_label.tsx | 2 +- stories/stylings/3_axis.tsx | 8 +- stories/utils/knobs.ts | 39 ++- 36 files changed, 793 insertions(+), 330 deletions(-) create mode 100644 src/chart_types/xy_chart/state/selectors/get_axis_styles.ts diff --git a/.eslintrc.js b/.eslintrc.js index f322e771f8..b236abadb5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,7 +34,7 @@ module.exports = { '@typescript-eslint/no-unsafe-return': 0, '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/restrict-template-expressions': 1, - '@typescript-eslint/restrict-plus-operands': 1, + '@typescript-eslint/restrict-plus-operands': 0, // rule is broken '@typescript-eslint/no-unsafe-call': 1, '@typescript-eslint/unbound-method': 1, 'unicorn/consistent-function-scoping': 1, diff --git a/api/charts.api.md b/api/charts.api.md index 62af02c8cb..206270d8eb 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -118,18 +118,18 @@ export const Axis: React.FunctionComponent; // @public (undocumented) export interface AxisConfig { // (undocumented) - axisLineStyle: StrokeStyle; + axisLine: StrokeStyle; // (undocumented) - axisTitleStyle: TextStyle; + axisTitle: TextStyle; // (undocumented) - gridLineStyle: { + gridLine: { horizontal: GridLineConfig; vertical: GridLineConfig; }; // (undocumented) - tickLabelStyle: TextStyle; + tickLabel: TextStyle; // (undocumented) - tickLineStyle: TickStyle; + tickLine: TickStyle; } // @public (undocumented) @@ -141,8 +141,8 @@ export type AxisId = string; export interface AxisSpec extends Spec { // (undocumented) chartType: typeof ChartTypes.XYAxis; + gridLine?: GridLineConfig; domain?: YDomainRange; - gridLineStyle?: GridLineConfig; groupId: GroupId; hide: boolean; id: AxisId; diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts index 11386cb3f0..adc995afda 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts @@ -20,9 +20,9 @@ import { withContext } from '../../../../../renderers/canvas'; import { Dimensions } from '../../../../../utils/dimensions'; import { AxisId } from '../../../../../utils/ids'; -import { AxisConfig } from '../../../../../utils/themes/theme'; +import { AxisStyle } from '../../../../../utils/themes/theme'; import { getSpecsById } from '../../../state/utils/spec'; -import { AxisTick, AxisTicksDimensions } from '../../../utils/axis_utils'; +import { AxisTick, AxisTicksDimensions, shouldShowTicks } from '../../../utils/axis_utils'; import { AxisSpec } from '../../../utils/specs'; import { renderDebugRect } from '../utils/debug'; import { renderLine } from './line'; @@ -32,7 +32,7 @@ import { renderTitle } from './title'; /** @internal */ export interface AxisProps { - axisConfig: AxisConfig; + axisStyle: AxisStyle; axisSpec: AxisSpec; axisTicksDimensions: AxisTicksDimensions; axisPosition: Dimensions; @@ -47,27 +47,41 @@ export interface AxesProps { axesSpecs: AxisSpec[]; axesTicksDimensions: Map; axesPositions: Map; - axisStyle: AxisConfig; + axesStyles: Map; + sharedAxesStyle: AxisStyle; debug: boolean; chartDimensions: Dimensions; } /** @internal */ export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { - const { axesVisibleTicks, axesSpecs, axesTicksDimensions, axesPositions, axisStyle, debug, chartDimensions } = props; + const { + axesVisibleTicks, + axesSpecs, + axesTicksDimensions, + axesPositions, + axesStyles, + sharedAxesStyle, + debug, + chartDimensions, + } = props; axesVisibleTicks.forEach((ticks, axisId) => { const axisSpec = getSpecsById(axesSpecs, axisId); const axisTicksDimensions = axesTicksDimensions.get(axisId); const axisPosition = axesPositions.get(axisId); - if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition) { + + if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition || axisSpec.hide) { return; } + + const axisStyle = axesStyles.get(axisSpec.id) ?? sharedAxesStyle; + renderAxis(ctx, { axisSpec, axisTicksDimensions, axisPosition, ticks, - axisConfig: axisStyle, + axisStyle, debug, chartDimensions, }); @@ -76,7 +90,8 @@ export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { function renderAxis(ctx: CanvasRenderingContext2D, props: AxisProps) { withContext(ctx, (ctx) => { - const { ticks, axisPosition, debug } = props; + const { ticks, axisPosition, debug, axisStyle, axisSpec } = props; + const showTicks = shouldShowTicks(axisStyle.tickLine, axisSpec.hide); ctx.translate(axisPosition.left, axisPosition.top); if (debug) { renderDebugRect(ctx, { @@ -90,18 +105,25 @@ function renderAxis(ctx: CanvasRenderingContext2D, props: AxisProps) { withContext(ctx, (ctx) => { renderLine(ctx, props); }); - withContext(ctx, (ctx) => { - ticks.forEach((tick) => { - renderTick(ctx, tick, props); - }); - }); - withContext(ctx, (ctx) => { - ticks - .filter((tick) => tick.label !== null) - .forEach((tick) => { - renderTickLabel(ctx, tick, props); + + if (showTicks) { + withContext(ctx, (ctx) => { + ticks.forEach((tick) => { + renderTick(ctx, tick, props); }); - }); + }); + } + + if (props.axisStyle.tickLabel.visible) { + withContext(ctx, (ctx) => { + ticks + .filter((tick) => tick.label !== null) + .forEach((tick) => { + renderTickLabel(ctx, tick, showTicks, props); + }); + }); + } + withContext(ctx, (ctx) => { renderTitle(ctx, props); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts index 23dd90f3f8..29b9aab2a8 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts @@ -22,12 +22,15 @@ import { Position } from '../../../../../utils/commons'; import { isVerticalAxis } from '../../../utils/axis_type_utils'; /** @internal */ -export function renderLine(ctx: CanvasRenderingContext2D, props: AxisProps) { - const { - axisSpec: { position }, - axisPosition, - axisConfig: { axisLineStyle }, - } = props; +export function renderLine(ctx: CanvasRenderingContext2D, { + axisSpec: { position }, + axisPosition, + axisStyle: { axisLine }, +}: AxisProps) { + if (!axisLine.visible) { + return; + } + const lineProps: number[] = []; if (isVerticalAxis(position)) { lineProps[0] = position === Position.Left ? axisPosition.width : 0; @@ -43,7 +46,7 @@ export function renderLine(ctx: CanvasRenderingContext2D, props: AxisProps) { ctx.beginPath(); ctx.moveTo(lineProps[0], lineProps[1]); ctx.lineTo(lineProps[2], lineProps[3]); - ctx.strokeStyle = axisLineStyle.stroke; - ctx.lineWidth = axisLineStyle.strokeWidth; + ctx.strokeStyle = axisLine.stroke; + ctx.lineWidth = axisLine.strokeWidth; ctx.stroke(); } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts index 72c462fb10..4ef06739ab 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -23,22 +23,19 @@ import { TickStyle } from '../../../../../utils/themes/theme'; import { stringToRGB } from '../../../../partition_chart/layout/utils/color_library_wrappers'; import { isVerticalAxis } from '../../../utils/axis_type_utils'; import { AxisTick } from '../../../utils/axis_utils'; -import { renderLine, MIN_STROKE_WIDTH } from '../primitives/line'; +import { renderLine } from '../primitives/line'; /** @internal */ export function renderTick(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { const { - axisSpec: { tickSize, position }, + axisSpec: { position }, axisPosition, - axisConfig: { tickLineStyle }, + axisStyle: { tickLine }, } = props; - if (!tickLineStyle.visible || tickLineStyle.strokeWidth < MIN_STROKE_WIDTH) { - return; - } if (isVerticalAxis(position)) { - renderVerticalTick(ctx, position, axisPosition.width, tickSize, tick.position, tickLineStyle); + renderVerticalTick(ctx, position, axisPosition.width, tickLine.size, tick.position, tickLine); } else { - renderHorizontalTick(ctx, position, axisPosition.height, tickSize, tick.position, tickLineStyle); + renderHorizontalTick(ctx, position, axisPosition.height, tickLine.size, tick.position, tickLine); } } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts index fc2c81f972..b651fd9525 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts @@ -25,41 +25,55 @@ import { renderText } from '../primitives/text'; import { renderDebugRectCenterRotated } from '../utils/debug'; /** @internal */ -export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { +export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, showTicks: boolean, props: AxisProps) { /** * padding is already computed through width * and bbox_calculator using tickLabelPadding * set padding to 0 to avoid conflict */ - const labelStyle = { - ...props.axisConfig.tickLabelStyle, - padding: 0, - }; + const labelStyle = props.axisStyle.tickLabel; const { - axisSpec: { tickSize, tickPadding, position }, + axisSpec: { position }, axisTicksDimensions, axisPosition, debug, } = props; + const { + rotation: tickLabelRotation, + alignment, + offset, + } = props.axisStyle.tickLabel; - const tickLabelRotation = props.axisSpec.tickLabelRotation || 0; - - const tickLabelProps = getTickLabelProps( - tickLabelRotation, - tickSize, - tickPadding, + const { + maxLabelBboxWidth, + maxLabelBboxHeight, + maxLabelTextWidth, + maxLabelTextHeight, + } = axisTicksDimensions; + const { + x, + y, + offsetX, + offsetY, + textOffsetX, + textOffsetY, + align, + verticalAlign, + } = getTickLabelProps( + props.axisStyle, tick.position, position, axisPosition, axisTicksDimensions, + showTicks, + offset, + alignment, ); - const { maxLabelTextWidth, maxLabelTextHeight } = axisTicksDimensions; - - const { x, y, offsetX, offsetY, align, verticalAlign } = tickLabelProps; if (debug) { + // full text container renderDebugRectCenterRotated( ctx, { @@ -76,6 +90,25 @@ export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, p undefined, tickLabelRotation, ); + // rotated text container + if (![0, -90, 90, 180].includes(tickLabelRotation)) { + renderDebugRectCenterRotated( + ctx, + { + x: x + offsetX, + y: y + offsetY, + }, + { + x: x + offsetX, + y: y + offsetY, + height: maxLabelBboxHeight, + width: maxLabelBboxWidth, + }, + undefined, + undefined, + 0, + ); + } } const font: Font = { fontFamily: labelStyle.fontFamily, @@ -86,13 +119,11 @@ export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, p textOpacity: 1, }; withContext(ctx, (ctx) => { - const textOffsetX = tickLabelRotation === 0 ? 0 : offsetX; - const textOffsetY = tickLabelRotation === 0 ? 0 : offsetY; renderText( ctx, { - x: x + textOffsetX, - y: y + textOffsetY, + x: x + offsetX, + y: y + offsetY, }, tick.label, { @@ -103,6 +134,10 @@ export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, p baseline: verticalAlign as CanvasTextBaseline, }, tickLabelRotation, + { + x: textOffsetX, + y: textOffsetY, + }, ); }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts index 52b797388e..172de9dda2 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts @@ -19,8 +19,10 @@ import { AxisProps } from '.'; import { Position } from '../../../../../utils/commons'; +import { getSimplePadding } from '../../../../../utils/dimensions'; import { Font, FontStyle } from '../../../../partition_chart/layout/types/types'; import { isHorizontalAxis } from '../../../utils/axis_type_utils'; +import { shouldShowTicks } from '../../../utils/axis_utils'; import { renderText } from '../primitives/text'; import { renderDebugRect } from '../utils/debug'; @@ -41,48 +43,57 @@ export function renderTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { const { axisPosition: { height }, - axisSpec: { title, position, tickSize, tickPadding }, + axisSpec: { title, position, hide: hideAxis }, axisTicksDimensions: { maxLabelBboxWidth }, - axisConfig: { axisTitleStyle }, + axisStyle: { + axisTitle, + tickLine, + tickLabel, + }, debug, } = props; if (!title) { return null; } - const { padding, ...titleStyle } = axisTitleStyle; + const tickDimension = shouldShowTicks(tickLine, hideAxis) ? tickLine.size + tickLine.padding : 0; + const titlePadding = getSimplePadding(axisTitle.padding); + const labelPadding = getSimplePadding(tickLabel.padding); + const labelPaddingSum = labelPadding.outer + labelPadding.inner; const top = height; - const left = position === Position.Left ? 0 : tickSize + tickPadding + maxLabelBboxWidth + padding; + const left = position === Position.Left ? titlePadding.outer : tickDimension + maxLabelBboxWidth + labelPaddingSum + titlePadding.inner; if (debug) { - renderDebugRect(ctx, { x: left, y: top, width: height, height: titleStyle.fontSize }, undefined, undefined, -90); + renderDebugRect(ctx, { x: left, y: top, width: height, height: axisTitle.fontSize }, undefined, undefined, -90); } const font: Font = { - fontFamily: titleStyle.fontFamily, + fontFamily: axisTitle.fontFamily, fontVariant: 'normal', - fontStyle: titleStyle.fontStyle ? (titleStyle.fontStyle as FontStyle) : 'normal', + fontStyle: axisTitle.fontStyle ? (axisTitle.fontStyle as FontStyle) : 'normal', fontWeight: 'normal', - textColor: titleStyle.fill, + textColor: axisTitle.fill, textOpacity: 1, }; renderText( ctx, { - x: left + titleStyle.fontSize / 2, + x: left + axisTitle.fontSize / 2, y: top - height / 2, }, title, - { ...font, fill: titleStyle.fill, align: 'center', baseline: 'middle', fontSize: titleStyle.fontSize }, + { ...font, fill: axisTitle.fill, align: 'center', baseline: 'middle', fontSize: axisTitle.fontSize }, -90, ); } function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { const { axisPosition: { width }, - axisSpec: { title, position, tickSize, tickPadding }, + axisSpec: { title, position, hide: hideAxis }, axisTicksDimensions: { maxLabelBboxHeight }, - axisConfig: { - axisTitleStyle: { padding, ...titleStyle }, + axisStyle: { + axisTitle, + tickLine, + tickLabel, }, debug, } = props; @@ -91,33 +102,37 @@ function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) return; } - const top = position === Position.Top ? 0 : maxLabelBboxHeight + tickPadding + tickSize + padding; + const tickDimension = shouldShowTicks(tickLine, hideAxis) ? tickLine.size + tickLine.padding : 0; + const titlePadding = getSimplePadding(axisTitle.padding); + const labelPadding = getSimplePadding(tickLabel.padding); + const labelPaddingSum = labelPadding.outer + labelPadding.inner; + const top = position === Position.Top ? titlePadding.outer : maxLabelBboxHeight + labelPaddingSum + tickDimension + titlePadding.inner; const left = 0; if (debug) { - renderDebugRect(ctx, { x: left, y: top, width, height: titleStyle.fontSize }); + renderDebugRect(ctx, { x: left, y: top, width, height: axisTitle.fontSize }); } const font: Font = { - fontFamily: titleStyle.fontFamily, + fontFamily: axisTitle.fontFamily, fontVariant: 'normal', - fontStyle: titleStyle.fontStyle ? (titleStyle.fontStyle as FontStyle) : 'normal', + fontStyle: axisTitle.fontStyle ? (axisTitle.fontStyle as FontStyle) : 'normal', fontWeight: 'normal', - textColor: titleStyle.fill, + textColor: axisTitle.fill, textOpacity: 1, }; renderText( ctx, { x: left + width / 2, - y: top + titleStyle.fontSize / 2, + y: top + axisTitle.fontSize / 2, }, title, { ...font, - fill: titleStyle.fill, + fill: axisTitle.fill, align: 'center', baseline: 'middle', - fontSize: titleStyle.fontSize, + fontSize: axisTitle.fontSize, }, ); } diff --git a/src/chart_types/xy_chart/renderer/canvas/grids.ts b/src/chart_types/xy_chart/renderer/canvas/grids.ts index 682dc99e68..f373c54f4f 100644 --- a/src/chart_types/xy_chart/renderer/canvas/grids.ts +++ b/src/chart_types/xy_chart/renderer/canvas/grids.ts @@ -45,20 +45,20 @@ export function renderGrids(ctx: CanvasRenderingContext2D, props: GridProps) { const axisSpec = getSpecsById(axesSpecs, axisId); if (axisSpec && axisGridLinesPositions.length > 0) { const themeConfig = isVerticalGrid(axisSpec.position) - ? chartTheme.axes.gridLineStyle.vertical - : chartTheme.axes.gridLineStyle.horizontal; + ? chartTheme.axes.gridLine.vertical + : chartTheme.axes.gridLine.horizontal; - const axisSpecConfig = axisSpec.gridLineStyle; - const gridLineStyle = axisSpecConfig ? mergeGridLineConfigs(axisSpecConfig, themeConfig) : themeConfig; - if (!gridLineStyle.stroke || !gridLineStyle.strokeWidth || gridLineStyle.strokeWidth < MIN_STROKE_WIDTH) { + const axisSpecConfig = axisSpec.gridLine; + const gridLine = axisSpecConfig ? mergeGridLineConfigs(axisSpecConfig, themeConfig) : themeConfig; + if (!gridLine.stroke || !gridLine.strokeWidth || gridLine.strokeWidth < MIN_STROKE_WIDTH) { return; } - const strokeColor = stringToRGB(gridLineStyle.stroke); - strokeColor.opacity = gridLineStyle.opacity !== undefined ? strokeColor.opacity * gridLineStyle.opacity : strokeColor.opacity; + const strokeColor = stringToRGB(gridLine.stroke); + strokeColor.opacity = gridLine.opacity !== undefined ? strokeColor.opacity * gridLine.opacity : strokeColor.opacity; const stroke: Stroke = { color: strokeColor, - width: gridLineStyle.strokeWidth, - dash: gridLineStyle.dash, + width: gridLine.strokeWidth, + dash: gridLine.dash, }; const lines = axisGridLinesPositions.map(([x1, y1, x2, y2]) => ({ x1, y1, x2, y2 })); renderMultiLine(ctx, lines, stroke); diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts index 5ab3ca8094..bed64074b5 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/text.ts @@ -27,18 +27,26 @@ export function renderText( ctx: CanvasRenderingContext2D, origin: Point, text: string, - font: Font & { fill: string; fontSize: number; align: TextAlign; baseline: TextBaseline }, + font: Font & { + fill: string; + fontSize: number; + align: TextAlign; + baseline: TextBaseline; + }, degree: number = 0, + translation?: Partial, ) { if (text === undefined || text === null) { return; } + withRotatedOrigin(ctx, origin, degree, (ctx) => { withContext(ctx, (ctx) => { ctx.fillStyle = font.fill; ctx.textAlign = font.align; ctx.textBaseline = font.baseline; ctx.font = cssFontShorthand(font, font.fontSize); + ctx.translate(translation?.x ?? 0, translation?.y ?? 0); ctx.fillText(text, origin.x, origin.y); }); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/src/chart_types/xy_chart/renderer/canvas/renderers.ts index cc0a8a7efb..18da94ec3b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -54,6 +54,7 @@ export function renderXYChartCanvas2d( axisTickPositions, axesSpecs, axesTicksDimensions, + axesStyles, axesGridLinesPositions, debug, } = props; @@ -67,7 +68,7 @@ export function renderXYChartCanvas2d( // The layers are callbacks, because of the need to not bake in the `ctx`, it feels more composable and uncoupled this way. renderLayers(ctx, [ // clear the canvas - (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000 /* , backgroundColor */), + (ctx: CanvasRenderingContext2D) => clearCanvas(ctx, 200000, 200000), (ctx: CanvasRenderingContext2D) => { renderAxes(ctx, { @@ -77,7 +78,8 @@ export function renderXYChartCanvas2d( axesVisibleTicks: axisTickPositions.axisVisibleTicks, chartDimensions, debug, - axisStyle: theme.axes, + axesStyles, + sharedAxesStyle: theme.axes, }); }, (ctx: CanvasRenderingContext2D) => { diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 78f87885f8..55a7328d29 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -34,7 +34,7 @@ import { Dimensions } from '../../../../utils/dimensions'; import { deepEqual } from '../../../../utils/fast_deep_equal'; import { AnnotationId, AxisId } from '../../../../utils/ids'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; -import { Theme } from '../../../../utils/themes/theme'; +import { Theme, AxisStyle } from '../../../../utils/themes/theme'; import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; import { computeAxisTicksDimensionsSelector } from '../../state/selectors/compute_axis_ticks_dimensions'; @@ -42,6 +42,7 @@ import { AxisVisibleTicks, computeAxisVisibleTicksSelector } from '../../state/s import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; +import { getAxesStylesSelector } from '../../state/selectors/get_axis_styles'; import { getHighlightedSeriesSelector } from '../../state/selectors/get_highlighted_series'; import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; @@ -66,6 +67,7 @@ export interface ReactiveChartStateProps { highlightedLegendItem?: LegendItem; axesSpecs: AxisSpec[]; axesTicksDimensions: Map; + axesStyles: Map; axisTickPositions: AxisVisibleTicks; axesGridLinesPositions: Map; annotationDimensions: Map; @@ -213,6 +215,7 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { axisVisibleTicks: new Map(), }, axesTicksDimensions: new Map(), + axesStyles: new Map(), axesGridLinesPositions: new Map(), annotationDimensions: new Map(), annotationSpecs: [], @@ -240,6 +243,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { axesSpecs: getAxisSpecsSelector(state), axisTickPositions: computeAxisVisibleTicksSelector(state), axesTicksDimensions: computeAxisTicksDimensionsSelector(state), + axesStyles: getAxesStylesSelector(state), axesGridLinesPositions: computeAxisVisibleTicksSelector(state).axisGridLinesPositions, annotationDimensions: computeAnnotationDimensionsSelector(state), annotationSpecs: getAnnotationSpecsSelector(state), diff --git a/src/chart_types/xy_chart/specs/axis.tsx b/src/chart_types/xy_chart/specs/axis.tsx index ce6fb096dc..3f38428c1f 100644 --- a/src/chart_types/xy_chart/specs/axis.tsx +++ b/src/chart_types/xy_chart/specs/axis.tsx @@ -33,8 +33,6 @@ const defaultProps = { showOverlappingTicks: false, showOverlappingLabels: false, position: Position.Left, - tickSize: 10, - tickPadding: 10, tickFormat: (tick: any) => `${tick}`, tickLabelRotation: 0, }; @@ -50,9 +48,6 @@ export const Axis: React.FunctionComponent = getCo | 'showOverlappingTicks' | 'showOverlappingLabels' | 'position' - | 'tickSize' - | 'tickPadding' | 'tickFormat' - | 'tickLabelRotation' >(defaultProps), ); diff --git a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts b/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts index 6d73763d72..0e4c6496e3 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts @@ -29,6 +29,7 @@ import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensi import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; import { countBarsInClusterSelector } from './count_bars_in_cluster'; +import { getAxesStylesSelector } from './get_axis_styles'; import { getBarPaddingsSelector } from './get_bar_paddings'; import { getAxisSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; @@ -48,6 +49,7 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( getSettingsSpecSelector, getAxisSpecsSelector, computeAxisTicksDimensionsSelector, + getAxesStylesSelector, computeSeriesDomainsSelector, countBarsInClusterSelector, isHistogramModeEnabledSelector, @@ -59,6 +61,7 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( settingsSpec, axesSpecs, axesTicksDimensions, + axesStyles, seriesDomainsAndData, totalBarsInCluster, isHistogramMode, @@ -71,6 +74,7 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( settingsSpec.rotation, axesSpecs, axesTicksDimensions, + axesStyles, xDomain, yDomain, totalBarsInCluster, diff --git a/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts b/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts index f890e66c6a..9d0908db56 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts @@ -25,6 +25,7 @@ import { getChartThemeSelector } from '../../../../state/selectors/get_chart_the import { Dimensions } from '../../../../utils/dimensions'; import { computeChartDimensions } from '../../utils/dimensions'; import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensions'; +import { getAxesStylesSelector } from './get_axis_styles'; import { getAxisSpecsSelector } from './get_specs'; /** @internal */ @@ -34,14 +35,16 @@ export const computeChartDimensionsSelector = createCachedSelector( getChartThemeSelector, computeAxisTicksDimensionsSelector, getAxisSpecsSelector, + getAxesStylesSelector, ], ( chartContainerDimensions, chartTheme, axesTicksDimensions, axesSpecs, + axesStyles, ): { chartDimensions: Dimensions; leftMargin: number; - } => computeChartDimensions(chartContainerDimensions, chartTheme, axesTicksDimensions, axesSpecs), + } => computeChartDimensions(chartContainerDimensions, chartTheme, axesTicksDimensions, axesStyles, axesSpecs), )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts b/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts new file mode 100644 index 0000000000..c47c0d918a --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/get_axis_styles.ts @@ -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 createCachedSelector from 're-reselect'; + +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { mergePartial } from '../../../../utils/commons'; +import { AxisId } from '../../../../utils/ids'; +import { AxisStyle } from '../../../../utils/themes/theme'; +import { getAxisSpecsSelector } from './get_specs'; + +/** + * Get merged axis styles. **Only** include axes with styles overrides. + * + * @internal + */ +export const getAxesStylesSelector = createCachedSelector( + [getAxisSpecsSelector, getChartThemeSelector], + (axesSpecs, { axes: sharedAxesStyle }): Map => { + const axesStyles = new Map(); + axesSpecs.forEach(({ id, style }) => { + const newStyle = style ? mergePartial(sharedAxesStyle, style, { mergeOptionalPartialValues: true }) : null; + axesStyles.set(id, newStyle); + }); + return axesStyles; + }, +)(getChartIdSelector); diff --git a/src/chart_types/xy_chart/tooltip/tooltip.ts b/src/chart_types/xy_chart/tooltip/tooltip.ts index a991ed2730..ed22408756 100644 --- a/src/chart_types/xy_chart/tooltip/tooltip.ts +++ b/src/chart_types/xy_chart/tooltip/tooltip.ts @@ -21,6 +21,7 @@ import { LegendItemExtraValues } from '../../../commons/legend'; import { SeriesKey } from '../../../commons/series_id'; import { TooltipValue } from '../../../specs'; import { getAccessorFormatLabel } from '../../../utils/accessor'; +import { identity } from '../../../utils/commons'; import { IndexedGeometry, BandedAccessorType } from '../../../utils/geometry'; import { getSeriesName } from '../utils/series'; import { @@ -88,14 +89,10 @@ export function formatTooltip( seriesIdentifier, valueAccessor: accessor, label, - value: axisSpec ? axisSpec.tickFormat(value, tickFormatOptions) : emptyFormatter(value), + value: axisSpec ? axisSpec.tickFormat(value, tickFormatOptions) : identity(value), markValue: isHeader || mark === null ? null : mark, color, isHighlighted: isHeader ? false : isHighlighted, isVisible, }; } - -function emptyFormatter(value: T): T { - return value; -} diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index 00f47012a7..412341879f 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -51,11 +51,10 @@ import { getVerticalAxisTickLineProps, getVisibleTicks, isYDomain, - getAxisTickLabelPadding, enableDuplicatedTicks, } from './axis_utils'; import { computeXScale } from './scales'; -import { AxisSpec, DomainRange, AxisStyle, DEFAULT_GLOBAL_ID, TickFormatter } from './specs'; +import { AxisSpec, DomainRange, DEFAULT_GLOBAL_ID, TickFormatter } from './specs'; describe('Axis computational utils', () => { const mockedRect = { @@ -1386,15 +1385,6 @@ describe('Axis computational utils', () => { expect(JSON.stringify(negativeReducer)).toEqual(JSON.stringify(positiveReducer)); }); - test('should expect axisSpec.style.tickLabelPadding if specified', () => { - const axisSpecStyle: AxisStyle = { - tickLabelPadding: 2, - }; - - const axisConfigTickLabelPadding = 1; - - expect(getAxisTickLabelPadding(axisConfigTickLabelPadding, axisSpecStyle)).toEqual(2); - }); test('should show unique tick labels if duplicateTicks is set to false', () => { const now = DateTime.fromISO('2019-01-11T00:00:00.000') .setZone('utc+1') diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index 5c8abfdceb..addbced409 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -19,20 +19,16 @@ import { Scale } from '../../../scales'; import { BBox, BBoxCalculator } from '../../../utils/bbox/bbox_calculator'; -import { Position, Rotation, getUniqueValues } from '../../../utils/commons'; -import { Dimensions, Margins } from '../../../utils/dimensions'; +import { Position, Rotation, getUniqueValues, VerticalAlignment, HorizontalAlignment, getPercenageValue } from '../../../utils/commons'; +import { Dimensions, Margins, getSimplePadding, SimplePadding } from '../../../utils/dimensions'; import { AxisId } from '../../../utils/ids'; -import { AxisConfig, Theme } from '../../../utils/themes/theme'; +import { AxisStyle, Theme, TextAlignment, TextOffset } from '../../../utils/themes/theme'; import { XDomain, YDomain } from '../domains/types'; +import { MIN_STROKE_WIDTH } from '../renderer/canvas/primitives/line'; import { getSpecsById } from '../state/utils/spec'; import { isVerticalAxis } from './axis_type_utils'; import { computeXScale, computeYScales } from './scales'; -import { - AxisSpec, - TickFormatter, - AxisStyle, - TickFormatterOptions, -} from './specs'; +import { AxisSpec, TickFormatterOptions } from './specs'; export type AxisLinePosition = [number, number, number, number]; @@ -56,6 +52,8 @@ export interface TickLabelProps { y: number; offsetX: number; offsetY: number; + textOffsetX: number; + textOffsetY: number; align: string; verticalAlign: string; } @@ -78,13 +76,14 @@ export function computeAxisTicksDimensions( totalBarsInCluster: number, bboxCalculator: BBoxCalculator, chartRotation: Rotation, - axisConfig: AxisConfig, + { gridLine, tickLabel }: AxisStyle, barsPadding?: number, enableHistogramMode?: boolean, ): AxisTicksDimensions | null { - if (axisSpec.hide) { + if (axisSpec.hide && !gridLine.horizontal.visible && !gridLine.vertical.visible) { return null; } + const scale = getScaleForAxisSpec( axisSpec, xDomain, @@ -96,22 +95,17 @@ export function computeAxisTicksDimensions( barsPadding, enableHistogramMode, ); + if (!scale) { throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`); } - const tickLabelPadding = getAxisTickLabelPadding(axisConfig.tickLabelStyle.padding, axisSpec.style); - const dimensions = computeTickDimensions( scale, - axisSpec.tickFormat, + axisSpec, bboxCalculator, - axisConfig, - tickLabelPadding, - axisSpec.tickLabelRotation, - { - timeZone: xDomain.timeZone, - }, + tickLabel, + { timeZone: xDomain.timeZone }, ); return { @@ -119,14 +113,6 @@ export function computeAxisTicksDimensions( }; } -/** @internal */ -export function getAxisTickLabelPadding(axisConfigTickLabelPadding: number, axisSpecStyle?: AxisStyle): number { - if (axisSpecStyle && axisSpecStyle.tickLabelPadding !== undefined) { - return axisSpecStyle.tickLabelPadding; - } - return axisConfigTickLabelPadding; -} - /** @internal */ export function isYDomain(position: Position, chartRotation: Rotation): boolean { const isStraightRotation = chartRotation === 0 || chartRotation === 180; @@ -190,14 +176,15 @@ export function computeRotatedLabelDimensions(unrotatedDims: BBox, degreesRotati } /** @internal */ -export const getMaxBboxDimensions = ( +export const getMaxLabelDimensions = ( bboxCalculator: BBoxCalculator, - fontSize: number, - fontFamily: string, - tickLabelRotation: number, - tickLabelPadding: number, + { + fontSize, + fontFamily, + rotation, + }: AxisStyle['tickLabel'], ) => ( - acc: { [key: string]: number }, + acc: Record, tickLabel: string, ): { maxLabelBboxWidth: number; @@ -205,9 +192,8 @@ export const getMaxBboxDimensions = ( maxLabelTextWidth: number; maxLabelTextHeight: number; } => { - const bbox = bboxCalculator.compute(tickLabel, tickLabelPadding, fontSize, fontFamily); - - const rotatedBbox = computeRotatedLabelDimensions(bbox, tickLabelRotation); + const bbox = bboxCalculator.compute(tickLabel, 0, fontSize, fontFamily); + const rotatedBbox = computeRotatedLabelDimensions(bbox, rotation); const width = Math.ceil(rotatedBbox.width); const height = Math.ceil(rotatedBbox.height); @@ -228,41 +214,122 @@ export const getMaxBboxDimensions = ( function computeTickDimensions( scale: Scale, - tickFormat: TickFormatter, + { tickFormat }: AxisSpec, bboxCalculator: BBoxCalculator, - axisConfig: AxisConfig, - tickLabelPadding: number, - tickLabelRotation = 0, + tickLabelStyle: AxisStyle['tickLabel'], tickFormatOptions?: TickFormatterOptions, ) { const tickValues = scale.ticks(); const tickLabels = tickValues.map((d) => tickFormat(d, tickFormatOptions)); - const { - tickLabelStyle: { fontFamily, fontSize }, - } = axisConfig; - const { - maxLabelBboxWidth, - maxLabelBboxHeight, - maxLabelTextWidth, - maxLabelTextHeight, - } = tickLabels.reduce( - getMaxBboxDimensions(bboxCalculator, fontSize, fontFamily, tickLabelRotation, tickLabelPadding), - { maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 }, - ); + const defaultAcc = { maxLabelBboxWidth: 0, maxLabelBboxHeight: 0, maxLabelTextWidth: 0, maxLabelTextHeight: 0 }; + const dimensions = tickLabelStyle.visible + ? tickLabels.reduce( + getMaxLabelDimensions(bboxCalculator, tickLabelStyle), + defaultAcc, + ) + : defaultAcc; + return { + ...dimensions, tickValues, tickLabels, - maxLabelBboxWidth, - maxLabelBboxHeight, - maxLabelTextWidth, - maxLabelTextHeight, }; } +function getUserTextOffsets(dimensions: AxisTicksDimensions, offset: TextOffset) { + const defaults = { + x: 0, + y: 0, + }; + + if (offset.reference === 'global') { + return { + local: defaults, + global: { + x: getPercenageValue(offset.x, dimensions.maxLabelBboxWidth, 0), + y: getPercenageValue(offset.y, dimensions.maxLabelBboxHeight, 0), + }, + }; + } + + return { + global: defaults, + local: { + x: getPercenageValue(offset.x, dimensions.maxLabelTextWidth, 0), + y: getPercenageValue(offset.y, dimensions.maxLabelTextHeight, 0), + }, + }; +} + +function getHorizontalTextOffset(width: number, alignment: 'center' | 'right' | 'left'): number { + switch (alignment) { + case 'left': + return -width / 2; + case 'right': + return width / 2; + case 'center': + default: + return 0; + } +} + +function getVerticalTextOffset(height: number, alignment: 'middle' | 'top' | 'bottom'): number { + switch (alignment) { + case 'top': + return -height / 2; + case 'bottom': + return height / 2; + case 'middle': + default: + return 0; + } +} + +function getHorizontalAlign(position: Position, alignment: HorizontalAlignment = HorizontalAlignment.Near): Exclude { + if ( + alignment === HorizontalAlignment.Center + || alignment === HorizontalAlignment.Right + || alignment === HorizontalAlignment.Left + ) { + return alignment; + } + + if (position === Position.Left) { + return alignment === HorizontalAlignment.Near ? HorizontalAlignment.Right : HorizontalAlignment.Left; + } + + if (position === Position.Right) { + return alignment === HorizontalAlignment.Near ? HorizontalAlignment.Left : HorizontalAlignment.Right; + } + + // fallback for near/far on top/bottom axis + return 'center'; +} + +function getVerticalAlign(position: Position, alignment: VerticalAlignment = VerticalAlignment.Middle): Exclude { + if ( + alignment === VerticalAlignment.Middle + || alignment === VerticalAlignment.Top + || alignment === VerticalAlignment.Bottom + ) { + return alignment; + } + + if (position === Position.Top) { + return alignment === VerticalAlignment.Near ? VerticalAlignment.Bottom : VerticalAlignment.Top; + } + + if (position === Position.Bottom) { + return alignment === VerticalAlignment.Near ? VerticalAlignment.Top : VerticalAlignment.Bottom; + } + + // fallback for near/far on left/right axis + return VerticalAlignment.Middle; +} + /** * Gets the computed x/y coordinates & alignment properties for an axis tick label. * @param isVerticalAxis if the axis is vertical (in contrast to horizontal) - * @param tickLabelRotation degree of rotation of the tick label * @param tickSize length of tick line * @param tickPadding amount of padding between label and tick line * @param tickPosition position of tick relative to axis line origin and other ticks along it @@ -271,39 +338,58 @@ function computeTickDimensions( * @internal */ export function getTickLabelProps( - tickLabelRotation: number, - tickSize: number, - tickPadding: number, + { + tickLine, + tickLabel, + }: AxisStyle, tickPosition: number, position: Position, axisPosition: Dimensions, - axisTickDimensions: Pick, + tickDimensions: AxisTicksDimensions, + showTicks: boolean, + textOffset: TextOffset, + textAlignment?: TextAlignment, ): TickLabelProps { - const { maxLabelBboxWidth, maxLabelBboxHeight } = axisTickDimensions; - const isRotated = tickLabelRotation !== 0; + const { maxLabelBboxWidth, maxLabelTextWidth, maxLabelBboxHeight, maxLabelTextHeight } = tickDimensions; + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const labelPadding = getSimplePadding(tickLabel.padding); + const isLeftAxis = position === Position.Left; + const isAxisTop = position === Position.Top; + const align = getHorizontalAlign(position, textAlignment?.horizontal); + const verticalAlign = getVerticalAlign(position, textAlignment?.vertical); + + const userOffsets = getUserTextOffsets(tickDimensions, textOffset); + const textOffsetX = getHorizontalTextOffset(maxLabelTextWidth, align) + userOffsets.local.x; + const textOffsetY = getVerticalTextOffset(maxLabelTextHeight, verticalAlign) + userOffsets.local.y; + if (isVerticalAxis(position)) { - const isLeftAxis = position === Position.Left; - const x = isLeftAxis ? axisPosition.width - tickSize - tickPadding : tickSize + tickPadding; - const offsetX = isLeftAxis ? -maxLabelBboxWidth / 2 : maxLabelBboxWidth / 2; + const x = isLeftAxis ? axisPosition.width - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner; + const offsetX = (isLeftAxis ? -1 : 1) * (maxLabelBboxWidth / 2); + return { x, y: tickPosition, - offsetX, - offsetY: 0, - align: isRotated ? 'center' : (isLeftAxis ? 'right' : 'left'), - verticalAlign: 'middle', + offsetX: offsetX + userOffsets.global.x, + offsetY: userOffsets.global.y, + textOffsetY, + textOffsetX, + align, + verticalAlign, }; } - const isAxisTop = position === Position.Top; + const offsetY = isAxisTop ? -maxLabelBboxHeight / 2 : maxLabelBboxHeight / 2; + return { x: tickPosition, - y: isAxisTop ? axisPosition.height - tickSize - tickPadding : tickSize + tickPadding, - offsetX: 0, - offsetY: isAxisTop ? -maxLabelBboxHeight / 2 : maxLabelBboxHeight / 2, - align: 'center', - verticalAlign: isRotated ? 'middle' : (isAxisTop ? 'bottom' : 'top'), + y: isAxisTop ? axisPosition.height - tickDimension - labelPadding.inner : tickDimension + labelPadding.inner, + offsetX: userOffsets.global.x, + offsetY: offsetY + userOffsets.global.y, + textOffsetX, + textOffsetY, + align, + verticalAlign, }; } @@ -514,14 +600,19 @@ export function getAxisPosition( chartDimensions: Dimensions, chartMargins: Margins, axisTitleHeight: number, + titlePadding: SimplePadding, + labelPadding: SimplePadding, axisSpec: AxisSpec, axisDim: AxisTicksDimensions, cumTopSum: number, cumBottomSum: number, cumLeftSum: number, cumRightSum: number, + tickDimension: number, ) { - const { position, tickSize, tickPadding } = axisSpec; + const titleDimension = titlePadding.inner + axisTitleHeight + titlePadding.outer; + const labelPaddingSum = labelPadding.inner + labelPadding.outer; + const { position } = axisSpec; const { maxLabelBboxHeight, maxLabelBboxWidth } = axisDim; const { top, left, height, width } = chartDimensions; const dimensions = { @@ -536,9 +627,9 @@ export function getAxisPosition( let rightIncrement = 0; if (isVerticalAxis(position)) { - const dimWidth = maxLabelBboxWidth + tickSize + tickPadding + axisTitleHeight; + const dimWidth = labelPaddingSum + maxLabelBboxWidth + tickDimension + titleDimension; if (position === Position.Left) { - leftIncrement = dimWidth + chartMargins.left; + leftIncrement = chartMargins.left + dimWidth; dimensions.left = cumLeftSum + chartMargins.left; } else { rightIncrement = dimWidth + chartMargins.right; @@ -546,7 +637,7 @@ export function getAxisPosition( } dimensions.width = dimWidth; } else { - const dimHeight = maxLabelBboxHeight + tickSize + tickPadding + axisTitleHeight; + const dimHeight = labelPaddingSum + maxLabelBboxHeight + tickDimension + titleDimension; if (position === Position.Top) { topIncrement = dimHeight + chartMargins.top; dimensions.top = cumTopSum + chartMargins.top; @@ -560,16 +651,24 @@ export function getAxisPosition( return { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement }; } +export function shouldShowTicks( + { visible, strokeWidth, size }: AxisStyle['tickLine'], + axisHidden: boolean, +): boolean { + return !axisHidden && visible && size > 0 && strokeWidth >= MIN_STROKE_WIDTH; +} + /** @internal */ export function getAxisTicksPositions( computedChartDims: { chartDimensions: Dimensions; leftMargin: number; }, - chartTheme: Theme, + { chartPaddings, chartMargins, axes: sharedAxesStyle }: Theme, chartRotation: Rotation, axisSpecs: AxisSpec[], axisDimensions: Map, + axesStyles: Map, xDomain: XDomain, yDomain: YDomain[], totalGroupsCount: number, @@ -581,7 +680,6 @@ export function getAxisTicksPositions( axisVisibleTicks: Map; axisGridLinesPositions: Map; } { - const { chartPaddings, chartMargins } = chartTheme; const axisPositions: Map = new Map(); const axisVisibleTicks: Map = new Map(); const axisTicks: Map = new Map(); @@ -632,25 +730,37 @@ export function getAxisTicksPositions( axisGridLinesPositions.set(id, gridLines); } - const { fontSize, padding } = chartTheme.axes.axisTitleStyle; + const { + axisTitle: { fontSize, padding }, + tickLine, + tickLabel, + } = axesStyles.get(id) ?? sharedAxesStyle; + const titlePadding = getSimplePadding(padding); + const labelPadding = getSimplePadding(tickLabel.padding); + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const axisTitleHeight = axisSpec.title !== undefined ? fontSize : 0; + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; - const axisTitleHeight = axisSpec.title !== undefined ? fontSize + padding : 0; const axisPosition = getAxisPosition( chartDimensions, chartMargins, axisTitleHeight, + titlePadding, + labelPadding, axisSpec, axisDim, cumTopSum, cumBottomSum, cumLeftSum, cumRightSum, + tickDimension, ); cumTopSum += axisPosition.topIncrement; cumBottomSum += axisPosition.bottomIncrement; cumLeftSum += axisPosition.leftIncrement; cumRightSum += axisPosition.rightIncrement; + axisPositions.set(id, axisPosition.dimensions); axisVisibleTicks.set(id, visibleTicks); axisTicks.set(id, allTicks); diff --git a/src/chart_types/xy_chart/utils/dimensions.test.ts b/src/chart_types/xy_chart/utils/dimensions.test.ts index d43cd2e3b5..6837398793 100644 --- a/src/chart_types/xy_chart/utils/dimensions.test.ts +++ b/src/chart_types/xy_chart/utils/dimensions.test.ts @@ -84,8 +84,8 @@ describe('Computed chart dimensions', () => { }, ...legend, }; - chartTheme.axes.axisTitleStyle.fontSize = 10; - chartTheme.axes.axisTitleStyle.padding = 10; + chartTheme.axes.axisTitle.fontSize = 10; + chartTheme.axes.axisTitle.padding = 10; test('should be equal to parent dimension with no axis minus margins', () => { const axisDims = new Map(); const axisSpecs: AxisSpec[] = []; diff --git a/src/chart_types/xy_chart/utils/dimensions.ts b/src/chart_types/xy_chart/utils/dimensions.ts index c65f1f5d3b..8318a5274d 100644 --- a/src/chart_types/xy_chart/utils/dimensions.ts +++ b/src/chart_types/xy_chart/utils/dimensions.ts @@ -18,11 +18,11 @@ */ import { Position } from '../../../utils/commons'; -import { Dimensions } from '../../../utils/dimensions'; +import { Dimensions, getSimplePadding } from '../../../utils/dimensions'; import { AxisId } from '../../../utils/ids'; -import { Theme } from '../../../utils/themes/theme'; +import { Theme, AxisStyle } from '../../../utils/themes/theme'; import { getSpecsById } from '../state/utils/spec'; -import { AxisTicksDimensions } from './axis_utils'; +import { AxisTicksDimensions, shouldShowTicks } from './axis_utils'; import { AxisSpec } from './specs'; /** @@ -38,8 +38,9 @@ import { AxisSpec } from './specs'; */ export function computeChartDimensions( parentDimensions: Dimensions, - chartTheme: Theme, + { chartMargins, chartPaddings, axes: sharedAxesStyles }: Theme, axisDimensions: Map, + axesStyles: Map, axisSpecs: AxisSpec[], ): { chartDimensions: Dimensions; @@ -56,10 +57,6 @@ export function computeChartDimensions( leftMargin: 0, }; } - const { chartMargins, chartPaddings } = chartTheme; - const { axisTitleStyle } = chartTheme.axes; - - const axisTitleHeight = axisTitleStyle.fontSize + axisTitleStyle.padding; let vLeftAxisSpecWidth = 0; let vRightAxisSpecWidth = 0; @@ -72,10 +69,16 @@ export function computeChartDimensions( if (!axisSpec || axisSpec.hide) { return; } - const { position, tickSize, tickPadding, title } = axisSpec; - const titleHeight = title !== undefined ? axisTitleHeight : 0; - const maxAxisHeight = maxLabelBboxHeight + tickSize + tickPadding + titleHeight; - const maxAxisWidth = maxLabelBboxWidth + tickSize + tickPadding + titleHeight; + const { tickLine, axisTitle, tickLabel } = axesStyles.get(id) ?? sharedAxesStyles; + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const { position, title } = axisSpec; + const titlePadding = getSimplePadding(axisTitle.padding); + const labelPadding = getSimplePadding(tickLabel.padding); + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const titleHeight = title !== undefined ? axisTitle.fontSize : 0; + const axisDimension = labelPadding.inner + labelPadding.outer + tickDimension + titleHeight + titlePadding.outer + titlePadding.inner; + const maxAxisHeight = maxLabelBboxHeight + axisDimension; + const maxAxisWidth = maxLabelBboxWidth + axisDimension; switch (position) { case Position.Top: hTopAxisSpecHeight += maxAxisHeight + chartMargins.top; @@ -95,7 +98,6 @@ export function computeChartDimensions( default: vLeftAxisSpecWidth += maxAxisWidth + chartMargins.left; verticalEdgeLabelOverflow = Math.max(verticalEdgeLabelOverflow, maxLabelBboxHeight / 2); - break; } }); const chartLeftAxisMaxWidth = Math.max(vLeftAxisSpecWidth, horizontalEdgeLabelOverflow + chartMargins.left); diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index afdbf1b1ca..f29c0eeeb5 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -38,6 +38,7 @@ import { PointStyle, RectAnnotationStyle, BubbleSeriesStyle, + AxisStyle, } from '../../../utils/themes/theme'; import { PrimitiveValue } from '../../partition_chart/layout/utils/group_by_rollup'; import { AnnotationTooltipFormatter, CustomAnnotationTooltip } from '../annotations/types'; @@ -579,7 +580,7 @@ export interface AxisSpec extends Spec { /** The ID of the spec */ id: AxisId; /** Style options for grid line */ - gridLineStyle?: GridLineConfig; + gridLine?: GridLineConfig; /** * The ID of the axis group * @defaultValue {@link DEFAULT_GLOBAL_ID} @@ -598,22 +599,16 @@ export interface AxisSpec extends Spec { showGridLines?: boolean; /** Where the axis appear on the chart */ position: Position; - /** The length of the tick line */ - tickSize: number; - /** The padding between the label and the tick */ - tickPadding: number; /** A function called to format each single tick label */ tickFormat: TickFormatter; - /** The degrees of rotation of the tick labels */ - tickLabelRotation?: number; /** An approximate count of how many ticks will be generated */ ticks?: number; /** The axis title */ title?: string; + /** Custom style overrides */ + style?: RecursivePartial; /** If specified, it constrains the domain for these values */ domain?: YDomainRange; - /** Object to hold custom styling */ - style?: AxisStyle; /** Show only integar values * */ integersOnly?: boolean; /** @@ -631,11 +626,6 @@ export type TickFormatterOptions = { /** @public */ export type TickFormatter = (value: any, options?: TickFormatterOptions) => string; -export interface AxisStyle { - /** Specifies the amount of padding on the tick label bounding box */ - tickLabelPadding?: number; -} - export const AnnotationTypes = Object.freeze({ Line: 'line' as const, Rectangle: 'rectangle' as const, diff --git a/src/specs/constants.ts b/src/specs/constants.ts index 17ed528b5f..42f9879295 100644 --- a/src/specs/constants.ts +++ b/src/specs/constants.ts @@ -104,7 +104,7 @@ export const DEFAULT_SETTINGS_SPEC: SettingsSpec = { legendPosition: Position.Right, showLegendExtra: false, hideDuplicateAxes: false, - theme: LIGHT_THEME, + baseTheme: LIGHT_THEME, brushAxis: BrushAxis.X, minBrushDelta: 2, }; diff --git a/src/utils/commons.ts b/src/utils/commons.ts index 273b24e083..9df268a20b 100644 --- a/src/utils/commons.ts +++ b/src/utils/commons.ts @@ -40,6 +40,50 @@ export const ColorVariant = Object.freeze({ }); export type ColorVariant = $Values; +/** + * Horizontal text alignment + */ +export const HorizontalAlignment = Object.freeze({ + Center: 'center' as const, + Right: 'right' as const, + Left: 'left' as const, + /** + * Near side of relative baseline + * + * i.e. near side of axis depending on position + */ + Near: 'near' as const, + /** + * Far side of relative baseline + * + * i.e. far side of axis depending on position + */ + Far: 'far' as const, +}); +export type HorizontalAlignment = $Values; + +/** + * Vertical text alignment + */ +export const VerticalAlignment = Object.freeze({ + Middle: 'middle' as const, + Top: 'top' as const, + Bottom: 'bottom' as const, + /** + * Near side of relative baseline + * + * i.e. near side of axis depending on position + */ + Near: 'near' as const, + /** + * Far side of relative baseline + * + * i.e. far side of axis depending on position + */ + Far: 'far' as const, +}); +export type VerticalAlignment = $Values; + /** @public */ export type Datum = any; // unknown; /** @public */ @@ -454,3 +498,28 @@ export const round = (value: number, fractionDigits = 0): number => { return scaledValue / precision; }; + +/** + * Get number/percentage value from string + * + * i.e. `'90%'` with relative value of `100` returns `90` + */ +export const getPercenageValue = ( + ratio: string | number, + relativeValue: number, + defaultValue: T, +): number | T => { + if (typeof ratio === 'number') { + return ratio; + } + + const ratioStr = ratio.trim(); + + if (/\d+%$/.test(ratioStr)) { + const percentage = Number.parseInt(ratioStr.slice(0, -1), 10); + return relativeValue * (percentage / 100); + } + const num = Number.parseFloat(ratioStr); + + return num && !isNaN(num) ? num : defaultValue; +}; diff --git a/src/utils/dimensions.ts b/src/utils/dimensions.ts index efbdbd028d..822925f694 100644 --- a/src/utils/dimensions.ts +++ b/src/utils/dimensions.ts @@ -37,3 +37,32 @@ export interface PerSideDistance { * see https://github.com/elastic/elastic-charts/pull/660#discussion_r419474171 */ export type Margins = PerSideDistance; + +export type Padding = PerSideDistance; + +export type SimplePadding = { + outer: number; + inner: number; +}; + +/** + * Computes padding from number or `SimplePadding` with optional `minPadding` + * + * @param padding + * @param minPadding should be at least one to avoid browser measureText inconsistencies + */ +export function getSimplePadding(padding: number | Partial, minPadding = 0): SimplePadding { + if (typeof padding === 'number') { + const adjustedPadding = Math.max(minPadding, padding); + + return { + inner: adjustedPadding, + outer: adjustedPadding, + }; + } + + return { + inner: Math.max(minPadding, padding?.inner ?? 0), + outer: Math.max(minPadding, padding?.outer ?? 0), + }; +} diff --git a/src/utils/domain.ts b/src/utils/domain.ts index 38696f206b..debc3ab657 100644 --- a/src/utils/domain.ts +++ b/src/utils/domain.ts @@ -21,6 +21,7 @@ import { extent } from 'd3-array'; import { YDomainRange } from '../specs'; import { AccessorFn } from './accessor'; +import { getPercenageValue } from './commons'; export type Domain = any[]; @@ -51,17 +52,8 @@ function getPaddedRange(start: number, end: number, domainOptions?: YDomainRange let computedPadding = 0; if (typeof domainOptions.padding === 'string') { - const padding = domainOptions.padding.trim(); - - if (/\d+%$/.test(padding.trim())) { - const paddingPercent = Number.parseInt(padding.trim().slice(0, -1), 10); - const delta = Math.abs(end - start); - computedPadding = delta * (paddingPercent / 100); - } else { - const num = Number.parseFloat(padding); - - computedPadding = num && !isNaN(num) ? num : 0; - } + const delta = Math.abs(end - start); + computedPadding = getPercenageValue(domainOptions.padding, delta, 0); } else { computedPadding = domainOptions.padding; } diff --git a/src/utils/themes/dark_theme.ts b/src/utils/themes/dark_theme.ts index 0a3d0b199b..608a44e119 100644 --- a/src/utils/themes/dark_theme.ts +++ b/src/utils/themes/dark_theme.ts @@ -102,30 +102,45 @@ export const DARK_THEME: Theme = { histogramPadding: 0.05, }, axes: { - axisTitleStyle: { + axisTitle: { fontSize: 12, fontStyle: 'bold', fontFamily: 'sans-serif', padding: 8, fill: '#D4D4D4', + visible: true, }, - axisLineStyle: { + axisLine: { + visible: true, stroke: '#444', strokeWidth: 1, }, - tickLabelStyle: { + tickLabel: { + visible: true, fontSize: 10, fontFamily: 'sans-serif', fontStyle: 'normal', fill: '#999', padding: 1, + rotation: 0, + offset: { + x: 0, + y: 0, + reference: 'local', + }, + alignment: { + vertical: 'near', + horizontal: 'near', + }, }, - tickLineStyle: { + tickLine: { visible: true, stroke: '#444', strokeWidth: 1, + size: 10, + padding: 10, }, - gridLineStyle: { + gridLine: { horizontal: { visible: true, stroke: '#D3DAE6', diff --git a/src/utils/themes/light_theme.ts b/src/utils/themes/light_theme.ts index 1bb04762db..009395a3a9 100644 --- a/src/utils/themes/light_theme.ts +++ b/src/utils/themes/light_theme.ts @@ -102,30 +102,45 @@ export const LIGHT_THEME: Theme = { histogramPadding: 0.05, }, axes: { - axisTitleStyle: { + axisTitle: { + visible: true, fontSize: 12, fontStyle: 'bold', fontFamily: 'sans-serif', padding: 8, fill: '#333', }, - axisLineStyle: { + axisLine: { + visible: true, stroke: '#eaeaea', strokeWidth: 1, }, - tickLabelStyle: { + tickLabel: { + visible: true, fontSize: 10, fontFamily: 'sans-serif', fontStyle: 'normal', fill: '#777', padding: 4, + rotation: 0, + offset: { + x: 0, + y: 0, + reference: 'local', + }, + alignment: { + vertical: 'near', + horizontal: 'near', + }, }, - tickLineStyle: { + tickLine: { visible: true, stroke: '#eaeaea', strokeWidth: 1, + size: 10, + padding: 10, }, - gridLineStyle: { + gridLine: { horizontal: { visible: true, stroke: '#D3DAE6', diff --git a/src/utils/themes/theme.test.ts b/src/utils/themes/theme.test.ts index 206d5206f2..686ae5dc57 100644 --- a/src/utils/themes/theme.test.ts +++ b/src/utils/themes/theme.test.ts @@ -58,7 +58,7 @@ describe('Theme', () => { dash: [0, 0], }; const partialConfig = { strokeWidth: 5 }; - const themeConfig = LIGHT_THEME.axes.gridLineStyle.vertical; + const themeConfig = LIGHT_THEME.axes.gridLine.vertical; expect(mergeGridLineConfigs(fullConfig, themeConfig)).toEqual(fullConfig); expect(mergeGridLineConfigs({}, themeConfig)).toEqual(themeConfig); @@ -311,10 +311,10 @@ describe('Theme', () => { it('should merge partial theme: axes', () => { const partialTheme: PartialTheme = { axes: { - axisTitleStyle: { + axisTitle: { fontStyle: 'elastic_charts', }, - axisLineStyle: { + axisLine: { stroke: 'elastic_charts', }, }, @@ -324,13 +324,13 @@ describe('Theme', () => { ...DARK_THEME, axes: { ...DARK_THEME.axes, - axisTitleStyle: { - ...DARK_THEME.axes.axisTitleStyle, - ...partialTheme.axes!.axisTitleStyle, + axisTitle: { + ...DARK_THEME.axes.axisTitle, + ...partialTheme.axes!.axisTitle, }, - axisLineStyle: { - ...DARK_THEME.axes.axisLineStyle, - ...partialTheme.axes!.axisLineStyle, + axisLine: { + ...DARK_THEME.axes.axisLine, + ...partialTheme.axes!.axisLine, }, }, }); diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index 94b9d77219..2764ef990e 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -17,8 +17,8 @@ * under the License. */ -import { mergePartial, RecursivePartial, Color, ColorVariant } from '../commons'; -import { Margins } from '../dimensions'; +import { mergePartial, RecursivePartial, Color, ColorVariant, HorizontalAlignment, VerticalAlignment } from '../commons'; +import { Margins, SimplePadding } from '../dimensions'; import { LIGHT_THEME } from './light_theme'; interface Visible { @@ -30,7 +30,33 @@ export interface TextStyle { fontFamily: string; fontStyle?: string; fill: Color; - padding: number; + padding: number | SimplePadding; +} + +/** + * Offset in pixels + */ +export interface TextOffset { + /** + * X offset of tick in px or string with % of height + */ + x: number | string; + /** + * X offset of tick in px or string with % of height + */ + y: number | string; + /** + * Sets reference for offset to `global` coordinate or `local` + */ + reference: 'global' | 'local'; +} + +/** + * Text alignment + */ +export interface TextAlignment { + horizontal: HorizontalAlignment; + vertical: VerticalAlignment; } /** Shared style properties for varies geometries */ @@ -71,7 +97,16 @@ export interface StrokeStyle { } /** @public */ -export type TickStyle = StrokeStyle & Visible; +export type TickStyle = StrokeStyle & Visible & { + /** + * Amount of padding between tick line and labels + */ + padding: number; + /** + * length of tick line + */ + size: number; +}; /** * The dash array for a stroke @@ -90,12 +125,22 @@ export interface Opacity { opacity: number; } -export interface AxisConfig { - axisTitleStyle: TextStyle; - axisLineStyle: StrokeStyle; - tickLabelStyle: TextStyle; - tickLineStyle: TickStyle; - gridLineStyle: { +export interface AxisStyle { + axisTitle: TextStyle & Visible; + axisLine: StrokeStyle & Visible; + tickLabel: TextStyle & Visible & { + /** The degrees of rotation of the tick labels */ + rotation: number; + /** + * Offset in pixels to render text relative to anchor + * + * @note rotation aligns to global cartesian coordinates + */ + offset: TextOffset; + alignment: TextAlignment + }; + tickLine: TickStyle; + gridLine: { horizontal: GridLineConfig; vertical: GridLineConfig; }; @@ -199,7 +244,7 @@ export interface Theme { bubbleSeriesStyle: BubbleSeriesStyle; arcSeriesStyle: ArcSeriesStyle; sharedStyle: SharedGeometryStateStyle; - axes: AxisConfig; + axes: AxisStyle; scales: ScalesConfig; colors: ColorConfig; legend: LegendStyle; diff --git a/stories/axes/2_tick_label_rotation.tsx b/stories/axes/2_tick_label_rotation.tsx index 3067454f15..ee1fa79a4c 100644 --- a/stories/axes/2_tick_label_rotation.tsx +++ b/stories/axes/2_tick_label_rotation.tsx @@ -17,73 +17,110 @@ * under the License. */ -import { boolean, number } from '@storybook/addon-knobs'; +import { boolean, number, select } from '@storybook/addon-knobs'; import React from 'react'; -import { AreaSeries, Axis, Chart, Position, ScaleType, Settings } from '../../src'; +import { AreaSeries, Axis, Chart, Position, ScaleType, Settings, AxisStyle, RecursivePartial } from '../../src'; +import { getVerticalTextAlignmentKnob, getHorizontalTextAlignmentKnob, getPositiveNumberKnob } from '../utils/knobs'; + +const getAxisKnobs = (group?: string): RecursivePartial => ({ + axisTitle: { + padding: { + outer: getPositiveNumberKnob('Axis title padding - outer', 6, group), + inner: getPositiveNumberKnob('Axis title padding - inner', 6, group), + }, + }, + axisLine: { + visible: !boolean('Hide axis line', false, group), + }, + tickLine: { + visible: !boolean('Hide tick lines', false, group), + padding: getPositiveNumberKnob('Tick line padding', 10, group), + size: getPositiveNumberKnob('Tick line size', 10, group), + }, + tickLabel: { + visible: !boolean('Hide tick labels', false, group), + rotation: number('Tick label rotation', 0, { + range: true, + min: -90, + max: 90, + step: 1, + }, group), + padding: { + outer: getPositiveNumberKnob('Tick label padding - outer', 0, group), + inner: getPositiveNumberKnob('Tick label padding - inner', 0, group), + }, + offset: { + y: number('Tick label y offset', 0, { + range: true, + min: -10, + max: 10, + step: 1, + }, group), + x: number('Tick label x offset', 0, { + range: true, + min: -10, + max: 10, + step: 1, + }, group), + reference: select('Tick label offset reference', { + Global: 'global', + Local: 'local', + }, 'local', group), + }, + alignment: { + vertical: getVerticalTextAlignmentKnob(group), + horizontal: getHorizontalTextAlignmentKnob(group), + }, + }, +}); export const Example = () => { - const customStyle = { - tickLabelPadding: number('Tick Label Padding', 0), + const debug = boolean('debug', false, 'general'); + const onlyGlobal = !boolean('disable axis overrides', false, 'general'); + const bottomAxisStyles = getAxisKnobs(Position.Bottom); + const leftAxisStyles = getAxisKnobs(Position.Left); + const topAxisStyles = getAxisKnobs(Position.Top); + const rightAxisStyles = getAxisKnobs(Position.Right); + const theme = { + axes: getAxisKnobs('shared'), }; return ( + Number(d).toFixed(2)} - style={customStyle} - hide={boolean('hide left axis', false)} /> Number(d).toFixed(2)} - style={customStyle} - hide={boolean('hide top axis', false)} /> Number(d).toFixed(2)} - style={customStyle} - hide={boolean('hide right axis', false)} + style={onlyGlobal ? rightAxisStyles : undefined} + tickFormat={(d) => d % 2 === 0 ? Number(d).toFixed(2) : ''} + domain={{ min: 0, max: 10 }} /> { { x: 3, y: 6 }, ]} /> - ); }; diff --git a/stories/grids/1_basic.tsx b/stories/grids/1_basic.tsx index a2090de760..4e324ca1b6 100644 --- a/stories/grids/1_basic.tsx +++ b/stories/grids/1_basic.tsx @@ -81,13 +81,13 @@ export const Example = () => { const rightAxisGridLineConfig = generateGridLineConfig(Position.Right, 'red'); const topAxisGridLineConfig = generateGridLineConfig(Position.Top, 'teal'); const bottomAxisGridLineConfig = generateGridLineConfig(Position.Bottom, 'blue'); - const toggleBottomAxisGridLineStyle = boolean('use axis gridLineStyle vertically', false, 'bottom axis'); - const toggleHorizontalAxisGridLineStyle = boolean('use axis gridLineStyle horizontally', false, 'left axis'); + const toggleBottomAxisGridLineStyle = boolean('use axis gridLine vertically', false, 'bottom axis'); + const toggleHorizontalAxisGridLineStyle = boolean('use axis gridLine horizontally', false, 'left axis'); const bottomAxisThemeGridLineConfig = generateGridLineConfig('Vertical Axis Theme', 'violet'); const leftAxisThemeGridLineConfig = generateGridLineConfig('Horizontal Axis Theme', 'hotpink'); const theme = { axes: { - gridLineStyle: { vertical: leftAxisThemeGridLineConfig, horizontal: bottomAxisThemeGridLineConfig }, + gridLine: { vertical: leftAxisThemeGridLineConfig, horizontal: bottomAxisThemeGridLineConfig }, }, }; const integersOnlyLeft = boolean('left axis show only integer values', false, 'left axis'); @@ -101,7 +101,7 @@ export const Example = () => { title="Bottom axis" showOverlappingTicks showGridLines={boolean('show bottom axis grid lines', false, 'bottom axis')} - gridLineStyle={toggleBottomAxisGridLineStyle ? bottomAxisGridLineConfig : undefined} + gridLine={toggleBottomAxisGridLineStyle ? bottomAxisGridLineConfig : undefined} integersOnly={boolean('bottom axis show only integer values', false, 'bottom axis')} /> { title="Left axis 1" tickFormat={integersOnlyLeft ? (d) => Number(d).toFixed(0) : (d) => Number(d).toFixed(2)} showGridLines={boolean('show left axis grid lines', false, 'left axis')} - gridLineStyle={toggleHorizontalAxisGridLineStyle ? leftAxisGridLineConfig : undefined} + gridLine={toggleHorizontalAxisGridLineStyle ? leftAxisGridLineConfig : undefined} integersOnly={integersOnlyLeft} /> { title="Top axis" showOverlappingTicks showGridLines={boolean('show top axis grid lines', false, 'top axis')} - gridLineStyle={topAxisGridLineConfig} + gridLine={topAxisGridLineConfig} integersOnly={boolean('top axis show only integer values', false, 'top axis')} /> { position={Position.Right} tickFormat={integersOnlyRight ? (d) => Number(d).toFixed(0) : (d) => Number(d).toFixed(2)} showGridLines={boolean('show right axis grid lines', false, 'right axis')} - gridLineStyle={rightAxisGridLineConfig} + gridLine={rightAxisGridLineConfig} integersOnly={integersOnlyRight} /> { position={Position.Left} tickFormat={(d) => Number(d).toFixed(2)} showGridLines={boolean('show left axis grid lines', false, 'left axis')} - gridLineStyle={leftAxisGridLineConfig} + gridLine={leftAxisGridLineConfig} /> { position={Position.Left} tickFormat={(d) => Number(d).toFixed(2)} showGridLines={boolean('show left axis 2 grid lines', false, 'left2 axis')} - gridLineStyle={leftAxisGridLineConfig2} + gridLine={leftAxisGridLineConfig2} /> ( - + Number(d).toFixed(2)} /> diff --git a/stories/stylings/15_tick_label.tsx b/stories/stylings/15_tick_label.tsx index 924904edd5..cd6d2a3e8b 100644 --- a/stories/stylings/15_tick_label.tsx +++ b/stories/stylings/15_tick_label.tsx @@ -39,7 +39,7 @@ function range(title: string, min: number, max: number, value: number, groupId?: export const Example = () => { const theme: PartialTheme = { axes: { - tickLabelStyle: { + tickLabel: { fill: color('tickFill', '#333', 'Tick Label'), fontSize: range('tickFontSize', 0, 40, 10, 'Tick Label'), fontFamily: '\'Open Sans\', Helvetica, Arial, sans-serif', diff --git a/stories/stylings/3_axis.tsx b/stories/stylings/3_axis.tsx index e9678fdfb9..d4ad13d890 100644 --- a/stories/stylings/3_axis.tsx +++ b/stories/stylings/3_axis.tsx @@ -39,25 +39,25 @@ function range(title: string, min: number, max: number, value: number, groupId?: export const Example = () => { const theme: PartialTheme = { axes: { - axisTitleStyle: { + axisTitle: { 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: { + axisLine: { stroke: color('axisLinecolor', '#333', 'Axis Line'), strokeWidth: range('axisLineWidth', 0, 5, 1, 'Axis Line'), }, - tickLabelStyle: { + tickLabel: { fill: color('tickFill', '#333', 'Tick Label'), fontSize: range('tickFontSize', 0, 40, 10, 'Tick Label'), fontFamily: '\'Open Sans\', Helvetica, Arial, sans-serif', fontStyle: 'normal', padding: number('tickLabelPadding', 1, {}, 'Tick Label'), }, - tickLineStyle: { + tickLine: { visible: boolean('showTicks', true, 'Tick Line'), stroke: color('tickLineColor', '#333', 'Tick Line'), strokeWidth: range('tickLineWidth', 0, 5, 1, 'Tick Line'), diff --git a/stories/utils/knobs.ts b/stories/utils/knobs.ts index ce80a2c3cc..fa460b13bf 100644 --- a/stories/utils/knobs.ts +++ b/stories/utils/knobs.ts @@ -17,10 +17,17 @@ * under the License. */ -import { select, array, optionsKnob } from '@storybook/addon-knobs'; +import { select, array, number, optionsKnob } from '@storybook/addon-knobs'; import { Rotation, Position, Placement, TooltipProps } from '../../src'; import { TooltipType } from '../../src/specs/constants'; +import { VerticalAlignment, HorizontalAlignment } from '../../src/utils/commons'; + +export const getPositiveNumberKnob = ( + name: string, + value: number, + groupId?: string, +) => number(name, value, { min: 0 }, groupId); export const numberSelect = ( name: string, @@ -146,3 +153,33 @@ export const getBoundaryKnob = () => select( }, undefined, ); + +export const getVerticalTextAlignmentKnob = (group?: string) => + select( + 'Vertical Text alignment', + { + None: undefined, + Middle: VerticalAlignment.Middle, + Top: VerticalAlignment.Top, + Bottom: VerticalAlignment.Bottom, + Near: VerticalAlignment.Near, + Far: VerticalAlignment.Far, + }, + undefined, + group, + ) || undefined; + +export const getHorizontalTextAlignmentKnob = (group?: string) => + select( + 'Horizontal Text alignment', + { + None: undefined, + Center: HorizontalAlignment.Center, + Left: HorizontalAlignment.Left, + Right: HorizontalAlignment.Right, + Near: HorizontalAlignment.Near, + Far: HorizontalAlignment.Far, + }, + undefined, + group, + ) || undefined;