Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: outside rect annotation placement and group relations #2471

Merged
merged 11 commits into from
Jun 24, 2024
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"lint:fix:it": "yarn lint:it --fix",
"prettier:check": "prettier --check \"**/*.{json,html,css,scss}\"",
"prettier:fix": "prettier --w \"**/*.{json,html,css,scss}\"",
"playground": "cd playground && RNG_SEED='elastic-charts' webpack serve",
"playground": "export NODE_OPTIONS=--openssl-legacy-provider; cd playground && RNG_SEED='elastic-charts' webpack serve",
"pq": "pretty-quick",
"semantic-release": "semantic-release --debug",
"start": "yarn storybook",
Expand Down
144 changes: 111 additions & 33 deletions packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@

import { AnnotationRectProps } from './types';
import { getPanelSize, SmallMultipleScales } from '../../../../common/panel_utils';
import { Rect } from '../../../../geoms/types';
import { ScaleBand, ScaleContinuous } from '../../../../scales';
import { isBandScale, isContinuousScale } from '../../../../scales/types';
import { isDefined, isNil, Position, Rotation } from '../../../../utils/common';
import { Size } from '../../../../utils/dimensions';
import { AxisId, GroupId } from '../../../../utils/ids';
import { Logger } from '../../../../utils/logger';
import { Point } from '../../../../utils/point';
import { AxisStyle } from '../../../../utils/themes/theme';
import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup';
import { isHorizontalRotation, isVerticalRotation } from '../../state/utils/common';
import { isHorizontalRotation } from '../../state/utils/common';
import { getAxesSpecForSpecId } from '../../state/utils/spec';
import { AxisSpec, RectAnnotationDatum, RectAnnotationSpec } from '../../utils/specs';
import { Bounds } from '../types';
Expand All @@ -40,7 +43,7 @@ export function computeRectAnnotationDimensions(
isHistogram: boolean = false,
): AnnotationRectProps[] | null {
const { dataValues, groupId, outside, id: annotationSpecId } = annotationSpec;
const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId);
const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId, chartRotation);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to match to correct axis

const yScale = yScales.get(groupId);
const rectsProps: Omit<AnnotationRectProps, 'id' | 'panel'>[] = [];
const panelSize = getPanelSize(smallMultiplesScales);
Expand Down Expand Up @@ -72,25 +75,19 @@ export function computeRectAnnotationDimensions(
return;
}

const hasYValues = isDefined(initialY0) || isDefined(initialY1);
const outsideDim = annotationSpec.outsideDimension ?? getOutsideDimension(getAxisStyle(xAxis?.id ?? yAxis?.id));

