diff --git a/.gitignore b/.gitignore index 53a06febdd..36759fa78d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ test/failure-screenshots/**/*.png reports/ tmp/ +.temp/ dist/ coverage/ .out/ diff --git a/.playground/index.html b/.playground/index.html index fe305c8bcd..3233ed3dcc 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -2,44 +2,22 @@ - Charts Playground + Elastic-Charts Playground - Document diff --git a/.playground/playground.tsx b/.playground/playground.tsx index df9ed05a59..50ee0cf18d 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -17,14 +17,9 @@ * under the License. */ import React from 'react'; -import { XYChartElementEvent, PartitionElementEvent } from '../src'; import { example } from '../stories/treemap/6_custom_style'; export class Playground extends React.Component { - onElementClick = (elements: (XYChartElementEvent | PartitionElementEvent)[]) => { - // eslint-disable-next-line no-console - console.log(elements); - }; render() { return (
diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-bar-chart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-bar-chart-visually-looks-correct-1-snap.png index 63da9acf86..4cba31aca5 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-bar-chart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-bar-chart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-line-chart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-line-chart-visually-looks-correct-1-snap.png index 6446828588..d9470c9625 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-line-chart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-linear-line-chart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-ordinal-bar-chart-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-ordinal-bar-chart-visually-looks-correct-1-snap.png index 3a79d31379..33bfc727be 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-ordinal-bar-chart-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-ordinal-bar-chart-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-tooltip-visibility-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-tooltip-visibility-visually-looks-correct-1-snap.png index 63da9acf86..a62a0bedd0 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-tooltip-visibility-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-annotations-rects-tooltip-visibility-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png index 2e045a44d3..cd398778ae 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-histogram-mode-linear-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png index 2e045a44d3..cd398778ae 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-0-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png index 8b7d6c19dd..44fde0aa28 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-180-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png index 70375cac0c..c662e46d59 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png index 25a434a410..5a91e0f34d 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-false-rotation-negative-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-0-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-0-1-snap.png index 02a43108d9..ae5a133d8c 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-0-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-0-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-180-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-180-1-snap.png index 142c764920..58ada2c3de 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-180-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-180-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png index a3f4a5370e..c1ed6c452e 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png index e55d1d04f9..78deaa6b9d 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-enable-histogram-mode-is-true-rotation-negative-90-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png index 77aabe9da9..547ec98609 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-center-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png index c2946e9e94..9a87134d13 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-end-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png index 9203dc5900..753571197f 100644 Binary files a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-1-snap.png differ diff --git a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx index cbfd81d4b9..1a468a794f 100644 --- a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx +++ b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx @@ -23,9 +23,10 @@ import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { Dimensions } from '../../../utils/dimensions'; import { GroupId } from '../../../utils/ids'; import { Scale, ScaleType, ScaleContinuous } from '../../../scales'; -import { computeLineAnnotationDimensions, AnnotationLineProps } from './line_annotation_tooltip'; -import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; +import { computeLineAnnotationDimensions } from './line/dimensions'; +import { AnnotationLineProps } from './line/types'; +import { ChartTypes } from '../..'; describe('annotation marker', () => { const groupId = 'foo-group'; @@ -75,7 +76,6 @@ describe('annotation marker', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -130,7 +130,6 @@ describe('annotation marker', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -184,7 +183,6 @@ describe('annotation marker', () => { yScales, xScale, Position.Bottom, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ diff --git a/src/chart_types/xy_chart/annotations/annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/annotation_tooltip.ts deleted file mode 100644 index 7ee3f9946f..0000000000 --- a/src/chart_types/xy_chart/annotations/annotation_tooltip.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { Dimensions } from '../../../utils/dimensions'; -import { Position } from '../../../utils/commons'; - -/** @internal */ -export function getFinalAnnotationTooltipPosition( - /** the dimensions of the chart parent container */ - container: Dimensions, - chartDimensions: Dimensions, - /** the dimensions of the tooltip container */ - tooltip: Dimensions, - /** the tooltip computed position not adjusted within chart bounds */ - tooltipAnchor: { top: number; left: number }, - /** the width of the tooltip portal container */ - portalWidth: number, - padding = 10, -): { - left: string | null; - top: string | null; - anchor: 'left' | 'right'; -} { - let left = 0; - let anchor: Position = Position.Left; - - const annotationXOffset = window.pageXOffset + container.left + chartDimensions.left + tooltipAnchor.left; - if (chartDimensions.left + tooltipAnchor.left + portalWidth + padding >= container.width) { - left = annotationXOffset - portalWidth - padding; - anchor = Position.Right; - } else { - left = annotationXOffset + padding; - } - let top = window.pageYOffset + container.top + chartDimensions.top + tooltipAnchor.top; - if (chartDimensions.top + tooltipAnchor.top + tooltip.height + padding >= container.height) { - top -= tooltip.height + padding; - } else { - top += padding; - } - - return { - left: `${Math.round(left)}px`, - top: `${Math.round(top)}px`, - anchor, - }; -} diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.ts b/src/chart_types/xy_chart/annotations/annotation_utils.ts deleted file mode 100644 index 8cf152eeae..0000000000 --- a/src/chart_types/xy_chart/annotations/annotation_utils.ts +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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 { - AnnotationDomainType, - AnnotationDomainTypes, - AnnotationSpec, - AnnotationType, - AxisSpec, - HistogramModeAlignments, - isLineAnnotation, - isRectAnnotation, -} from '../utils/specs'; -import { Dimensions } from '../../../utils/dimensions'; -import { AnnotationId, GroupId } from '../../../utils/ids'; -import { Scale, ScaleType } from '../../../scales'; -import { computeXScaleOffset, getAxesSpecForSpecId, isHorizontalRotation, getSpecsById } from '../state/utils'; -import { Point } from '../../../utils/point'; -import { - computeLineAnnotationTooltipState, - AnnotationLineProps, - computeLineAnnotationDimensions, -} from './line_annotation_tooltip'; -import { - computeRectAnnotationTooltipState, - AnnotationRectProps, - computeRectAnnotationDimensions, -} from './rect_annotation_tooltip'; -import { Rotation, Position, Color } from '../../../utils/commons'; - -export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; - -/** @internal */ -export type AnnotationTooltipState = AnnotationTooltipVisibleState | AnnotationTooltipHiddenState; - -/** @internal */ -export interface AnnotationTooltipVisibleState { - isVisible: true; - annotationType: AnnotationType; - header?: string; - details?: string; - anchor: { position?: Position; top: number; left: number }; - renderTooltip?: AnnotationTooltipFormatter; -} - -/** @internal */ -export interface AnnotationTooltipHiddenState { - isVisible: false; -} -/** - * The header and description strings for an Annotation - */ -export interface AnnotationDetails { - headerText?: string; - detailsText?: string; -} - -/** - * The marker for an Annotation. Usually a JSX element - */ -export interface AnnotationMarker { - icon: JSX.Element; - position: { top: number; left: number }; - dimension: { width: number; height: number }; - color: Color; -} - -/** @internal */ -export type AnnotationDimensions = AnnotationLineProps[] | AnnotationRectProps[]; - -/** @internal */ -export type Bounds = { startX: number; endX: number; startY: number; endY: number }; - -/** @internal */ -export function scaleAndValidateDatum(dataValue: any, scale: Scale, alignWithTick: boolean): number | null { - const isContinuous = scale.type !== ScaleType.Ordinal; - const scaledValue = scale.scale(dataValue); - // d3.scale will return 0 for '', rendering the line incorrectly at 0 - if (scaledValue === null || (isContinuous && dataValue === '')) { - return null; - } - - if (isContinuous) { - const [domainStart, domainEnd] = scale.domain; - - // if we're not aligning the ticks, we need to extend the domain by one more tick for histograms - const domainEndOffset = alignWithTick ? 0 : scale.minInterval; - - if (domainStart > dataValue || domainEnd + domainEndOffset < dataValue) { - return null; - } - } - - return scaledValue; -} - -/** @internal */ -export function getAnnotationAxis( - axesSpecs: AxisSpec[], - groupId: GroupId, - domainType: AnnotationDomainType, - chartRotation: Rotation, -): Position | null { - const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); - const isHorizontalRotated = isHorizontalRotation(chartRotation); - const isXDomainAnnotation = isXDomain(domainType); - const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; - const rotatedAnnotation = isHorizontalRotated ? annotationAxis : isXDomainAnnotation ? yAxis : xAxis; - return rotatedAnnotation ? rotatedAnnotation.position : null; -} - -/** @internal */ -export function computeClusterOffset(totalBarsInCluster: number, barsShift: number, bandwidth: number): number { - if (totalBarsInCluster > 1) { - return barsShift - bandwidth / 2; - } - - return 0; -} - -/** @internal */ -export function isXDomain(domainType: AnnotationDomainType): boolean { - return domainType === AnnotationDomainTypes.XDomain; -} - -/** @internal */ -export function getRotatedCursor( - /** the cursor position relative to the projection area */ - cursorPosition: Point, - chartDimensions: Dimensions, - chartRotation: Rotation, -): Point { - const { x, y } = cursorPosition; - const { height, width } = chartDimensions; - switch (chartRotation) { - case 0: - return { x, y }; - case 90: - return { x: y, y: width - x }; - case -90: - return { x: height - y, y: x }; - case 180: - return { x: width - x, y: height - y }; - } -} - -/** @internal */ -export function computeAnnotationDimensions( - annotations: AnnotationSpec[], - chartDimensions: Dimensions, - chartRotation: Rotation, - yScales: Map, - xScale: Scale, - axesSpecs: AxisSpec[], - totalBarsInCluster: number, - enableHistogramMode: boolean, -): Map { - const annotationDimensions = new Map(); - - const barsShift = (totalBarsInCluster * xScale.bandwidth) / 2; - - const band = xScale.bandwidth / (1 - xScale.barsPadding); - const halfPadding = (band - xScale.bandwidth) / 2; - const barsPadding = halfPadding * totalBarsInCluster; - const clusterOffset = computeClusterOffset(totalBarsInCluster, barsShift, xScale.bandwidth); - - // Annotations should always align with the axis line in histogram mode - const xScaleOffset = computeXScaleOffset(xScale, enableHistogramMode, HistogramModeAlignments.Start); - annotations.forEach((annotationSpec) => { - const { id } = annotationSpec; - if (isLineAnnotation(annotationSpec)) { - const { groupId, domainType } = annotationSpec; - const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType, chartRotation); - - if (!annotationAxisPosition) { - return; - } - const dimensions = computeLineAnnotationDimensions( - annotationSpec, - chartDimensions, - chartRotation, - yScales, - xScale, - annotationAxisPosition, - xScaleOffset - clusterOffset, - enableHistogramMode, - ); - - if (dimensions) { - annotationDimensions.set(id, dimensions); - } - } else if (isRectAnnotation(annotationSpec)) { - const dimensions = computeRectAnnotationDimensions( - annotationSpec, - yScales, - xScale, - enableHistogramMode, - barsPadding, - ); - - if (dimensions) { - annotationDimensions.set(id, dimensions); - } - } - }); - - return annotationDimensions; -} - -/** @internal */ -export function computeAnnotationTooltipState( - cursorPosition: Point, - annotationDimensions: Map, - annotationSpecs: AnnotationSpec[], - chartRotation: Rotation, - axesSpecs: AxisSpec[], - chartDimensions: Dimensions, -): AnnotationTooltipState | null { - for (const [annotationId, annotationDimension] of annotationDimensions) { - const spec = getSpecsById(annotationSpecs, annotationId); - - if (!spec || spec.hideTooltips) { - continue; - } - - const groupId = spec.groupId; - - if (isLineAnnotation(spec)) { - if (spec.hideLines) { - continue; - } - const lineAnnotationTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationDimension as AnnotationLineProps[], - groupId, - spec.domainType, - axesSpecs, - ); - - if (lineAnnotationTooltipState.isVisible) { - return lineAnnotationTooltipState; - } - } else if (isRectAnnotation(spec)) { - const rectAnnotationTooltipState = computeRectAnnotationTooltipState( - cursorPosition, - annotationDimension as AnnotationRectProps[], - chartRotation, - chartDimensions, - spec.renderTooltip, - ); - - if (rectAnnotationTooltipState.isVisible) { - return rectAnnotationTooltipState; - } - } - } - - return null; -} diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts b/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts new file mode 100644 index 0000000000..fbe6622adc --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/dimensions.integration.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { MockStore } from '../../../../mocks/store'; +import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { ScaleType } from '../../../../scales'; +import { AnnotationDomainTypes } from '../../utils/specs'; +import { Position } from '../../../../utils/commons'; + +function expectAnnotationAtPosition( + data: Array<{ x: number; y: number }>, + type: 'line' | 'bar', + indexPosition: number, + expectedLinePosition: number, + numOfSpecs = 1, + xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, +) { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const specs = new Array(numOfSpecs).fill(0).map((d, i) => { + return MockSeriesSpec.byTypePartial(type)({ + id: `spec_${i}`, + xScaleType, + data, + }); + }); + const annotation = MockAnnotationSpec.line({ + dataValues: [ + { + dataValue: indexPosition, + }, + ], + }); + + MockStore.addSpecs([settings, ...specs, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + expect(annotations.get(annotation.id)).toEqual([ + { + anchor: { left: expectedLinePosition, position: 'bottom', top: 100 }, + details: { detailsText: undefined, headerText: `${indexPosition}` }, + linePathPoints: { + start: { x1: expectedLinePosition, y1: 100 }, + end: { x2: expectedLinePosition, y2: 0 }, + }, + marker: undefined, + }, + ]); +} + +describe('Render vertical line annotation within', () => { + it.each([ + [0, 1, 12.5], // middle of 1st bar + [1, 1, 37.5], // middle of 2nd bar + [2, 1, 62.5], // middle of 3rd bar + [3, 1, 87.5], // middle of 4th bar + [1, 2, 37.5], // middle of 2nd bar + [1, 3, 37.5], // middle of 2nd bar + ])('a bar at position %i, %i specs, all scales', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValue, linePosition, numOfSpecs, ScaleType.Time); + }); + + it.each([ + [0, 1, 0], // the start of the chart + [1, 1, 50], // the middle of the chart + [2, 1, 100], // the end of the chart + [1, 2, 50], // the middle of the chart + [1, 3, 50], // the middle of the chart + ])('line point at position %i, %i specs, linear scale', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 1 }, + { x: 1, y: 1 }, + { x: 2, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'line', dataValue, linePosition, numOfSpecs); + }); + + it.each([ + [0, 1, 12.5], // 1st ordinal line point + [1, 1, 37.5], // 2nd ordinal line point + [2, 1, 62.5], // 3rd ordinal line point + [3, 1, 87.5], // 4th ordinal line point + [1, 2, 37.5], // 2nd ordinal line point + [1, 3, 37.5], // 2nd ordinal line point + ])('line point at position %i, %i specs, Ordinal scale', (dataValue, numOfSpecs, linePosition) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + expectAnnotationAtPosition(data, 'line', dataValue, linePosition, numOfSpecs, ScaleType.Ordinal); + }); + + it('histogramMode with line after the max value but before the max + minInterval ', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins({ + xDomain: { + min: 0, + max: 9, + minInterval: 1, + }, + }); + const spec = MockSeriesSpec.histogramBar({ + xScaleType: ScaleType.Linear, + data: [ + { + x: 0, + y: 1, + }, + { + x: 9, + y: 20, + }, + ], + }); + const annotation = MockAnnotationSpec.line({ + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 9.5, details: 'foo' }], + }); + + MockStore.addSpecs([settings, spec, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + expect(annotations.get(annotation.id)).toEqual([ + { + anchor: { + top: 100, + left: 95, + position: Position.Bottom, + }, + linePathPoints: { + start: { x1: 95, y1: 100 }, + end: { x2: 95, y2: 0 }, + }, + details: { detailsText: 'foo', headerText: '9.5' }, + marker: undefined, + }, + ]); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/line/dimensions.ts similarity index 62% rename from src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts rename to src/chart_types/xy_chart/annotations/line/dimensions.ts index e9c72a1b35..28f56de25a 100644 --- a/src/chart_types/xy_chart/annotations/line_annotation_tooltip.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.ts @@ -16,67 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { - AnnotationDomainType, - AnnotationDomainTypes, - AnnotationTypes, - LineAnnotationSpec, - LineAnnotationDatum, - AxisSpec, -} from '../utils/specs'; -import { Position, Rotation } from '../../../utils/commons'; -import { - AnnotationTooltipState, - AnnotationDetails, - AnnotationMarker, - scaleAndValidateDatum, - isXDomain, - Bounds, -} from './annotation_utils'; -import { isHorizontalRotation, getAxesSpecForSpecId } from '../state/utils'; -import { isHorizontalAxis } from '../utils/axis_utils'; -import { Dimensions } from '../../../utils/dimensions'; -import { Scale } from '../../../scales'; -import { GroupId } from '../../../utils/ids'; -import { LineAnnotationStyle } from '../../../utils/themes/theme'; -import { Point } from '../../../utils/point'; -import { isWithinRectBounds } from './rect_annotation_tooltip'; - -/** @internal */ -export type AnnotationLinePosition = [number, number, number, number]; - -/** - * Start and end points of a line annotation - * @internal - */ -export interface AnnotationLinePathPoints { - /** x1,y1 the start point anchored to the linked axis */ - start: { - x1: number; - y1: number; - }; - /** x2,y2 the end point */ - end: { - x2: number; - y2: number; - }; -} - -/** @internal */ -export interface AnnotationLineProps { - /** the position of the start point relative to the Chart */ - anchor: { - position: Position; - top: number; - left: number; - }; - /** - * The path points of a line annotation - */ - linePathPoints: AnnotationLinePathPoints; - details: AnnotationDetails; - marker?: AnnotationMarker; -} +import { AnnotationDomainTypes, LineAnnotationSpec, LineAnnotationDatum } from '../../utils/specs'; +import { Position, Rotation } from '../../../../utils/commons'; +import { AnnotationMarker } from '../types'; +import { isHorizontalRotation } from '../../state/utils'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Scale } from '../../../../scales'; +import { GroupId } from '../../../../utils/ids'; +import { AnnotationLineProps, AnnotationLinePathPoints } from './types'; +import { isContinuousScale, isBandScale } from '../../../../scales/types'; +import { computeXScaleOffset } from '../../state/utils'; /** @internal */ export const DEFAULT_LINE_OVERFLOW = 0; @@ -85,7 +34,7 @@ function computeYDomainLineAnnotationDimensions( dataValues: LineAnnotationDatum[], yScale: Scale, chartRotation: Rotation, - axisPosition: Position, + axisPosition: Position | null, chartDimensions: Dimensions, lineColor: string, marker?: JSX.Element, @@ -94,7 +43,10 @@ function computeYDomainLineAnnotationDimensions( const chartHeight = chartDimensions.height; const chartWidth = chartDimensions.width; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); - + // let's use a default Bottom-X/Left-Y axis orientation if we are not showing an axis + // but we are displaying a line annotation + const anchorPosition = + axisPosition === null ? (isHorizontalChartRotation ? Position.Left : Position.Bottom) : axisPosition; const lineProps: AnnotationLineProps[] = []; dataValues.forEach((datum: LineAnnotationDatum) => { @@ -113,11 +65,12 @@ function computeYDomainLineAnnotationDimensions( const [domainStart, domainEnd] = yScale.domain; // avoid rendering annotation with values outside the scale domain - if (domainStart > dataValue || domainEnd < dataValue) { + if (dataValue < domainStart || dataValue > domainEnd) { return; } + const anchor = { - position: axisPosition, + position: anchorPosition, top: 0, left: 0, }; @@ -129,7 +82,7 @@ function computeYDomainLineAnnotationDimensions( // the Y axis is vertical, X axis is horizontal y|--x--|y if (isHorizontalChartRotation) { // y|__x__ - if (axisPosition === Position.Left) { + if (anchorPosition === Position.Left) { anchor.left = 0; markerPosition.left = -markerDimension.width; linePathPoints.start.x1 = 0; @@ -155,7 +108,7 @@ function computeYDomainLineAnnotationDimensions( // the Y axis is horizontal, X axis is vertical x|--y--|x } else { // ¯¯y¯¯ - if (axisPosition === Position.Top) { + if (anchorPosition === Position.Top) { anchor.top = 0; markerPosition.top = -markerDimension.height; linePathPoints.start.x1 = 0; @@ -208,11 +161,10 @@ function computeXDomainLineAnnotationDimensions( dataValues: LineAnnotationDatum[], xScale: Scale, chartRotation: Rotation, - axisPosition: Position, + axisPosition: Position | null, chartDimensions: Dimensions, lineColor: string, - xScaleOffset: number, - enableHistogramMode: boolean, + isHistogramMode: boolean, marker?: JSX.Element, markerDimension = { width: 0, height: 0 }, ): AnnotationLineProps[] { @@ -220,19 +172,46 @@ function computeXDomainLineAnnotationDimensions( const chartWidth = chartDimensions.width; const lineProps: AnnotationLineProps[] = []; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); + // let's use a default Bottom-X/Left-Y axis orientation if we are not showing an axis + // but we are displaying a line annotation + const anchorPosition = + axisPosition === null ? (isHorizontalChartRotation ? Position.Bottom : Position.Left) : axisPosition; - const alignWithTick = xScale.bandwidth > 0 && !enableHistogramMode; dataValues.forEach((datum: LineAnnotationDatum) => { const { dataValue } = datum; - - const scaledXValue = scaleAndValidateDatum(dataValue, xScale, alignWithTick); - - if (scaledXValue == null) { + let annotationValueXposition = xScale.scale(dataValue); + if (annotationValueXposition == null) { + return; + } + if (isContinuousScale(xScale) && typeof dataValue === 'number') { + const minDomain = xScale.domain[0]; + const maxDomain = isHistogramMode ? xScale.domain[1] + xScale.minInterval : xScale.domain[1]; + if (dataValue < minDomain || dataValue > maxDomain) { + return; + } + if (isHistogramMode) { + const offset = computeXScaleOffset(xScale, true); + const pureScaledValue = xScale.pureScale(dataValue); + if (pureScaledValue == null) { + return; + } + annotationValueXposition = pureScaledValue - offset; + } else { + annotationValueXposition = annotationValueXposition + (xScale.bandwidth * xScale.totalBarsInCluster) / 2; + } + } else if (isBandScale(xScale)) { + if (isHistogramMode) { + const padding = (xScale.step - xScale.originalBandwidth) / 2; + annotationValueXposition = annotationValueXposition - padding; + } else { + annotationValueXposition = annotationValueXposition + xScale.originalBandwidth / 2; + } + } else { + return; + } + if (isNaN(annotationValueXposition) || annotationValueXposition == null) { return; } - - const offset = xScale.bandwidth / 2 - xScaleOffset; - const annotationValueXposition = scaledXValue + offset; const markerPosition = { top: 0, left: 0 }; const linePathPoints: AnnotationLinePathPoints = { @@ -240,14 +219,14 @@ function computeXDomainLineAnnotationDimensions( end: { x2: 0, y2: 0 }, }; const anchor = { - position: axisPosition, + position: anchorPosition, top: 0, left: 0, }; // the Y axis is vertical, X axis is horizontal y|--x--|y if (isHorizontalChartRotation) { // __x__ - if (axisPosition === Position.Bottom) { + if (anchorPosition === Position.Bottom) { linePathPoints.start.y1 = chartHeight; linePathPoints.end.y2 = 0; anchor.top = chartHeight; @@ -273,7 +252,7 @@ function computeXDomainLineAnnotationDimensions( // the Y axis is horizontal, X axis is vertical x|--y--|x } else { // x|--y-- - if (axisPosition === Position.Left) { + if (anchorPosition === Position.Left) { anchor.left = 0; markerPosition.left = -markerDimension.width; linePathPoints.start.x1 = annotationValueXposition; @@ -330,9 +309,8 @@ export function computeLineAnnotationDimensions( chartRotation: Rotation, yScales: Map, xScale: Scale, - axisPosition: Position, - xScaleOffset: number, - enableHistogramMode: boolean, + axisPosition: Position | null, + isHistogramMode: boolean, ): AnnotationLineProps[] | null { const { domainType, dataValues, marker, markerDimensions, hideLines } = annotationSpec; @@ -341,8 +319,8 @@ export function computeLineAnnotationDimensions( } // this type is guaranteed as this has been merged with default - const lineStyle = annotationSpec.style as LineAnnotationStyle; - const lineColor = lineStyle.line.stroke; + const lineStyle = annotationSpec.style; + const lineColor = lineStyle?.line?.stroke ?? 'red'; if (domainType === AnnotationDomainTypes.XDomain) { return computeXDomainLineAnnotationDimensions( @@ -352,8 +330,7 @@ export function computeLineAnnotationDimensions( axisPosition, chartDimensions, lineColor, - xScaleOffset, - enableHistogramMode, + isHistogramMode, marker, markerDimensions, ); @@ -376,100 +353,3 @@ export function computeLineAnnotationDimensions( markerDimensions, ); } - -/** @internal */ -export function getAnnotationLineTooltipXOffset(chartRotation: Rotation, axisPosition: Position): number { - let xOffset = 0; - const isChartHorizontalRotation = isHorizontalRotation(chartRotation); - - if (isHorizontalAxis(axisPosition)) { - xOffset = isChartHorizontalRotation ? 50 : 0; - } else { - if (isChartHorizontalRotation) { - xOffset = axisPosition === Position.Right ? 100 : 0; - } else { - xOffset = 50; - } - } - - return xOffset; -} - -/** @internal */ -export function getAnnotationLineTooltipYOffset(chartRotation: Rotation, axisPosition: Position): number { - let yOffset = 0; - const isChartHorizontalRotation = isHorizontalRotation(chartRotation); - - if (isHorizontalAxis(axisPosition)) { - if (isChartHorizontalRotation) { - yOffset = axisPosition === Position.Top ? 0 : 100; - } else { - yOffset = 50; - } - } else { - yOffset = isChartHorizontalRotation ? 50 : 100; - } - - return yOffset; -} - -/** @internal */ -export function isVerticalAnnotationLine(isXDomainAnnotation: boolean, isHorizontalChartRotation: boolean): boolean { - if (isXDomainAnnotation) { - return isHorizontalChartRotation; - } - - return !isHorizontalChartRotation; -} - -/** - * Checks if the cursorPosition is within the line annotation marker - * @param cursorPosition the cursor position relative to the projected area - * @param marker the line annotation marker - */ -function isWithinLineMarkerBounds(cursorPosition: Point, marker: AnnotationMarker): boolean { - const { top, left } = marker.position; - const { width, height } = marker.dimension; - const markerRect: Bounds = { startX: left, startY: top, endX: left + width, endY: top + height }; - return isWithinRectBounds(cursorPosition, markerRect); -} - -/** @internal */ -export function computeLineAnnotationTooltipState( - cursorPosition: Point, - annotationLines: AnnotationLineProps[], - groupId: GroupId, - domainType: AnnotationDomainType, - axesSpecs: AxisSpec[], -): AnnotationTooltipState { - const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); - const isXDomainAnnotation = isXDomain(domainType); - const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; - - if (!annotationAxis) { - return { - isVisible: false, - }; - } - - const totalAnnotationLines = annotationLines.length; - for (let i = 0; i < totalAnnotationLines; i++) { - const line = annotationLines[i]; - const isWithinBounds = line.marker && isWithinLineMarkerBounds(cursorPosition, line.marker); - - if (isWithinBounds) { - return { - annotationType: AnnotationTypes.Line, - isVisible: true, - anchor: { - ...line.anchor, - }, - ...(line.details && { header: line.details.headerText }), - ...(line.details && { details: line.details.detailsText }), - }; - } - } - return { - isVisible: false, - }; -} diff --git a/src/chart_types/xy_chart/annotations/line/marker.test.tsx b/src/chart_types/xy_chart/annotations/line/marker.test.tsx new file mode 100644 index 0000000000..5ba827f24a --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/marker.test.tsx @@ -0,0 +1,216 @@ +/* + * 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 React from 'react'; +import { AnnotationDomainTypes, AnnotationSpec, AnnotationTypes } from '../../utils/specs'; +import { Position, Rotation } from '../../../../utils/commons'; +import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../../utils/themes/theme'; +import { Dimensions } from '../../../../utils/dimensions'; +import { GroupId } from '../../../../utils/ids'; +import { Scale, ScaleType, ScaleContinuous } from '../../../../scales'; +import { computeLineAnnotationDimensions } from './dimensions'; +import { AnnotationLineProps } from './types'; +import { ChartTypes } from '../../..'; +import { SpecTypes } from '../../../../specs/settings'; + +describe('annotation marker', () => { + const groupId = 'foo-group'; + + const minRange = 0; + const maxRange = 100; + + const continuousData = [0, 10]; + const continuousScale = new ScaleContinuous({ + type: ScaleType.Linear, + domain: continuousData, + range: [minRange, maxRange], + }); + + const chartDimensions: Dimensions = { + width: 10, + height: 20, + top: 5, + left: 15, + }; + + const yScales: Map = new Map(); + yScales.set(groupId, continuousScale); + + const xScale: Scale = continuousScale; + + test('should compute line annotation dimensions with marker if defined (y domain)', () => { + const chartRotation: Rotation = 0; + + const id = 'foo-line'; + const lineAnnotation: AnnotationSpec = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Line, + id, + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + marker:
, + }; + + const dimensions = computeLineAnnotationDimensions( + lineAnnotation, + chartDimensions, + chartRotation, + yScales, + xScale, + Position.Left, + false, + ); + const expectedDimensions: AnnotationLineProps[] = [ + { + anchor: { + position: Position.Left, + top: 20, + left: 0, + }, + linePathPoints: { + start: { + x1: 0, + y1: 20, + }, + end: { + x2: 10, + y2: 20, + }, + }, + details: { detailsText: 'foo', headerText: '2' }, + + marker: { + icon:
, + color: '#777', + dimension: { width: 0, height: 0 }, + position: { left: -0, top: 20 }, + }, + }, + ]; + expect(dimensions).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions with marker if defined (y domain: 180 deg rotation)', () => { + const chartRotation: Rotation = 180; + + const lineAnnotation: AnnotationSpec = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Line, + id: 'foo-line', + domainType: AnnotationDomainTypes.YDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + marker:
, + }; + + const dimensions = computeLineAnnotationDimensions( + lineAnnotation, + chartDimensions, + chartRotation, + yScales, + xScale, + Position.Left, + false, + ); + const expectedDimensions: AnnotationLineProps[] = [ + { + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, + linePathPoints: { + start: { + x1: 0, + y1: 20, + }, + end: { + x2: 10, + y2: 20, + }, + }, + details: { detailsText: 'foo', headerText: '2' }, + marker: { + icon:
, + color: '#777', + dimension: { width: 0, height: 0 }, + position: { left: -0, top: 0 }, + }, + }, + ]; + expect(dimensions).toEqual(expectedDimensions); + }); + + test('should compute line annotation dimensions with marker if defined (x domain)', () => { + const chartRotation: Rotation = 0; + + const lineAnnotation: AnnotationSpec = { + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Line, + id: 'foo-line', + domainType: AnnotationDomainTypes.XDomain, + dataValues: [{ dataValue: 2, details: 'foo' }], + groupId, + style: DEFAULT_ANNOTATION_LINE_STYLE, + marker:
, + }; + + const dimensions = computeLineAnnotationDimensions( + lineAnnotation, + chartDimensions, + chartRotation, + yScales, + xScale, + Position.Bottom, + false, + ); + const expectedDimensions: AnnotationLineProps[] = [ + { + anchor: { + position: Position.Bottom, + top: 20, + left: 20, + }, + details: { detailsText: 'foo', headerText: '2' }, + linePathPoints: { + start: { + x1: 20, + y1: 20, + }, + end: { + x2: 20, + y2: 0, + }, + }, + marker: { + icon:
, + color: '#777', + dimension: { width: 0, height: 0 }, + position: { top: 20, left: 20 }, + }, + }, + ]; + expect(dimensions).toEqual(expectedDimensions); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/line/tooltip.ts b/src/chart_types/xy_chart/annotations/line/tooltip.ts new file mode 100644 index 0000000000..e91fb5c123 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/tooltip.ts @@ -0,0 +1,78 @@ +/* + * 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 { AnnotationLineProps } from './types'; +import { isWithinRectBounds } from '../rect/dimensions'; +import { isXDomain } from '../utils'; +import { AnnotationTooltipState, AnnotationMarker, Bounds } from '../types'; +import { getAxesSpecForSpecId } from '../../state/utils'; +import { AnnotationDomainType, AnnotationTypes, AxisSpec } from '../../utils/specs'; +import { GroupId } from '../../../../utils/ids'; +import { Point } from '../../../../utils/point'; + +/** @internal */ +export function computeLineAnnotationTooltipState( + cursorPosition: Point, + annotationLines: AnnotationLineProps[], + groupId: GroupId, + domainType: AnnotationDomainType, + axesSpecs: AxisSpec[], +): AnnotationTooltipState { + const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); + const isXDomainAnnotation = isXDomain(domainType); + const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; + + if (!annotationAxis) { + return { + isVisible: false, + }; + } + + const totalAnnotationLines = annotationLines.length; + for (let i = 0; i < totalAnnotationLines; i++) { + const line = annotationLines[i]; + const isWithinBounds = line.marker && isWithinLineMarkerBounds(cursorPosition, line.marker); + + if (isWithinBounds) { + return { + annotationType: AnnotationTypes.Line, + isVisible: true, + anchor: { + ...line.anchor, + }, + ...(line.details && { header: line.details.headerText }), + ...(line.details && { details: line.details.detailsText }), + }; + } + } + return { + isVisible: false, + }; +} + +/** + * Checks if the cursorPosition is within the line annotation marker + * @param cursorPosition the cursor position relative to the projected area + * @param marker the line annotation marker + */ +function isWithinLineMarkerBounds(cursorPosition: Point, marker: AnnotationMarker): boolean { + const { top, left } = marker.position; + const { width, height } = marker.dimension; + const markerRect: Bounds = { startX: left, startY: top, endX: left + width, endY: top + height }; + return isWithinRectBounds(cursorPosition, markerRect); +} diff --git a/src/chart_types/xy_chart/annotations/line/types.ts b/src/chart_types/xy_chart/annotations/line/types.ts new file mode 100644 index 0000000000..1955d57aad --- /dev/null +++ b/src/chart_types/xy_chart/annotations/line/types.ts @@ -0,0 +1,53 @@ +/* + * 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 { Position } from '../../../../utils/commons'; +import { AnnotationDetails, AnnotationMarker } from '../types'; + +/** + * Start and end points of a line annotation + * @internal + */ +export interface AnnotationLinePathPoints { + /** x1,y1 the start point anchored to the linked axis */ + start: { + x1: number; + y1: number; + }; + /** x2,y2 the end point */ + end: { + x2: number; + y2: number; + }; +} + +/** @internal */ +export interface AnnotationLineProps { + /** the position of the start point relative to the Chart */ + anchor: { + position: Position; + top: number; + left: number; + }; + /** + * The path points of a line annotation + */ + linePathPoints: AnnotationLinePathPoints; + details: AnnotationDetails; + marker?: AnnotationMarker; +} diff --git a/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts b/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts new file mode 100644 index 0000000000..7a2d9dd9c4 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts @@ -0,0 +1,258 @@ +/* + * 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 { MockStore } from '../../../../mocks/store'; +import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../../mocks/specs'; +import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; +import { ScaleType } from '../../../../scales'; +import { RectAnnotationDatum } from '../../utils/specs'; +import { AnnotationRectProps } from './types'; + +function expectAnnotationAtPosition( + data: Array<{ x: number; y: number }>, + type: 'line' | 'bar' | 'histogram', + dataValues: RectAnnotationDatum[], + expectedRect: { + x: number; + y: number; + width: number; + height: number; + }, + numOfSpecs = 1, + xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, +) { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const specs = new Array(numOfSpecs).fill(0).map((d, i) => { + return MockSeriesSpec.byTypePartial(type)({ + id: `spec_${i}`, + xScaleType, + data, + }); + }); + const annotation = MockAnnotationSpec.rect({ + dataValues, + }); + + MockStore.addSpecs([settings, ...specs, annotation], store); + const annotations = computeAnnotationDimensionsSelector(store.getState()); + const renderedAnnotations = annotations.get(annotation.id)!; + expect(renderedAnnotations.length).toBe(1); + const { rect } = renderedAnnotations[0] as AnnotationRectProps; + expect(rect.x).toBeCloseTo(expectedRect.x, 3); + expect(rect.y).toBeCloseTo(expectedRect.y, 3); + expect(rect.width).toBeCloseTo(expectedRect.width, 3); + expect(rect.height).toBeCloseTo(expectedRect.height, 3); +} + +describe('Render rect annotation within', () => { + it.each` + x0 | numOfSpecs | x | width + ${0} | ${1} | ${0} | ${100} + ${1} | ${1} | ${25} | ${75} + ${2} | ${1} | ${50} | ${50} + ${3} | ${1} | ${75} | ${25} + ${1} | ${2} | ${25} | ${75} + ${2} | ${3} | ${50} | ${50} + `('bars starting from $x0, $numOfSpecs specs, all scales', ({ x0, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each` + x1 | numOfSpecs | x | width + ${0} | ${1} | ${0} | ${25} + ${1} | ${1} | ${0} | ${50} + ${2} | ${1} | ${0} | ${75} + ${3} | ${1} | ${0} | ${100} + ${1} | ${2} | ${0} | ${50} + ${2} | ${2} | ${0} | ${75} + `('bars starting ending at $x1, $numOfSpecs specs, all scales', ({ x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${25} + ${0} | ${1} | ${1} | ${0} | ${50} + ${1} | ${3} | ${1} | ${25} | ${75} + ${0} | ${1} | ${2} | ${0} | ${50} + ${1} | ${3} | ${3} | ${25} | ${75} + `('bars starting at $x0, ending at $x1, $numOfSpecs specs, all scales', ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, numOfSpecs, ScaleType.Time); + }); + + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${25} + ${0} | ${1} | ${1} | ${0} | ${50} + ${1} | ${3} | ${1} | ${25} | ${75} + ${0} | ${1} | ${2} | ${0} | ${50} + ${1} | ${3} | ${3} | ${25} | ${75} + `('lines starting at $x0, ending at $x1, $numOfSpecs specs, ordinal scale', ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, numOfSpecs, ScaleType.Ordinal); + }); + + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${0} + ${0} | ${1} | ${1} | ${0} | ${50} + ${1} | ${2} | ${1} | ${50} | ${50} + ${0} | ${2} | ${1} | ${0} | ${100} + ${0} | ${1} | ${2} | ${0} | ${50} + ${1} | ${2} | ${3} | ${50} | ${50} + `( + 'on line starting at $x0, ending at $x1, $numOfSpecs specs, continuous scale', + ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, numOfSpecs, ScaleType.Linear); + }, + ); + it.each` + x0 | x1 | numOfSpecs | x | width + ${0} | ${0} | ${1} | ${0} | ${0} + ${0} | ${1} | ${1} | ${0} | ${25} + ${1} | ${2} | ${1} | ${25} | ${25} + ${0} | ${2} | ${1} | ${0} | ${50} + ${0} | ${1} | ${2} | ${0} | ${25} + ${1} | ${2} | ${3} | ${25} | ${25} + `( + 'on histogram starting at $x0, ending at $x1, $numOfSpecs specs, continuous scale', + ({ x0, x1, numOfSpecs, x, width }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + { x: 3, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { x0, x1 }, + }, + ]; + const rect = { x, width, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'histogram', dataValues, rect, numOfSpecs, ScaleType.Linear); + }, + ); + + it.each` + prop | x | y | width | height + ${'x0'} | ${50} | ${0} | ${50} | ${100} + ${'x1'} | ${0} | ${0} | ${50} | ${100} + ${'y0'} | ${0} | ${0} | ${100} | ${75} + ${'y1'} | ${0} | ${75} | ${100} | ${25} + `('expand annotation with only one prop configured: $prop', ({ prop, x, y, width, height }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 2 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { [prop]: 1 }, + }, + ]; + const rect = { x, width, y, height }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + }); + + it.each` + value | prop + ${10} | ${'y1'} + ${-4} | ${'y0'} + ${-4} | ${'x0'} + ${5} | ${'x1'} + `('out of bound annotations for $prop', ({ prop, value }) => { + const data = [ + { x: 0, y: 4 }, + { x: 1, y: 1 }, + { x: 2, y: 3 }, + ]; + const dataValues: RectAnnotationDatum[] = [ + { + coordinates: { [prop]: value }, + }, + ]; + const rect = { x: 0, width: 100, y: 0, height: 100 }; + expectAnnotationAtPosition(data, 'line', dataValues, rect, 1, ScaleType.Linear); + expectAnnotationAtPosition(data, 'bar', dataValues, rect, 1, ScaleType.Linear); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/rect/dimensions.ts b/src/chart_types/xy_chart/annotations/rect/dimensions.ts new file mode 100644 index 0000000000..2af690dd9c --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect/dimensions.ts @@ -0,0 +1,204 @@ +/* + * 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 { RectAnnotationDatum, RectAnnotationSpec } from '../../utils/specs'; +import { GroupId } from '../../../../utils/ids'; +import { Scale, ScaleBand, ScaleContinuous } from '../../../../scales'; +import { Point } from '../../../../utils/point'; +import { Bounds } from '../types'; +import { AnnotationRectProps } from './types'; +import { isBandScale, isContinuousScale } from '../../../../scales/types'; +import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; + +/** @internal */ +export function isWithinRectBounds({ x, y }: Point, { startX, endX, startY, endY }: Bounds): boolean { + const withinXBounds = x >= startX && x <= endX; + const withinYBounds = y >= startY && y <= endY; + + return withinXBounds && withinYBounds; +} + +/** @internal */ +export function computeRectAnnotationDimensions( + annotationSpec: RectAnnotationSpec, + yScales: Map, + xScale: Scale, + isHistogram: boolean = false, +): AnnotationRectProps[] | null { + const { dataValues } = annotationSpec; + const groupId = annotationSpec.groupId; + const yScale = yScales.get(groupId); + if (!yScale) { + return null; + } + + const rectsProps: AnnotationRectProps[] = []; + dataValues.forEach((dataValue: RectAnnotationDatum) => { + let { x0, x1, y0, y1 } = dataValue.coordinates; + + // if everything is null, return; otherwise we coerce the other coordinates + if (x0 == null && x1 == null && y0 == null && y1 == null) { + return; + } + [x0, x1] = limitValueToDomainRange(xScale, x0, x1, isHistogram); + [y0, y1] = limitValueToDomainRange(yScale, y0, y1); + + // something is wrong with the data types, don't draw this annotation + if (x0 == null || x1 == null || y0 == null || y1 == null) { + return; + } + + let xAndWidth: { x: number; width: number } | null = null; + if (isBandScale(xScale)) { + xAndWidth = scaleXonBandScale(xScale, x0, x1); + } else if (isContinuousScale(xScale)) { + xAndWidth = scaleXonContinuousScale(xScale, x0, x1, isHistogram); + } + + // something is wrong with scales, don't draw + if (!xAndWidth) { + return; + } + const scaledY1 = yScale.pureScale(y1); + const scaledY0 = yScale.pureScale(y0); + if (scaledY1 == null || scaledY0 == null) { + return; + } + const height = Math.abs(scaledY0 - scaledY1); + + const rectDimensions = { + ...xAndWidth, + y: scaledY1, + height, + }; + + rectsProps.push({ + rect: rectDimensions, + details: dataValue.details, + }); + }); + return rectsProps; +} + +function scaleXonBandScale( + xScale: ScaleBand, + x0: PrimitiveValue, + x1: PrimitiveValue, +): { x: number; width: number } | null { + // the band scale return the start of the band, we need to cover + // also the inner padding of the bar + const padding = (xScale.step - xScale.originalBandwidth) / 2; + let scaledX1 = xScale.scale(x1); + let scaledX0 = xScale.scale(x0); + if (scaledX1 == null || scaledX0 == null) { + return null; + } + // extend the x1 scaled value to fully cover the last bar + scaledX1 += xScale.originalBandwidth + padding; + // give the x1 value a maximum of the chart range + if (scaledX1 > xScale.range[1]) { + scaledX1 = xScale.range[1]; + } + + scaledX0 -= padding; + if (scaledX0 < xScale.range[0]) { + scaledX0 = xScale.range[0]; + } + const width = Math.abs(scaledX1 - scaledX0); + return { + x: scaledX0, + width, + }; +} + +function scaleXonContinuousScale( + xScale: ScaleContinuous, + x0: PrimitiveValue, + x1: PrimitiveValue, + isHistogramModeEnabled: boolean = false, +): { x: number; width: number } | null { + if (typeof x1 !== 'number' || typeof x0 !== 'number') { + return null; + } + const scaledX0 = xScale.scale(x0); + let scaledX1: number | null; + if (xScale.totalBarsInCluster > 0 && !isHistogramModeEnabled) { + scaledX1 = xScale.scale(x1 + xScale.minInterval); + } else { + scaledX1 = xScale.scale(x1); + } + if (scaledX1 == null || scaledX0 == null) { + return null; + } + // the width needs to be computed before adjusting the x anchor + const width = Math.abs(scaledX1 - scaledX0); + return { + x: scaledX0 - (xScale.bandwidthPadding / 2) * xScale.totalBarsInCluster, + width, + }; +} + +/** + * This function extend and limits the values in a scale domain + * @param scale the scale + * @param minValue a min value + * @param maxValue a max value + */ +function limitValueToDomainRange( + scale: Scale, + minValue?: PrimitiveValue, + maxValue?: PrimitiveValue, + isHistogram: boolean = false, +): [PrimitiveValue, PrimitiveValue] { + const domainStartValue = scale.domain[0]; + // this fix the case where rendering on categorical scale and we have only one element + const domainEndValue = scale.domain.length > 0 ? scale.domain[scale.domain.length - 1] : scale.domain[0]; + + // extend to edge values if values are null/undefined + let min = minValue == null ? domainStartValue : minValue; + let max = maxValue == null ? domainEndValue : maxValue; + + if (isContinuousScale(scale)) { + if (minValue == null) { + // we expand null/undefined values to the edge + min = domainStartValue; + } else if (typeof minValue !== 'number') { + // we need to restrict to number only for continuous scales + min = null; + } else if (minValue < domainStartValue) { + // we limit values to the edge + min = domainStartValue; + } else { + min = minValue; + } + + if (maxValue == null) { + // we expand null/undefined values to the edge + max = isHistogram ? domainEndValue + scale.minInterval : domainEndValue; + } else if (typeof maxValue !== 'number') { + // we need to restrict to number only for continuous scales + max = null; + } else if (maxValue > domainEndValue) { + // we limit values to the edge + max = domainEndValue; + } else { + max = maxValue; + } + } + return [min, max]; +} diff --git a/src/chart_types/xy_chart/annotations/rect/tooltip.ts b/src/chart_types/xy_chart/annotations/rect/tooltip.ts new file mode 100644 index 0000000000..e411786e84 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect/tooltip.ts @@ -0,0 +1,64 @@ +/* + * 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 { AnnotationTypes } from '../../utils/specs'; +import { Rotation } from '../../../../utils/commons'; +import { Dimensions } from '../../../../utils/dimensions'; +import { Point } from '../../../../utils/point'; +import { getRotatedCursor } from '../utils'; +import { AnnotationTooltipFormatter, AnnotationTooltipState, Bounds } from '../types'; +import { AnnotationRectProps } from './types'; +import { isWithinRectBounds } from './dimensions'; + +/** @internal */ +export function computeRectAnnotationTooltipState( + /** the cursor position relative to the projection area */ + cursorPosition: Point, + annotationRects: AnnotationRectProps[], + chartRotation: Rotation, + chartDimensions: Dimensions, + renderTooltip?: AnnotationTooltipFormatter, +): AnnotationTooltipState { + const rotatedCursorPosition = getRotatedCursor(cursorPosition, chartDimensions, chartRotation); + const totalAnnotationRect = annotationRects.length; + for (let i = 0; i < totalAnnotationRect; i++) { + const rectProps = annotationRects[i]; + const { rect, details } = rectProps; + const startX = rect.x; + const endX = startX + rect.width; + const startY = rect.y; + const endY = startY + rect.height; + const bounds: Bounds = { startX, endX, startY, endY }; + const isWithinBounds = isWithinRectBounds(rotatedCursorPosition, bounds); + if (isWithinBounds) { + return { + isVisible: true, + annotationType: AnnotationTypes.Rectangle, + anchor: { + left: rotatedCursorPosition.x, + top: rotatedCursorPosition.y, + }, + ...(details && { details }), + ...(renderTooltip && { renderTooltip }), + }; + } + } + return { + isVisible: false, + }; +} diff --git a/src/chart_types/xy_chart/annotations/rect/types.ts b/src/chart_types/xy_chart/annotations/rect/types.ts new file mode 100644 index 0000000000..24e4d7c263 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/rect/types.ts @@ -0,0 +1,28 @@ +/* + * 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. */ + +/** @internal */ +export interface AnnotationRectProps { + rect: { + x: number; + y: number; + width: number; + height: number; + }; + details?: string; +} diff --git a/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts deleted file mode 100644 index b62a4210c9..0000000000 --- a/src/chart_types/xy_chart/annotations/rect_annotation_tooltip.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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 { AnnotationTypes, RectAnnotationDatum, RectAnnotationSpec } from '../utils/specs'; -import { Rotation } from '../../../utils/commons'; -import { Dimensions } from '../../../utils/dimensions'; -import { GroupId } from '../../../utils/ids'; -import { Scale } from '../../../scales'; -import { Point } from '../../../utils/point'; -import { - AnnotationTooltipFormatter, - AnnotationTooltipState, - getRotatedCursor, - scaleAndValidateDatum, - Bounds, -} from './annotation_utils'; - -/** @internal */ -export interface AnnotationRectProps { - rect: { - x: number; - y: number; - width: number; - height: number; - }; - details?: string; -} - -/** @internal */ -export function computeRectAnnotationTooltipState( - /** the cursor position relative to the projection area */ - cursorPosition: Point, - annotationRects: AnnotationRectProps[], - chartRotation: Rotation, - chartDimensions: Dimensions, - renderTooltip?: AnnotationTooltipFormatter, -): AnnotationTooltipState { - const rotatedCursorPosition = getRotatedCursor(cursorPosition, chartDimensions, chartRotation); - - const totalAnnotationRect = annotationRects.length; - for (let i = 0; i < totalAnnotationRect; i++) { - const rectProps = annotationRects[i]; - const { rect, details } = rectProps; - const startX = rect.x; - const endX = startX + rect.width; - - const startY = rect.y; - const endY = startY + rect.height; - - const bounds: Bounds = { startX, endX, startY, endY }; - - const isWithinBounds = isWithinRectBounds(rotatedCursorPosition, bounds); - if (isWithinBounds) { - return { - isVisible: true, - annotationType: AnnotationTypes.Rectangle, - anchor: { - left: rotatedCursorPosition.x, - top: rotatedCursorPosition.y, - }, - ...(details && { details }), - ...(renderTooltip && { renderTooltip }), - }; - } - } - return { - isVisible: false, - }; -} - -/** @internal */ -export function isWithinRectBounds({ x, y }: Point, { startX, endX, startY, endY }: Bounds): boolean { - const withinXBounds = x >= startX && x <= endX; - const withinYBounds = y >= startY && y <= endY; - - return withinXBounds && withinYBounds; -} - -/** @internal */ -export function computeRectAnnotationDimensions( - annotationSpec: RectAnnotationSpec, - yScales: Map, - xScale: Scale, - enableHistogramMode: boolean, - barsPadding: number, -): AnnotationRectProps[] | null { - const { dataValues } = annotationSpec; - - const groupId = annotationSpec.groupId; - const yScale = yScales.get(groupId); - if (!yScale) { - return null; - } - - const xDomain = xScale.domain; - const yDomain = yScale.domain; - const lastX = xDomain[xDomain.length - 1]; - const xMinInterval = xScale.minInterval; - const rectsProps: AnnotationRectProps[] = []; - - dataValues.forEach((dataValue: RectAnnotationDatum) => { - let { x0, x1, y0, y1 } = dataValue.coordinates; - - // if everything is null, return; otherwise we coerce the other coordinates - if (x0 == null && x1 == null && y0 == null && y1 == null) { - return; - } - - if (x1 == null) { - // if x1 is defined, we want the rect to draw to the end of the scale - // if we're in histogram mode, extend domain end by min interval - x1 = enableHistogramMode && !xScale.isSingleValue() ? lastX + xMinInterval : lastX; - } - - if (x0 == null) { - // if x0 is defined, we want the rect to draw to the start of the scale - x0 = xDomain[0]; - } - - if (y0 == null) { - // if y0 is defined, we want the rect to draw to the end of the scale - y0 = yDomain[yDomain.length - 1]; - } - - if (y1 == null) { - // if y1 is defined, we want the rect to draw to the start of the scale - y1 = yDomain[0]; - } - - const alignWithTick = xScale.bandwidth > 0 && !enableHistogramMode; - - let x0Scaled = scaleAndValidateDatum(x0, xScale, alignWithTick); - let x1Scaled = scaleAndValidateDatum(x1, xScale, alignWithTick); - const y0Scaled = scaleAndValidateDatum(y0, yScale, false); - const y1Scaled = scaleAndValidateDatum(y1, yScale, false); - - // TODO: surface this as a warning - if (x0Scaled === null || x1Scaled === null || y0Scaled === null || y1Scaled === null) { - return; - } - - let xOffset = 0; - if (xScale.bandwidth > 0) { - const xBand = xScale.bandwidth / (1 - xScale.barsPadding); - xOffset = enableHistogramMode ? (xBand - xScale.bandwidth) / 2 : barsPadding; - } - - x0Scaled = x0Scaled - xOffset; - x1Scaled = x1Scaled - xOffset; - - const minX = Math.min(x0Scaled, x1Scaled); - const minY = Math.min(y0Scaled, y1Scaled); - - const deltaX = Math.abs(x0Scaled - x1Scaled); - const deltaY = Math.abs(y0Scaled - y1Scaled); - - const xOrigin = minX; - const yOrigin = minY; - - const width = deltaX; - const height = deltaY; - - const rectDimensions = { - x: xOrigin, - y: yOrigin, - width, - height, - }; - - rectsProps.push({ - rect: rectDimensions, - details: dataValue.details, - }); - }); - - return rectsProps; -} diff --git a/src/chart_types/xy_chart/annotations/tooltip.ts b/src/chart_types/xy_chart/annotations/tooltip.ts new file mode 100644 index 0000000000..9e3a41211d --- /dev/null +++ b/src/chart_types/xy_chart/annotations/tooltip.ts @@ -0,0 +1,123 @@ +/* + * 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 { AnnotationId } from '../../../utils/ids'; +import { AnnotationSpec, AxisSpec, isLineAnnotation, isRectAnnotation } from '../utils/specs'; +import { Rotation, Position } from '../../../utils/commons'; +import { AnnotationDimensions, AnnotationTooltipState } from './types'; +import { Dimensions } from '../../../utils/dimensions'; +import { computeLineAnnotationTooltipState } from './line/tooltip'; +import { computeRectAnnotationTooltipState } from './rect/tooltip'; +import { AnnotationRectProps } from './rect/types'; +import { Point } from '../../../utils/point'; +import { AnnotationLineProps } from './line/types'; + +/** @internal */ +export function computeAnnotationTooltipState( + cursorPosition: Point, + annotationDimensions: Map, + annotationSpecs: AnnotationSpec[], + chartRotation: Rotation, + axesSpecs: AxisSpec[], + chartDimensions: Dimensions, +): AnnotationTooltipState | null { + // allow picking up the last spec added as the top most or use it's zIndex value + const sortedSpecs = annotationSpecs + .slice() + .reverse() + .sort(({ zIndex: a = Number.MIN_SAFE_INTEGER }, { zIndex: b = Number.MIN_SAFE_INTEGER }) => b - a); + for (const spec of sortedSpecs) { + const annotationDimension = annotationDimensions.get(spec.id); + if (spec.hideTooltips || !annotationDimension) { + continue; + } + const groupId = spec.groupId; + + if (isLineAnnotation(spec)) { + if (spec.hideLines) { + continue; + } + const lineAnnotationTooltipState = computeLineAnnotationTooltipState( + cursorPosition, + annotationDimension as AnnotationLineProps[], + groupId, + spec.domainType, + axesSpecs, + ); + + if (lineAnnotationTooltipState.isVisible) { + return lineAnnotationTooltipState; + } + } else if (isRectAnnotation(spec)) { + const rectAnnotationTooltipState = computeRectAnnotationTooltipState( + cursorPosition, + annotationDimension as AnnotationRectProps[], + chartRotation, + chartDimensions, + spec.renderTooltip, + ); + + if (rectAnnotationTooltipState.isVisible) { + return rectAnnotationTooltipState; + } + } + } + + return null; +} + +/** @internal */ +export function getFinalAnnotationTooltipPosition( + /** the dimensions of the chart parent container */ + container: Dimensions, + chartDimensions: Dimensions, + /** the dimensions of the tooltip container */ + tooltip: Dimensions, + /** the tooltip computed position not adjusted within chart bounds */ + tooltipAnchor: { top: number; left: number }, + /** the width of the tooltip portal container */ + portalWidth: number, + padding = 10, +): { + left: string | null; + top: string | null; + anchor: typeof Position.Left | typeof Position.Right; +} { + let left = 0; + let anchor: Position = Position.Left; + + const annotationXOffset = window.pageXOffset + container.left + chartDimensions.left + tooltipAnchor.left; + if (chartDimensions.left + tooltipAnchor.left + portalWidth + padding >= container.width) { + left = annotationXOffset - portalWidth - padding; + anchor = Position.Right; + } else { + left = annotationXOffset + padding; + } + let top = window.pageYOffset + container.top + chartDimensions.top + tooltipAnchor.top; + if (chartDimensions.top + tooltipAnchor.top + tooltip.height + padding >= container.height) { + top -= tooltip.height + padding; + } else { + top += padding; + } + + return { + left: `${Math.round(left)}px`, + top: `${Math.round(top)}px`, + anchor, + }; +} diff --git a/src/chart_types/xy_chart/annotations/types.ts b/src/chart_types/xy_chart/annotations/types.ts new file mode 100644 index 0000000000..1039bd3c59 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/types.ts @@ -0,0 +1,83 @@ +/* + * 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 { AnnotationType } from '../utils/specs'; +import { AnnotationLineProps } from './line/types'; +import { AnnotationRectProps } from './rect/types'; +import { Position, Color } from '../../../utils/commons'; + +export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; + +/** + * The header and description strings for an Annotation + * @internal + */ +export interface AnnotationDetails { + headerText?: string; + detailsText?: string; +} + +/** + * The marker for an Annotation. Usually a JSX element + * @internal + */ +export interface AnnotationMarker { + icon: JSX.Element; + position: { + top: number; + left: number; + }; + dimension: { + width: number; + height: number; + }; + color: Color; +} + +/** @internal */ +export type AnnotationTooltipState = AnnotationTooltipVisibleState | AnnotationTooltipHiddenState; + +/** @internal */ +export interface AnnotationTooltipVisibleState { + isVisible: true; + annotationType: AnnotationType; + header?: string; + details?: string; + anchor: { + position?: Position; + top: number; + left: number; + }; + renderTooltip?: AnnotationTooltipFormatter; +} + +/** @internal */ +export interface AnnotationTooltipHiddenState { + isVisible: false; +} + +/** @internal */ +export type AnnotationDimensions = AnnotationLineProps[] | AnnotationRectProps[]; + +/** @internal */ +export type Bounds = { + startX: number; + endX: number; + startY: number; + endY: number; +}; diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts b/src/chart_types/xy_chart/annotations/utils.test.ts similarity index 76% rename from src/chart_types/xy_chart/annotations/annotation_utils.test.ts rename to src/chart_types/xy_chart/annotations/utils.test.ts index 4d885075ca..1cea87c840 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts +++ b/src/chart_types/xy_chart/annotations/utils.test.ts @@ -30,33 +30,20 @@ import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { Dimensions } from '../../../utils/dimensions'; import { GroupId, AnnotationId } from '../../../utils/ids'; import { Scale, ScaleType, ScaleBand, ScaleContinuous } from '../../../scales'; -import { - computeAnnotationDimensions, - computeAnnotationTooltipState, - computeClusterOffset, - getAnnotationAxis, - getRotatedCursor, - scaleAndValidateDatum, - AnnotationDimensions, - AnnotationTooltipState, - Bounds, -} from './annotation_utils'; -import { - AnnotationLineProps, - computeLineAnnotationDimensions, - computeLineAnnotationTooltipState, - isVerticalAnnotationLine, - getAnnotationLineTooltipXOffset, - getAnnotationLineTooltipYOffset, -} from './line_annotation_tooltip'; -import { - computeRectAnnotationDimensions, - isWithinRectBounds, - computeRectAnnotationTooltipState, -} from './rect_annotation_tooltip'; +import { computeAnnotationDimensions, getAnnotationAxis, getRotatedCursor } from './utils'; +import { AnnotationDimensions, AnnotationTooltipState, Bounds } from './types'; +import { computeLineAnnotationDimensions } from './line/dimensions'; +import { AnnotationLineProps } from './line/types'; +import { computeRectAnnotationDimensions, isWithinRectBounds } from './rect/dimensions'; +import { computeRectAnnotationTooltipState } from './rect/tooltip'; import { Point } from '../../../utils/point'; import { ChartTypes } from '../..'; import { SpecTypes } from '../../../specs/settings'; +import { computeLineAnnotationTooltipState } from './line/tooltip'; +import { computeAnnotationTooltipState } from './tooltip'; +import { MockStore } from '../../../mocks/store'; +import { MockGlobalSpec, MockSeriesSpec, MockAnnotationSpec } from '../../../mocks/specs'; +import { computeAnnotationDimensionsSelector } from '../state/selectors/compute_annotations'; describe('annotation utils', () => { const minRange = 0; @@ -116,70 +103,58 @@ describe('annotation utils', () => { axesSpecs.push(verticalAxisSpec); - test('should compute annotation dimensions', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; + test('should compute rect annotation in x ordinal scale', () => { + const store = MockStore.default(); + const settings = MockGlobalSpec.settingsNoMargins(); + const spec = MockSeriesSpec.bar({ + xScaleType: ScaleType.Ordinal, + groupId, + data: [ + { x: 'a', y: 1 }, + { x: 'b', y: 0 }, + { x: 'c', y: 10 }, + { x: 'd', y: 5 }, + ], + }); - const annotations: AnnotationSpec[] = []; - const annotationId = 'foo'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo', + groupId, domainType: AnnotationDomainTypes.YDomain, dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; + }); - const rectAnnotationId = 'rect'; - const rectAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: rectAnnotationId, + const rectAnnotation = MockAnnotationSpec.rect({ + id: 'rect', groupId, - annotationType: AnnotationTypes.Rectangle, dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }], - }; + }); - annotations.push(lineAnnotation); - annotations.push(rectAnnotation); + MockStore.addSpecs([settings, spec, lineAnnotation, rectAnnotation], store); + const dimensions = computeAnnotationDimensionsSelector(store.getState()); - const dimensions = computeAnnotationDimensions( - annotations, - chartDimensions, - chartRotation, - yScales, - xScale, - axesSpecs, - 1, - false, - ); const expectedDimensions = new Map(); - expectedDimensions.set(annotationId, [ + expectedDimensions.set('foo', [ { anchor: { - top: 20, + top: 80, left: 0, position: Position.Left, }, linePathPoints: { - start: { x1: 0, y1: 20 }, - end: { x2: 10, y2: 20 }, + start: { x1: 0, y1: 80 }, + end: { x2: 100, y2: 80 }, }, + marker: undefined, details: { detailsText: 'foo', headerText: '2' }, }, ]); - expectedDimensions.set(rectAnnotationId, [{ rect: { x: 0, y: 30, width: 25, height: 20 } }]); + expectedDimensions.set('rect', [{ details: undefined, rect: { x: 0, y: 50, width: 50, height: 20 } }]); expect(dimensions).toEqual(expectedDimensions); }); - test('should not compute annotation dimensions if a corresponding axis does not exist', () => { + test('should compute annotation dimensions also with missing axis', () => { const chartRotation: Rotation = 0; const yScales: Map = new Map(); yScales.set(groupId, continuousScale); @@ -208,11 +183,9 @@ describe('annotation utils', () => { yScales, xScale, [], // empty axesSpecs - 1, false, ); - const expectedDimensions = new Map(); - expect(dimensions).toEqual(expectedDimensions); + expect(dimensions.size).toEqual(1); }); test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, left axis)', () => { @@ -241,7 +214,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -287,7 +259,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Right, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -333,7 +304,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -378,7 +348,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); expect(dimensions).toEqual(null); @@ -408,7 +377,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -452,7 +420,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Top, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -497,7 +464,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Bottom, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -518,51 +484,6 @@ describe('annotation utils', () => { expect(dimensions).toEqual(expectedDimensions); }); - test('should compute line annotation dimensions for xDomain in histogramMode with extended upper bound', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 10.5, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - Position.Bottom, - 0, - true, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - anchor: { - top: 20, - left: 110, - position: Position.Bottom, - }, - linePathPoints: { - start: { x1: 110, y1: 20 }, - end: { x2: 110, y2: 0 }, - }, - details: { detailsText: 'foo', headerText: '10.5' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, ordinal scale)', () => { const chartRotation: Rotation = 90; const yScales: Map = new Map(); @@ -588,7 +509,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -634,7 +554,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -680,7 +599,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Left, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -726,7 +644,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Top, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -771,7 +688,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Bottom, - 0, false, ); const expectedDimensions: AnnotationLineProps[] = [ @@ -792,7 +708,7 @@ describe('annotation utils', () => { expect(dimensions).toEqual(expectedDimensions); }); - test('should not compute annotation line values for values outside of domain or AnnotationSpec.hideLines', () => { + test('should not compute annotation line values for invalid data values or AnnotationSpec.hideLines', () => { const chartRotation: Rotation = 0; const yScales: Map = new Map(); yScales.set(groupId, continuousScale); @@ -818,7 +734,6 @@ describe('annotation utils', () => { yScales, xScale, Position.Right, - 0, false, ); @@ -842,7 +757,6 @@ describe('annotation utils', () => { yScales, continuousScale, Position.Right, - 0, false, ); @@ -866,11 +780,9 @@ describe('annotation utils', () => { yScales, continuousScale, Position.Right, - 0, false, ); - - expect(emptyOutOfBoundsXDimensions).toEqual([]); + expect(emptyOutOfBoundsXDimensions).toHaveLength(0); const invalidYLineAnnotation: AnnotationSpec = { chartType: ChartTypes.XYAxis, @@ -883,18 +795,17 @@ describe('annotation utils', () => { style: DEFAULT_ANNOTATION_LINE_STYLE, }; - const emptyYDimensions = computeLineAnnotationDimensions( + const emptyOutOfBoundsYDimensions = computeLineAnnotationDimensions( invalidYLineAnnotation, chartDimensions, chartRotation, yScales, xScale, Position.Right, - 0, false, ); - expect(emptyYDimensions).toEqual([]); + expect(emptyOutOfBoundsYDimensions).toHaveLength(0); const outOfBoundsYLineAnnotation: AnnotationSpec = { chartType: ChartTypes.XYAxis, @@ -907,18 +818,17 @@ describe('annotation utils', () => { style: DEFAULT_ANNOTATION_LINE_STYLE, }; - const emptyOutOfBoundsYDimensions = computeLineAnnotationDimensions( + const outOfBoundsYAnn = computeLineAnnotationDimensions( outOfBoundsYLineAnnotation, chartDimensions, chartRotation, yScales, xScale, Position.Right, - 0, false, ); - expect(emptyOutOfBoundsYDimensions).toEqual([]); + expect(outOfBoundsYAnn).toHaveLength(0); const invalidStringYLineAnnotation: AnnotationSpec = { chartType: ChartTypes.XYAxis, @@ -938,7 +848,6 @@ describe('annotation utils', () => { yScales, continuousScale, Position.Right, - 0, false, ); @@ -963,79 +872,12 @@ describe('annotation utils', () => { yScales, continuousScale, Position.Right, - 0, false, ); expect(hiddenAnnotationDimensions).toEqual(null); }); - test('should determine if an annotation line is vertical dependent on domain type & chart rotation', () => { - const isHorizontal = true; - const isXDomain = true; - const xDomainHorizontalRotation = isVerticalAnnotationLine(isXDomain, isHorizontal); - expect(xDomainHorizontalRotation).toBe(true); - - const xDomainVerticalRotation = isVerticalAnnotationLine(isXDomain, !isHorizontal); - expect(xDomainVerticalRotation).toBe(false); - - const yDomainHorizontalRotation = isVerticalAnnotationLine(!isXDomain, isHorizontal); - expect(yDomainHorizontalRotation).toBe(false); - - const yDomainVerticalRotation = isVerticalAnnotationLine(isXDomain, !isHorizontal); - expect(yDomainVerticalRotation).toBe(false); - }); - test('should get the x offset for an annotation line tooltip', () => { - const bottomHorizontalRotationOffset = getAnnotationLineTooltipXOffset(0, Position.Bottom); - expect(bottomHorizontalRotationOffset).toBe(50); - - const topHorizontalRotationOffset = getAnnotationLineTooltipXOffset(0, Position.Top); - expect(topHorizontalRotationOffset).toBe(50); - - const bottomVerticalRotationOffset = getAnnotationLineTooltipXOffset(90, Position.Bottom); - expect(bottomVerticalRotationOffset).toBe(0); - - const topVerticalRotationOffset = getAnnotationLineTooltipXOffset(90, Position.Top); - expect(topVerticalRotationOffset).toBe(0); - - const leftHorizontalRotationOffset = getAnnotationLineTooltipXOffset(0, Position.Left); - expect(leftHorizontalRotationOffset).toBe(0); - - const rightHorizontalRotationOffset = getAnnotationLineTooltipXOffset(0, Position.Right); - expect(rightHorizontalRotationOffset).toBe(100); - - const leftVerticalRotationOffset = getAnnotationLineTooltipXOffset(90, Position.Left); - expect(leftVerticalRotationOffset).toBe(50); - - const rightVerticalRotationOffset = getAnnotationLineTooltipXOffset(90, Position.Right); - expect(rightVerticalRotationOffset).toBe(50); - }); - test('should get the y offset for an annotation line tooltip', () => { - const bottomHorizontalRotationOffset = getAnnotationLineTooltipYOffset(0, Position.Bottom); - expect(bottomHorizontalRotationOffset).toBe(100); - - const topHorizontalRotationOffset = getAnnotationLineTooltipYOffset(0, Position.Top); - expect(topHorizontalRotationOffset).toBe(0); - - const bottomVerticalRotationOffset = getAnnotationLineTooltipYOffset(90, Position.Bottom); - expect(bottomVerticalRotationOffset).toBe(50); - - const topVerticalRotationOffset = getAnnotationLineTooltipYOffset(90, Position.Top); - expect(topVerticalRotationOffset).toBe(50); - - const leftHorizontalRotationOffset = getAnnotationLineTooltipYOffset(0, Position.Left); - expect(leftHorizontalRotationOffset).toBe(50); - - const rightHorizontalRotationOffset = getAnnotationLineTooltipYOffset(0, Position.Right); - expect(rightHorizontalRotationOffset).toBe(50); - - const leftVerticalRotationOffset = getAnnotationLineTooltipYOffset(90, Position.Left); - expect(leftVerticalRotationOffset).toBe(100); - - const rightVerticalRotationOffset = getAnnotationLineTooltipYOffset(90, Position.Right); - expect(rightVerticalRotationOffset).toBe(100); - }); - test('should compute the tooltip state for an annotation line', () => { const cursorPosition: Point = { x: 1, y: 2 }; const annotationLines: AnnotationLineProps[] = [ @@ -1379,7 +1221,7 @@ describe('annotation utils', () => { dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], }; - const noYScale = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const noYScale = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); expect(noYScale).toBe(null); }); @@ -1401,17 +1243,27 @@ describe('annotation utils', () => { ], }; - const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); + const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); - expect(skippedInvalid).toEqual([]); + expect(skippedInvalid).toHaveLength(1); }); test('should compute rectangle dimensions shifted for histogram mode', () => { const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); + yScales.set( + groupId, + new ScaleContinuous( + { + type: ScaleType.Linear, + domain: continuousData, + range: [minRange, maxRange], + }, + { bandwidth: 0, minInterval: 1 }, + ), + ); const xScale: Scale = new ScaleContinuous( { type: ScaleType.Linear, domain: continuousData, range: [minRange, maxRange] }, - { bandwidth: 1, minInterval: 1 }, + { bandwidth: 72, minInterval: 1 }, ); const annotationRectangle: RectAnnotationSpec = { @@ -1428,115 +1280,30 @@ describe('annotation utils', () => { ], }; - const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, true, 0); + const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); const [dims1, dims2, dims3, dims4] = dimensions; expect(dims1.rect.x).toBe(10); - expect(dims1.rect.y).toBe(0); - expect(dims1.rect.width).toBeCloseTo(100); + expect(dims1.rect.y).toBe(100); expect(dims1.rect.height).toBe(100); + expect(dims1.rect.width).toBeCloseTo(100); expect(dims2.rect.x).toBe(0); - expect(dims2.rect.y).toBe(0); - expect(dims2.rect.width).toBe(10); + expect(dims2.rect.y).toBe(100); + expect(dims2.rect.width).toBe(20); expect(dims2.rect.height).toBe(100); expect(dims3.rect.x).toBe(0); - expect(dims3.rect.y).toBe(0); + expect(dims3.rect.y).toBe(100); expect(dims3.rect.width).toBeCloseTo(110); - expect(dims3.rect.height).toBe(10); + expect(dims3.rect.height).toBe(90); expect(dims4.rect.x).toBe(0); expect(dims4.rect.y).toBe(10); expect(dims4.rect.width).toBeCloseTo(110); - expect(dims4.rect.height).toBe(90); + expect(dims4.rect.height).toBe(10); }); - test('should compute rectangle dimensions when only a single coordinate defined', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = continuousScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [ - { coordinates: { x0: 1, x1: null, y0: null, y1: null } }, - { coordinates: { x0: null, x1: 1, y0: null, y1: null } }, - { coordinates: { x0: null, x1: null, y0: 1, y1: null } }, - { coordinates: { x0: null, x1: null, y0: null, y1: 1 } }, - ], - }; - const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); - - const expectedDimensions = [ - { rect: { x: 10, y: 0, width: 90, height: 100 } }, - { rect: { x: 0, y: 0, width: 10, height: 100 } }, - { rect: { x: 0, y: 0, width: 100, height: 10 } }, - { rect: { x: 0, y: 10, width: 100, height: 90 } }, - ]; - - expect(dimensions).toEqual(expectedDimensions); - }); - test('should compute rectangle annotation dimensions continuous (0 deg rotation)', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = continuousScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], - }; - - const unrotated = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); - - expect(unrotated).toEqual([{ rect: { x: 10, y: 30, width: 10, height: 20 } }]); - }); - test('should compute rectangle annotation dimensions ordinal (0 deg rotation)', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 0, y1: 2 } }], - }; - - const unrotated = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale, false, 0); - - expect(unrotated).toEqual([{ rect: { x: 0, y: 0, width: 25, height: 20 } }]); - }); - test('should validate scaled dataValues', () => { - // not aligned with tick - expect(scaleAndValidateDatum('', ordinalScale, false)).toBe(null); - expect(scaleAndValidateDatum('a', continuousScale, false)).toBe(null); - expect(scaleAndValidateDatum(-10, continuousScale, false)).toBe(null); - expect(scaleAndValidateDatum(20, continuousScale, false)).toBe(null); - - // allow values within domainEnd + minInterval when not alignWithTick - expect(scaleAndValidateDatum(10.25, continuousScale, false)).toBeCloseTo(102.5); - expect(scaleAndValidateDatum(10.25, continuousScale, true)).toBe(null); - - expect(scaleAndValidateDatum('a', ordinalScale, false)).toBe(0); - expect(scaleAndValidateDatum(0, continuousScale, false)).toBe(0); - - // aligned with tick - expect(scaleAndValidateDatum(1.25, continuousScale, true)).toBe(12.5); - }); test('should determine if a point is within a rectangle annotation', () => { const cursorPosition = { x: 3, y: 4 }; @@ -1590,12 +1357,6 @@ describe('annotation utils', () => { expect(visibleTooltip).toEqual(expectedVisibleTooltipState); }); - test('should determine if line is vertical annotation', () => { - expect(isVerticalAnnotationLine(true, true)).toBe(true); - expect(isVerticalAnnotationLine(true, false)).toBe(false); - expect(isVerticalAnnotationLine(false, true)).toBe(false); - expect(isVerticalAnnotationLine(false, false)).toBe(true); - }); test('should get rotated cursor position', () => { const cursorPosition = { x: 1, y: 2 }; @@ -1605,15 +1366,4 @@ describe('annotation utils', () => { expect(getRotatedCursor(cursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 1 }); expect(getRotatedCursor(cursorPosition, chartDimensions, 180)).toEqual({ x: 9, y: 18 }); }); - - test('should compute cluster offset', () => { - const singleBarCluster = 1; - const multiBarCluster = 2; - - const barsShift = 4; - const bandwidth = 2; - - expect(computeClusterOffset(singleBarCluster, barsShift, bandwidth)).toBe(0); - expect(computeClusterOffset(multiBarCluster, barsShift, bandwidth)).toBe(3); - }); }); diff --git a/src/chart_types/xy_chart/annotations/utils.ts b/src/chart_types/xy_chart/annotations/utils.ts new file mode 100644 index 0000000000..1d7487e227 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/utils.ts @@ -0,0 +1,119 @@ +/* + * 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 { + AnnotationDomainType, + AnnotationDomainTypes, + AnnotationSpec, + AxisSpec, + isLineAnnotation, + isRectAnnotation, +} from '../utils/specs'; +import { Dimensions } from '../../../utils/dimensions'; +import { AnnotationId, GroupId } from '../../../utils/ids'; +import { Scale } from '../../../scales'; +import { getAxesSpecForSpecId, isHorizontalRotation } from '../state/utils'; +import { Point } from '../../../utils/point'; +import { computeLineAnnotationDimensions } from './line/dimensions'; +import { computeRectAnnotationDimensions } from './rect/dimensions'; +import { Rotation, Position } from '../../../utils/commons'; +import { AnnotationDimensions } from './types'; + +/** @internal */ +export function getAnnotationAxis( + axesSpecs: AxisSpec[], + groupId: GroupId, + domainType: AnnotationDomainType, + chartRotation: Rotation, +): Position | null { + const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); + const isHorizontalRotated = isHorizontalRotation(chartRotation); + const isXDomainAnnotation = isXDomain(domainType); + const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; + const rotatedAnnotation = isHorizontalRotated ? annotationAxis : isXDomainAnnotation ? yAxis : xAxis; + return rotatedAnnotation ? rotatedAnnotation.position : null; +} + +/** @internal */ +export function isXDomain(domainType: AnnotationDomainType): boolean { + return domainType === AnnotationDomainTypes.XDomain; +} + +/** @internal */ +export function getRotatedCursor( + /** the cursor position relative to the projection area */ + cursorPosition: Point, + chartDimensions: Dimensions, + chartRotation: Rotation, +): Point { + const { x, y } = cursorPosition; + const { height, width } = chartDimensions; + switch (chartRotation) { + case 0: + return { x, y }; + case 90: + return { x: y, y: width - x }; + case -90: + return { x: height - y, y: x }; + case 180: + return { x: width - x, y: height - y }; + } +} + +/** @internal */ +export function computeAnnotationDimensions( + annotations: AnnotationSpec[], + chartDimensions: Dimensions, + chartRotation: Rotation, + yScales: Map, + xScale: Scale, + axesSpecs: AxisSpec[], + isHistogramModeEnabled: boolean, +): Map { + const annotationDimensions = new Map(); + + annotations.forEach((annotationSpec) => { + const { id } = annotationSpec; + if (isLineAnnotation(annotationSpec)) { + const { groupId, domainType } = annotationSpec; + const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType, chartRotation); + + const dimensions = computeLineAnnotationDimensions( + annotationSpec, + chartDimensions, + chartRotation, + yScales, + xScale, + annotationAxisPosition, + isHistogramModeEnabled, + ); + + if (dimensions) { + annotationDimensions.set(id, dimensions); + } + } else if (isRectAnnotation(annotationSpec)) { + const dimensions = computeRectAnnotationDimensions(annotationSpec, yScales, xScale, isHistogramModeEnabled); + + if (dimensions) { + annotationDimensions.set(id, dimensions); + } + } + }); + + return annotationDimensions; +} diff --git a/src/chart_types/xy_chart/legend/legend.ts b/src/chart_types/xy_chart/legend/legend.ts index 27efa96728..0da1997809 100644 --- a/src/chart_types/xy_chart/legend/legend.ts +++ b/src/chart_types/xy_chart/legend/legend.ts @@ -48,8 +48,7 @@ function getPostfix(spec: BasicSeriesSpec): Postfixes { return {}; } -/** @internal */ -export function getBandedLegendItemLabel(name: string, yAccessor: BandedAccessorType, postfixes: Postfixes) { +function getBandedLegendItemLabel(name: string, yAccessor: BandedAccessorType, postfixes: Postfixes) { return yAccessor === BandedAccessorType.Y1 ? `${name}${postfixes.y1AccessorFormat}` : `${name}${postfixes.y0AccessorFormat}`; diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts index 18ab7398be..a3c0c6bc68 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/index.ts @@ -16,15 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { AnnotationDimensions } from '../../../annotations/annotation_utils'; +import { AnnotationDimensions } from '../../../annotations/types'; import { AnnotationSpec, isLineAnnotation, isRectAnnotation } from '../../../utils/specs'; import { getSpecsById } from '../../../state/utils'; import { AnnotationId } from '../../../../../utils/ids'; import { mergeWithDefaultAnnotationLine, mergeWithDefaultAnnotationRect } from '../../../../../utils/themes/theme'; import { renderLineAnnotations } from './lines'; -import { AnnotationLineProps } from '../../../annotations/line_annotation_tooltip'; +import { AnnotationLineProps } from '../../../annotations/line/types'; import { renderRectAnnotations } from './rect'; -import { AnnotationRectProps } from '../../../annotations/rect_annotation_tooltip'; +import { AnnotationRectProps } from '../../../annotations/rect/types'; interface AnnotationProps { annotationDimensions: Map; diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts index 8d6e6fae62..216787f21b 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts @@ -18,7 +18,7 @@ import { Stroke, Line } from '../../../../../geoms/types'; import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; -import { AnnotationLineProps } from '../../../annotations/line_annotation_tooltip'; +import { AnnotationLineProps } from '../../../annotations/line/types'; import { LineAnnotationStyle } from '../../../../../utils/themes/theme'; import { renderMultiLine } from '../primitives/line'; diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts index 78e77c62da..914a54cc03 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/rect.ts @@ -18,7 +18,7 @@ import { renderRect } from '../primitives/rect'; import { Rect, Fill, Stroke } from '../../../../../geoms/types'; -import { AnnotationRectProps } from '../../../annotations/rect_annotation_tooltip'; +import { AnnotationRectProps } from '../../../annotations/rect/types'; import { RectAnnotationStyle } from '../../../../../utils/themes/theme'; import { stringToRGB } from '../../../../partition_chart/layout/utils/d3_utils'; import { withContext } from '../../../../../renderers/canvas'; diff --git a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts index ce09ed5426..35a54abf5f 100644 --- a/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts +++ b/src/chart_types/xy_chart/renderer/canvas/primitives/rect.ts @@ -33,7 +33,6 @@ export function renderRect( if (fill) { const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width : 0; - // console.log(stroke, borderOffset); const x = rect.x + borderOffset; const y = rect.y + borderOffset; const width = rect.width - borderOffset * 2; @@ -45,7 +44,6 @@ export function renderRect( if (stroke && stroke.width > 0.001) { const borderOffset = !disableBoardOffset && stroke && stroke.width > 0.001 ? stroke.width / 2 : 0; - // console.log(stroke, borderOffset); const x = rect.x + borderOffset; const y = rect.y + borderOffset; const width = rect.width - borderOffset * 2; 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 e20876f73e..07c04b81d5 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -30,7 +30,7 @@ import { Dimensions } from '../../../../utils/dimensions'; import { AnnotationId, AxisId } from '../../../../utils/ids'; import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme } from '../../../../utils/themes/theme'; -import { AnnotationDimensions } from '../../annotations/annotation_utils'; +import { AnnotationDimensions } from '../../annotations/types'; import { LegendItem } from '../../../../commons/legend'; import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; import { computeAxisTicksDimensionsSelector } from '../../state/selectors/compute_axis_ticks_dimensions'; diff --git a/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx b/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx index b88ff7cf39..64e13428ec 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx @@ -19,11 +19,7 @@ import React from 'react'; import { isLineAnnotation, AnnotationSpec, AnnotationTypes } from '../../utils/specs'; import { AnnotationId } from '../../../../utils/ids'; -import { - AnnotationDimensions, - AnnotationTooltipState, - AnnotationTooltipFormatter, -} from '../../annotations/annotation_utils'; +import { AnnotationDimensions, AnnotationTooltipState, AnnotationTooltipFormatter } from '../../annotations/types'; import { connect } from 'react-redux'; import { Dimensions } from '../../../../utils/dimensions'; import { GlobalChartState, BackwardRef } from '../../../../state/chart_state'; @@ -32,11 +28,12 @@ import { computeAnnotationDimensionsSelector } from '../../state/selectors/compu import { getAnnotationSpecsSelector } from '../../state/selectors/get_specs'; import { getAnnotationTooltipStateSelector } from '../../state/selectors/get_annotation_tooltip_state'; import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; -import { AnnotationLineProps } from '../../annotations/line_annotation_tooltip'; +import { AnnotationLineProps } from '../../annotations/line/types'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { createPortal } from 'react-dom'; -import { getFinalAnnotationTooltipPosition } from '../../annotations/annotation_tooltip'; +import { getFinalAnnotationTooltipPosition } from '../../annotations/tooltip'; import { getSpecsById } from '../../state/utils'; +import { Position } from '../../../../utils/commons'; interface AnnotationTooltipStateProps { isChartEmpty: boolean; @@ -124,8 +121,8 @@ class AnnotationTooltipComponent extends React.Component if (tooltipStyle.left) { this.portalNode.style.left = tooltipStyle.left; if (this.tooltipRef.current) { - this.tooltipRef.current.style.left = tooltipStyle.anchor === 'right' ? 'auto' : '0px'; - this.tooltipRef.current.style.right = tooltipStyle.anchor === 'right' ? '0px' : 'auto'; + this.tooltipRef.current.style.left = tooltipStyle.anchor === Position.Right ? 'auto' : '0px'; + this.tooltipRef.current.style.right = tooltipStyle.anchor === Position.Right ? '0px' : 'auto'; } } if (tooltipStyle.top) { diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 90c604b160..bbd27f2f0d 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -28,7 +28,7 @@ import { LineStyle, BubbleSeriesStyle, } from '../../../utils/themes/theme'; -import { Scale, ScaleType, isLogarithmicScale } from '../../../scales'; +import { Scale, ScaleType } from '../../../scales'; import { CurveType, getCurveFactory } from '../../../utils/curves'; import { DataSeriesDatum, DataSeries, XYChartSeriesIdentifier } from '../utils/series'; import { DisplayValueSpec, PointStyleAccessor, BarStyleAccessor } from '../utils/specs'; @@ -47,6 +47,7 @@ import { LegendItem } from '../../../commons/legend'; import { IndexedGeometryMap, GeometryType } from '../utils/indexed_geometry_map'; import { getDistance } from '../state/utils'; import { MarkBuffer } from '../../../specs'; +import { isLogarithmicScale } from '../../../scales/types'; export const DEFAULT_HIGHLIGHT_PADDING = 10; diff --git a/src/chart_types/xy_chart/state/selectors/compute_annotations.ts b/src/chart_types/xy_chart/state/selectors/compute_annotations.ts index 51d9a243d9..3e9003a7b4 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_annotations.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_annotations.ts @@ -20,9 +20,9 @@ import createCachedSelector from 're-reselect'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { countBarsInClusterSelector } from './count_bars_in_cluster'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; -import { computeAnnotationDimensions, AnnotationDimensions } from '../../annotations/annotation_utils'; +import { computeAnnotationDimensions } from '../../annotations/utils'; +import { AnnotationDimensions } from '../../annotations/types'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; import { AnnotationId } from '../../../../utils/ids'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; @@ -35,7 +35,6 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( getSettingsSpecSelector, computeSeriesGeometriesSelector, getAxisSpecsSelector, - countBarsInClusterSelector, isHistogramModeEnabledSelector, getAxisSpecsSelector, ], @@ -43,12 +42,10 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( annotationSpecs, chartDimensions, settingsSpec, - seriesGeometries, + { scales: { yScales, xScale } }, axesSpecs, - totalBarsInCluster, isHistogramMode, ): Map => { - const { yScales, xScale } = seriesGeometries.scales; return computeAnnotationDimensions( annotationSpecs, chartDimensions.chartDimensions, @@ -56,7 +53,6 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( yScales, xScale, axesSpecs, - totalBarsInCluster, isHistogramMode, ); }, diff --git a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts index 0dab98fa53..c653258f19 100644 --- a/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts +++ b/src/chart_types/xy_chart/state/selectors/get_annotation_tooltip_state.ts @@ -23,11 +23,8 @@ import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { AxisSpec, AnnotationSpec, AnnotationTypes } from '../../utils/specs'; import { Rotation } from '../../../../utils/commons'; -import { - computeAnnotationTooltipState, - AnnotationTooltipState, - AnnotationDimensions, -} from '../../annotations/annotation_utils'; +import { computeAnnotationTooltipState } from '../../annotations/tooltip'; +import { AnnotationTooltipState, AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensionsSelector } from './compute_annotations'; import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation'; import { AnnotationId } from '../../../../utils/ids'; 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 60beaf4710..5526381cc7 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -18,7 +18,7 @@ import { XDomain } from '../domains/x_domain'; import { YDomain } from '../domains/y_domain'; -import { AxisSpec, DomainRange, AxisStyle } from './specs'; +import { AxisSpec, DomainRange, AxisStyle, DEFAULT_GLOBAL_ID } from './specs'; import { Position } from '../../../utils/commons'; import { LIGHT_THEME } from '../../../utils/themes/light_theme'; import { AxisId, GroupId } from '../../../utils/ids'; @@ -1452,7 +1452,7 @@ describe('Axis computational utils', () => { showDuplicatedTicks: false, chartType: 'xy_axis', specType: 'axis', - groupId: '__global__', + groupId: DEFAULT_GLOBAL_ID, hide: false, showOverlappingLabels: false, showOverlappingTicks: false, @@ -1492,7 +1492,7 @@ describe('Axis computational utils', () => { showDuplicatedTicks: true, chartType: 'xy_axis', specType: 'axis', - groupId: '__global__', + groupId: DEFAULT_GLOBAL_ID, hide: false, showOverlappingLabels: false, showOverlappingTicks: false, diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index e320aba0f2..30e98914bd 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -33,9 +33,10 @@ import { AxisId, GroupId } from '../../../utils/ids'; import { ScaleContinuousType, ScaleType } from '../../../scales'; import { CurveType } from '../../../utils/curves'; import { RawDataSeriesDatum, XYChartSeriesIdentifier } from './series'; -import { AnnotationTooltipFormatter } from '../annotations/annotation_utils'; +import { AnnotationTooltipFormatter } from '../annotations/types'; import { SpecTypes, Spec } from '../../../specs'; import { ChartTypes } from '../..'; +import { PrimitiveValue } from '../../partition_chart/layout/utils/group_by_rollup'; export type BarStyleOverride = RecursivePartial | Color | null; export type PointStyleOverride = RecursivePartial | Color | null; @@ -630,19 +631,19 @@ export interface RectAnnotationDatum { /** * The minuimum value on the x axis. If undefined, the minuimum value of the x domain will be used. */ - x0?: any; + x0?: PrimitiveValue; /** * The maximum value on the x axis. If undefined, the maximum value of the x domain will be used. */ - x1?: any; + x1?: PrimitiveValue; /** * The minimum value on the y axis. If undefined, the minimum value of the y domain will be used. */ - y0?: any; + y0?: PrimitiveValue; /** * The maximum value on the y axis. If undefined, the maximum value of the y domain will be used. */ - y1?: any; + y1?: PrimitiveValue; }; /** * A textual description of the annotation @@ -668,7 +669,7 @@ export interface BaseAnnotationSpec< D extends RectAnnotationDatum | LineAnnotationDatum, S extends RectAnnotationStyle | LineAnnotationStyle > extends Spec { - chartType: ChartTypes; + chartType: typeof ChartTypes.XYAxis; specType: typeof SpecTypes.Annotation; /** * Annotation type: line, rectangle, text diff --git a/src/index.ts b/src/index.ts index 3d187a7ee2..b3a3b0c30f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,7 +32,7 @@ export { timeFormatter, niceTimeFormatter, niceTimeFormatByDay } from './utils/d export { Datum, Position, Rendering, Rotation } from './utils/commons'; export { SeriesIdentifier } from './commons/series_id'; export { XYChartSeriesIdentifier } from './chart_types/xy_chart/utils/series'; -export { AnnotationTooltipFormatter } from './chart_types/xy_chart/annotations/annotation_utils'; +export { AnnotationTooltipFormatter } from './chart_types/xy_chart/annotations/types'; export { GeometryValue } from './utils/geometry'; export { Config as PartitionConfig, diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts index 06d830f329..d25386618b 100644 --- a/src/mocks/specs/specs.ts +++ b/src/mocks/specs/specs.ts @@ -28,6 +28,11 @@ import { BasicSeriesSpec, SeriesTypes, BubbleSeriesSpec, + LineAnnotationSpec, + RectAnnotationSpec, + AnnotationTypes, + AnnotationDomainTypes, + AxisSpec, } from '../../chart_types/xy_chart/utils/specs'; import { ScaleType } from '../../scales'; import { ChartTypes } from '../../chart_types'; @@ -208,7 +213,7 @@ export class MockSeriesSpec { }); } - static byType(type?: SeriesTypes): BasicSeriesSpec { + static byType(type?: SeriesTypes | 'histogram'): BasicSeriesSpec { switch (type) { case SeriesTypes.Line: return MockSeriesSpec.lineBase; @@ -216,11 +221,26 @@ export class MockSeriesSpec { return MockSeriesSpec.areaBase; case SeriesTypes.Bubble: return MockSeriesSpec.bubbleBase; + case 'histogram': + return MockSeriesSpec.histogramBarBase; case SeriesTypes.Bar: default: return MockSeriesSpec.barBase; } } + static byTypePartial(type?: 'line' | 'bar' | 'area' | 'histogram') { + switch (type) { + case 'line': + return MockSeriesSpec.line; + case 'area': + return MockSeriesSpec.area; + case 'histogram': + return MockSeriesSpec.histogramBar; + case 'bar': + default: + return MockSeriesSpec.bar; + } + } } export class MockSeriesSpecs { @@ -264,7 +284,72 @@ export class MockGlobalSpec { theme: LIGHT_THEME, }; + private static readonly axisBase: AxisSpec = { + id: 'yAxis', + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Axis, + groupId: DEFAULT_GLOBAL_ID, + hide: false, + showOverlappingTicks: false, + showOverlappingLabels: false, + position: Position.Left, + tickSize: 10, + tickPadding: 10, + tickFormat: (tick: any) => `${tick}`, + tickLabelRotation: 0, + }; + + private static readonly settingsBaseNoMargings: SettingsSpec = { + ...MockGlobalSpec.settingsBase, + theme: { + ...LIGHT_THEME, + chartMargins: { top: 0, left: 0, right: 0, bottom: 0 }, + chartPaddings: { top: 0, left: 0, right: 0, bottom: 0 }, + scales: { + barsPadding: 0, + histogramPadding: 0, + }, + }, + }; + static settings(partial?: Partial): SettingsSpec { return mergePartial(MockGlobalSpec.settingsBase, partial, { mergeOptionalPartialValues: true }); } + static settingsNoMargins(partial?: Partial): SettingsSpec { + return mergePartial(MockGlobalSpec.settingsBaseNoMargings, partial, { + mergeOptionalPartialValues: true, + }); + } + static axis(partial?: Partial): AxisSpec { + return mergePartial(MockGlobalSpec.axisBase, partial, { mergeOptionalPartialValues: true }); + } +} + +/** @internal */ +export class MockAnnotationSpec { + private static readonly lineBase: LineAnnotationSpec = { + id: 'line_annotation_1', + groupId: DEFAULT_GLOBAL_ID, + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Line, + dataValues: [], + domainType: AnnotationDomainTypes.XDomain, + }; + + private static readonly rectBase: RectAnnotationSpec = { + id: 'rect_annotation_1', + groupId: DEFAULT_GLOBAL_ID, + chartType: ChartTypes.XYAxis, + specType: SpecTypes.Annotation, + annotationType: AnnotationTypes.Rectangle, + dataValues: [], + }; + + static line(partial?: Partial): LineAnnotationSpec { + return mergePartial(MockAnnotationSpec.lineBase, partial, { mergeOptionalPartialValues: true }); + } + static rect(partial?: Partial): RectAnnotationSpec { + return mergePartial(MockAnnotationSpec.rectBase, partial, { mergeOptionalPartialValues: true }); + } } diff --git a/src/mocks/store/index.ts b/src/mocks/store/index.ts new file mode 100644 index 0000000000..4702768221 --- /dev/null +++ b/src/mocks/store/index.ts @@ -0,0 +1,20 @@ +/* + * 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. */ + +/** @internal */ +export * from './store'; diff --git a/src/mocks/store/store.ts b/src/mocks/store/store.ts new file mode 100644 index 0000000000..14110e9398 --- /dev/null +++ b/src/mocks/store/store.ts @@ -0,0 +1,54 @@ +/* + * 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 { chartStoreReducer, GlobalChartState } from '../../state/chart_state'; +import { createStore, Store } from 'redux'; +import { specParsing, upsertSpec, specParsed } from '../../state/actions/specs'; +import { Spec } from '../../specs'; +import { updateParentDimensions } from '../../state/actions/chart_settings'; + +/** @internal */ +export class MockStore { + static default( + { width, height, top, left } = { width: 100, height: 100, top: 0, left: 0 }, + chartId = 'chartId', + ): Store { + const storeReducer = chartStoreReducer(chartId); + const store = createStore(storeReducer); + store.dispatch(updateParentDimensions({ width, height, top, left })); + return store; + } + + static addSpecs(specs: Spec | Array, store: Store) { + store.dispatch(specParsing()); + if (Array.isArray(specs)) { + const actions = specs.map(upsertSpec); + actions.forEach(store.dispatch); + } else { + store.dispatch(upsertSpec(specs)); + } + store.dispatch(specParsed()); + } + + static updateDimensions( + { width, height, top, left } = { width: 100, height: 100, top: 0, left: 0 }, + store: Store, + ) { + store.dispatch(updateParentDimensions({ width, height, top, left })); + } +} diff --git a/src/scales/index.ts b/src/scales/index.ts index 12c30a5626..99bffc752a 100644 --- a/src/scales/index.ts +++ b/src/scales/index.ts @@ -79,11 +79,3 @@ export { ScaleBand } from './scale_band'; /** @internal */ export { ScaleContinuous } from './scale_continuous'; - -/** - * Check if a scale is logaritmic - * @internal - */ -export function isLogarithmicScale(scale: Scale) { - return scale.type === ScaleType.Log; -} diff --git a/src/scales/scale_band.ts b/src/scales/scale_band.ts index ed8efa41e1..86a4eace8b 100644 --- a/src/scales/scale_band.ts +++ b/src/scales/scale_band.ts @@ -31,6 +31,9 @@ export class ScaleBand implements Scale { readonly bandwidth: number; readonly bandwidthPadding: number; readonly step: number; + readonly outerPadding: number; + readonly innerPadding: number; + readonly originalBandwidth: number; readonly type: ScaleType; readonly domain: any[]; readonly range: number[]; @@ -59,7 +62,10 @@ export class ScaleBand implements Scale { this.barsPadding = safeBarPadding; this.d3Scale.paddingInner(safeBarPadding); this.d3Scale.paddingOuter(safeBarPadding / 2); + this.outerPadding = this.d3Scale.paddingOuter(); + this.innerPadding = this.d3Scale.paddingInner(); this.bandwidth = this.d3Scale.bandwidth() || 0; + this.originalBandwidth = this.d3Scale.bandwidth() || 0; this.step = this.d3Scale.step(); this.domain = this.d3Scale.domain(); this.range = range.slice(); diff --git a/src/scales/scale_continuous.test.ts b/src/scales/scale_continuous.test.ts index 8501da4c21..fa9a8a2045 100644 --- a/src/scales/scale_continuous.test.ts +++ b/src/scales/scale_continuous.test.ts @@ -20,7 +20,8 @@ import { XDomain } from '../chart_types/xy_chart/domains/x_domain'; import { computeXScale } from '../chart_types/xy_chart/utils/scales'; import { Domain } from '../utils/domain'; import { DateTime, Settings } from 'luxon'; -import { ScaleContinuous, ScaleType, ScaleBand, isLogarithmicScale } from '.'; +import { ScaleContinuous, ScaleType, ScaleBand } from '.'; +import { isLogarithmicScale } from './types'; describe('Scale Continuous', () => { test('shall invert on continuous scale linear', () => { diff --git a/src/scales/types.ts b/src/scales/types.ts new file mode 100644 index 0000000000..23cc8dbe9b --- /dev/null +++ b/src/scales/types.ts @@ -0,0 +1,45 @@ +/* + * 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 { ScaleContinuous } from './scale_continuous'; +import { Scale, ScaleType } from '.'; +import { ScaleBand } from './scale_band'; + +/** + * Check if a scale is logaritmic + * @internal + */ +export function isLogarithmicScale(scale: Scale): scale is ScaleContinuous { + return scale.type === ScaleType.Log; +} + +/** + * Check if a scale is Band + * @internal + */ +export function isBandScale(scale: Scale): scale is ScaleBand { + return scale.type === ScaleType.Ordinal; +} + +/** + * Check if a scale is continuous + * @internal + */ +export function isContinuousScale(scale: Scale): scale is ScaleContinuous { + return scale.type !== ScaleType.Ordinal; +} diff --git a/stories/annotations/rects/1_linear_bar_chart.tsx b/stories/annotations/rects/1_linear_bar_chart.tsx index 57256aaf27..a466233eff 100644 --- a/stories/annotations/rects/1_linear_bar_chart.tsx +++ b/stories/annotations/rects/1_linear_bar_chart.tsx @@ -26,22 +26,24 @@ export const example = () => { const debug = boolean('debug', false); const rotation = getChartRotationKnob(); - const dataValues = [ - { - coordinates: { - x0: 0, - x1: 1, - y0: 0, - y1: 7, - }, - details: 'details about this annotation', - }, - ]; - return ( - + { yAccessors={['y']} data={[ { x: 0, y: 2 }, - { x: 1, y: 7 }, + { x: 1, y: 3 }, { x: 3, y: 6 }, ]} /> ); }; + +example.story = { + parameters: { + info: { + text: `A \`\` can be used to create a rectangular annotation. +As for most chart component, the required props are: \`id\` to uniquely identify the annotation and +a \`dataValues\` prop that describes one or more annotations. + +The \`dataValues\` prop takes an array of objects adhering to the following type: + +\`\`\`ts + +interface RectAnnotationDatum { + coordinates: { + x0?: PrimitiveValue; + x1?: PrimitiveValue; + y0?: PrimitiveValue; + y1?: PrimitiveValue; + }; + details?: string; +} + +type PrimitiveValue = string | number | null; +\`\`\` + +Each coordinate value can be omitted, if omitted then the corresponding min or max value is used instead. +A text can be issued to be shown within the tooltip. If omitted, no tooltip will be shown. + +In the above example, we are using a fixed set of coordinates: +\`\`\` +coordinates: { + x0: 0, + x1: 1, + y0: 0, + y1: 7, +} +\`\`\` + +This annotation will cover the X axis starting from the \`0\` value to the \`1\` value included. The \`y\` is covered from 0 to 7. +In a barchart with linear or ordinal x scale, the interval covered by the annotation fully include the \`x0\` and \`x1\` values. +If one value is out of the relative domain, we will clip the annotation to the max/min value of the chart domain. + `, + }, + }, +}; diff --git a/stories/annotations/rects/2_ordinal_bar_chart.tsx b/stories/annotations/rects/2_ordinal_bar_chart.tsx index 359e4cdde6..8ab086e9fb 100644 --- a/stories/annotations/rects/2_ordinal_bar_chart.tsx +++ b/stories/annotations/rects/2_ordinal_bar_chart.tsx @@ -26,20 +26,22 @@ export const example = () => { const debug = boolean('debug', false); const rotation = getChartRotationKnob(); - const dataValues = [ - { - coordinates: { - x0: 'a', - x1: 'b', - }, - details: 'details about this annotation', - }, - ]; - return ( - + { yAccessors={['y']} data={[ { x: 'a', y: 2 }, - { x: 'b', y: 7 }, + { x: 'b', y: 3 }, { x: 'c', y: 0 }, { x: 'd', y: 6 }, ]} @@ -58,3 +60,14 @@ export const example = () => { ); }; + +example.story = { + parameters: { + info: { + text: `On Ordinal Bar charts, you can draw a rectangular annotation the same way it's done within a linear bar chart. +The annotation will cover fully the extent defined by the \`coordinate\` object, extending to the max/min domain values any +missing/out-of-range parameters. + `, + }, + }, +}; diff --git a/stories/annotations/rects/3_linear_line_chart.tsx b/stories/annotations/rects/3_linear_line_chart.tsx index 7de9e38c02..48eb2adc9a 100644 --- a/stories/annotations/rects/3_linear_line_chart.tsx +++ b/stories/annotations/rects/3_linear_line_chart.tsx @@ -28,7 +28,7 @@ export const example = () => { const rotation = getChartRotationKnob(); const definedCoordinate = select( - 'defined coordinate', + 'green annotation defined coordinate', { x0: 'x0', x1: 'x1', @@ -38,7 +38,7 @@ export const example = () => { 'x0', ); - const dataValues: RectAnnotationDatum[] = [ + const dataValuesRed: RectAnnotationDatum[] = [ { coordinates: { x0: 1, @@ -46,8 +46,10 @@ export const example = () => { y0: 0, y1: 7, }, - details: 'details about this annotation', + details: 'red annotation', }, + ]; + const dataValuesBlue: RectAnnotationDatum[] = [ { coordinates: { x0: 2.0, @@ -55,16 +57,18 @@ export const example = () => { y0: 0, y1: 7, }, - details: 'details about this annotation', + details: 'blue annotation', }, + ]; + const dataValuesGreen: RectAnnotationDatum[] = [ { coordinates: { - x0: definedCoordinate === 'x0' ? 0.25 : null, - x1: definedCoordinate === 'x1' ? 2.75 : null, - y0: definedCoordinate === BandedAccessorType.Y0 ? 0.25 : null, - y1: definedCoordinate === BandedAccessorType.Y1 ? 6.75 : null, + x0: definedCoordinate === 'x0' ? 0.5 : null, + x1: definedCoordinate === 'x1' ? 2.5 : null, + y0: definedCoordinate === BandedAccessorType.Y0 ? 1.5 : null, + y1: definedCoordinate === BandedAccessorType.Y1 ? 5.5 : null, }, - details: 'can have null values', + details: 'green annotation', }, ]; @@ -79,7 +83,9 @@ export const example = () => { return ( - + + + { marker={
} /> +