if (!yScale) {
if (!isDefined(initialY0) && !isDefined(initialY1)) {
const isLeftSide =
(chartRotation === 0 && xAxis?.position === Position.Bottom) ||
(chartRotation === 180 && xAxis?.position === Position.Top) ||
(chartRotation === -90 && yAxis?.position === Position.Right) ||
(chartRotation === 90 && yAxis?.position === Position.Left);
const orthoDimension = isHorizontalRotation(chartRotation) ? panelSize.height : panelSize.width;
const outsideDim = annotationSpec.outsideDimension ?? getOutsideDimension(getAxisStyle(xAxis?.id ?? yAxis?.id));
const rectDimensions = {
if (!hasYValues) {
// only x values present, just fill full height of chart space
const rectDimensions: Rect = {
...xAndWidth,
...(outside
? {
y: isLeftSide ? orthoDimension : -outsideDim,
height: outsideDim,
}
? getXOutsideAnnotationDimensions(panelSize, chartRotation, xAxis?.position ?? 'bottom', outsideDim)
: {
y: 0,
height: orthoDimension,
height: isHorizontalRotation(chartRotation) ? panelSize.height : panelSize.width,
}),
};
rectsProps.push({
Expand All @@ -99,10 +96,33 @@ export function computeRectAnnotationDimensions(
datum,
});
}
return;
return; // cannot scale y values without a scale
}

const hasXValues = isDefined(initialX0) || isDefined(initialX1);

if (outside) {
if (hasXValues && hasYValues) {
Logger.warn(
`The RectAnnotation (${annotationSpecId}) was defined as outside but has both x and y values defined.`,
);
Comment on lines +104 to +108
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warns when attempting to render annotation as outside but both x and y coordinates are defined. The outside annotation should be limited to a single axis. Renders inside as fallback to avoid not rendering at all.

} else if (hasXValues) {
const rectDimensions: Rect = {
...xAndWidth,
...getXOutsideAnnotationDimensions(panelSize, chartRotation, xAxis?.position ?? 'bottom', outsideDim),
};

rectsProps.push({
specId: annotationSpecId,
rect: rectDimensions,
datum,
});
return;
}
}

const [y0, y1] = limitValueToDomainRange(yScale, initialY0, initialY1);

// something is wrong with the data types, don't draw this annotation
if (!Number.isFinite(y0) || !Number.isFinite(y1)) return;

Expand All @@ -111,29 +131,21 @@ export function computeRectAnnotationDimensions(
if (Number.isNaN(scaledY1) || Number.isNaN(scaledY0)) return;

height = Math.abs(scaledY0 - scaledY1);
// if the annotation height is 0 override it with the height from chart dimension and if the values in the domain are the same

// if the annotation height is 0, override it with the height from chart dimension and if the values in the domain are the same
if (height === 0 && yScale.domain.length === 2 && yScale.domain[0] === yScale.domain[1]) {
// eslint-disable-next-line prefer-destructuring
height = panelSize.height;
scaledY1 = 0;
}

const orthoDimension = isVerticalRotation(chartRotation) ? panelSize.height : panelSize.width;
const isLeftSide =
(chartRotation === 0 && yAxis?.position === Position.Left) ||
(chartRotation === 180 && yAxis?.position === Position.Right) ||
(chartRotation === -90 && xAxis?.position === Position.Bottom) ||
(chartRotation === 90 && xAxis?.position === Position.Top);
const outsideDim = annotationSpec.outsideDimension ?? getOutsideDimension(getAxisStyle(xAxis?.id ?? yAxis?.id));
const rectDimensions = {
...(!isDefined(initialX0) && !isDefined(initialX1) && outside
? {
x: isLeftSide ? -outsideDim : orthoDimension,
width: outsideDim,
}
: xAndWidth),
const rectDimensions: Rect = {
...xAndWidth,
y: scaledY1,
height,
...(outside &&
!(hasXValues && hasYValues) &&
getYOutsideAnnotationDimensions(panelSize, chartRotation, yAxis?.position ?? 'left', outsideDim)),
};

rectsProps.push({
Expand Down Expand Up @@ -244,7 +256,7 @@ function maxOf(base: number, value: number | string | null | undefined): number
}

function getOutsideDimension({ tickLine: { visible, size } }: AxisStyle): number {
return visible ? size : 0;
return visible ? size : 1;
}

/**
Expand All @@ -259,3 +271,69 @@ export function getAnnotationRectPropsId(
) {
return [specId, verticalValue, horizontalValue, ...Object.values(datum.coordinates), datum.details, index].join('__');
}

function getXOutsideAnnotationDimensions(
panelSize: Size,
rotation: Rotation,
axisPosition: Position,
thickness: number,
): Pick<Rect, 'y' | 'height'> {
const { height, width } = panelSize;

switch (axisPosition) {
case Position.Top:
return {
y: rotation === 180 ? height : rotation === 90 ? width : -thickness,
height: thickness,
};
case Position.Bottom:
return {
y: rotation === 0 ? height : rotation === 90 ? width : -thickness,
height: thickness,
};
case Position.Left:
return {
y: rotation === -90 ? -thickness : width,
height: thickness,
};
case Position.Right:
default:
return {
y: rotation === -90 ? width : -thickness,
height: thickness,
};
}
}

function getYOutsideAnnotationDimensions(
panelSize: Size,
rotation: Rotation,
axisPosition: Position,
thickness: number,
): Pick<Rect, 'x' | 'width'> {
const { height, width } = panelSize;

switch (axisPosition) {
case Position.Left:
return {
x: rotation === 180 ? width : rotation === 90 ? height : -thickness,
width: thickness,
};
case Position.Right:
return {
x: rotation === 0 ? width : rotation === 90 ? height : -thickness,
width: thickness,
};
case Position.Top:
return {
x: rotation === -90 ? height : -thickness,
width: thickness,
};
case Position.Bottom:
default:
return {
x: rotation === -90 ? -thickness : height,
width: thickness,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function isHorizontalRotation(chartRotation: Rotation) {
export function isVerticalRotation(chartRotation: Rotation) {
return chartRotation === -90 || chartRotation === 90;
}

/**
* Check if a specs map contains only line or area specs
* @param specs Map<SpecId, BasicSeriesSpec>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function getSpecsById<T extends Spec>(specs: T[], id: string): T | undefi
}

/** @internal */
export function getAxesSpecForSpecId(axesSpecs: AxisSpec[], groupId: GroupId, chartRotation: Rotation = 0) {
export function getAxesSpecForSpecId(axesSpecs: AxisSpec[], groupId: GroupId, chartRotation: Rotation) {
return axesSpecs.reduce<{ xAxis?: AxisSpec; yAxis?: AxisSpec }>((result, spec) => {
if (spec.groupId === groupId && isXDomain(spec.position, chartRotation)) result.xAxis = spec;
else if (spec.groupId === groupId && !isXDomain(spec.position, chartRotation)) result.yAxis = spec;
Expand Down
9 changes: 8 additions & 1 deletion storybook/stories/annotations/rects/6_zero_domain.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ export const Example: ChartsStory = (_, { title, description }) => {
const xAxisKnobs = getKnobs();
// only show the fit enable or disable if relevant
const fit = xAxisKnobs.minY === xAxisKnobs.maxY ? boolean('fit to the domain', false) : undefined;
const outside = boolean('render outside', false);

return (
<Chart title={title} description={description}>
<RectAnnotation id="rect" dataValues={[{ coordinates: xAxisKnobs }]} style={{ fill: 'red' }} />
<RectAnnotation
id="rect"
dataValues={[{ coordinates: xAxisKnobs, details: 'My Annotation' }]}
style={{ fill: 'red' }}
outside={outside}
outsideDimension={4}
/>
<Settings baseTheme={useBaseTheme()} />
<Axis id="bottom" position={Position.Bottom} title="x-domain axis" />
<Axis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Position } from '@elastic/charts/src/utils/common';

import { ChartsStory } from '../../../types';
import { useBaseTheme } from '../../../use_base_theme';
import { customKnobs } from '../../utils/knobs';

const getKnobs = () => {
const enabled = boolean('enable annotation', true);
Expand All @@ -35,6 +36,7 @@ const getKnobs = () => {
x1,
y0: yDefined ? number('y0', 0) : undefined,
y1: yDefined ? number('y1', 3) : undefined,
outside: boolean('outside', false),
};
};
export const Example: ChartsStory = (_, { title, description }) => {
Expand All @@ -48,9 +50,11 @@ export const Example: ChartsStory = (_, { title, description }) => {
id="x axis"
dataValues={[{ coordinates: xAxisKnobs }]}
style={{ fill: 'red' }}
outside={xAxisKnobs.outside}
outsideDimension={5}
/>
)}
<Settings baseTheme={useBaseTheme()} />
<Settings baseTheme={useBaseTheme()} rotation={customKnobs.enum.rotation()} />
<Axis id="bottom" groupId="group2" position={Position.Bottom} title="Bottom (groupId=group2)" />
<Axis id="left" groupId="group1" position={Position.Left} title="Left (groupId=group1)" />
<Axis id="top" groupId="group1" position={Position.Top} title="Top (groupId=group1)" />
Expand Down
74 changes: 46 additions & 28 deletions storybook/stories/annotations/rects/8_outside.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const vGroups = {
};

export const Example: ChartsStory = (_, { title, description }) => {
const useGroupIds = boolean('use groupIds', false, 'Annotations');
const debug = boolean('debug', false);
const rotation = customKnobs.enum.rotation();
const tickSize = number('Tick size', 10, { min: 0, max: 20, step: 1 });
Expand Down Expand Up @@ -51,35 +52,52 @@ export const Example: ChartsStory = (_, { title, description }) => {
baseTheme={useBaseTheme()}
/>

<Axis
id="left"
hide={hideAxes}
groupId={isVert ? undefined : vGroups.Primary}
position={Position.Left}
title={isVert ? 'Left' : 'Primary - Left'}
/>
{!isVert && (
<Axis
id="right"
hide={hideAxes}
groupId={vGroups.Secondary}
position={Position.Right}
title="Secondary - Right"
/>
{useGroupIds && (
<>
<Axis
id="left"
hide={hideAxes}
groupId={isVert ? undefined : vGroups.Primary}
position={Position.Left}
title={isVert ? 'Left' : 'Primary - Left'}
/>
{!isVert && (
<Axis
id="right"
hide={hideAxes}
groupId={vGroups.Secondary}
position={Position.Right}
title="Secondary - Right"
/>
)}
<Axis
id="bottom"
hide={hideAxes}
groupId={isVert ? vGroups.Primary : undefined}
position={Position.Bottom}
title={isVert ? 'Primary - Bottom' : 'Bottom'}
/>
{isVert && (
<Axis
id="top"
hide={hideAxes}
groupId={vGroups.Secondary}
position={Position.Top}
title="Secondary - Top"
/>
)}
</>
)}
<Axis
id="bottom"
hide={hideAxes}
groupId={isVert ? vGroups.Primary : undefined}
position={Position.Bottom}
title={isVert ? 'Primary - Bottom' : 'Bottom'}
/>
{isVert && (
<Axis id="top" hide={hideAxes} groupId={vGroups.Secondary} position={Position.Top} title="Secondary - Top" />

{!useGroupIds && (
<>
<Axis id="left" hide={hideAxes} position={Position.Left} title="Left" />
<Axis id="bottom" hide={hideAxes} position={Position.Bottom} title="Bottom" />
</>
)}

<RectAnnotation
groupId={redGroupId}
groupId={useGroupIds ? redGroupId : undefined}
dataValues={[
{
coordinates: isX
Expand Down Expand Up @@ -112,7 +130,7 @@ export const Example: ChartsStory = (_, { title, description }) => {
outsideDimension={outsideDimension}
/>
<RectAnnotation
groupId={blueGroupId}
groupId={useGroupIds ? blueGroupId : undefined}
dataValues={[
{
coordinates: isX
Expand Down Expand Up @@ -147,7 +165,7 @@ export const Example: ChartsStory = (_, { title, description }) => {

<LineSeries
id="lines1"
groupId={isX ? undefined : vGroups.Primary}
groupId={useGroupIds && !isX ? vGroups.Primary : undefined}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
Expand All @@ -161,7 +179,7 @@ export const Example: ChartsStory = (_, { title, description }) => {
/>
<LineSeries
id="lines2"
groupId={isX ? undefined : vGroups.Secondary}
groupId={useGroupIds && !isX ? vGroups.Secondary : undefined}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
Expand Down
Loading