diff --git a/.eslintrc.js b/.eslintrc.js index b5acbf1b42..4d398c6920 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -468,5 +468,11 @@ module.exports = { 'promise/no-promise-in-callback': 0, }, }, + { + files: ['./integration/**/*.test.ts?(x)'], + rules: { + 'jest/expect-expect': 0, + }, + }, ], }; diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-inverted-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-inverted-visually-looks-correct-1-snap.png index 7e98c55e92..ac88760e70 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-inverted-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-inverted-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-visually-looks-correct-1-snap.png index db05b8eb1a..52da8bff8a 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-visually-looks-correct-1-snap.png differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-3-\317\200-4-0-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-3-\317\200-4-0-1-snap.png" new file mode 100644 index 0000000000..8675a3459b Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-3-\317\200-4-0-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-3-\317\200-4-\317\200-4-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-3-\317\200-4-\317\200-4-1-snap.png" new file mode 100644 index 0000000000..b93e932736 Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-3-\317\200-4-\317\200-4-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-neg-\317\200-8-\317\200-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-neg-\317\200-8-\317\200-1-snap.png" new file mode 100644 index 0000000000..ccb15f126c Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-neg-\317\200-8-\317\200-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-0-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-0-1-snap.png" new file mode 100644 index 0000000000..0b1f3932f4 Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-0-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-4-3-\317\200-4-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-4-3-\317\200-4-1-snap.png" new file mode 100644 index 0000000000..9f92704d01 Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-4-3-\317\200-4-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-8-\317\200-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-8-\317\200-1-snap.png" new file mode 100644 index 0000000000..6946e41a72 Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-bottom-shift-\317\200-8-\317\200-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-3-\317\200-4-neg-\317\200-4-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-3-\317\200-4-neg-\317\200-4-1-snap.png" new file mode 100644 index 0000000000..9820204a81 Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-3-\317\200-4-neg-\317\200-4-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-4-neg-3-\317\200-4-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-4-neg-3-\317\200-4-1-snap.png" new file mode 100644 index 0000000000..304a2472ce Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-4-neg-3-\317\200-4-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-8-neg-\317\200-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-8-neg-\317\200-1-snap.png" new file mode 100644 index 0000000000..457ec2d13a Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-8-neg-\317\200-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-neg-\317\200-8-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-neg-\317\200-8-1-snap.png" new file mode 100644 index 0000000000..ddf541c3de Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-neg-\317\200-8-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-\317\200-8-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-\317\200-8-1-snap.png" new file mode 100644 index 0000000000..01f2c12443 Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-neg-\317\200-\317\200-8-1-snap.png" differ diff --git "a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-\317\200-8-neg-\317\200-1-snap.png" "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-\317\200-8-neg-\317\200-1-snap.png" new file mode 100644 index 0000000000..b8114cf91a Binary files /dev/null and "b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-sagitta-shifted-goal-charts-should-apply-correct-top-shift-\317\200-8-neg-\317\200-1-snap.png" differ diff --git a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-should-prevent-overlapping-angles-clockwise-1-snap.png b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-should-prevent-overlapping-angles-clockwise-1-snap.png new file mode 100644 index 0000000000..a8e5d8bff9 Binary files /dev/null and b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-should-prevent-overlapping-angles-clockwise-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-should-prevent-overlapping-angles-counterclockwise-1-snap.png b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-should-prevent-overlapping-angles-counterclockwise-1-snap.png new file mode 100644 index 0000000000..e353234b68 Binary files /dev/null and b/integration/tests/__image_snapshots__/goal-stories-test-ts-goal-stories-should-prevent-overlapping-angles-counterclockwise-1-snap.png differ diff --git a/integration/tests/goal_stories.test.ts b/integration/tests/goal_stories.test.ts index 09ecc74643..54341020e0 100644 --- a/integration/tests/goal_stories.test.ts +++ b/integration/tests/goal_stories.test.ts @@ -70,4 +70,45 @@ describe('Goal stories', () => { ); }); }); + + it('should prevent overlapping angles - clockwise', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/goal-alpha--full-circle&globals=theme:light&knob-endAngle%20(%CF%80)=-0.625&knob-startAngle%20(%CF%80)=1.5', + ); + }); + + it('should prevent overlapping angles - counterclockwise', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/goal-alpha--full-circle&globals=theme:light&knob-endAngle%20(%CF%80)=1.625&knob-startAngle%20(%CF%80)=-0.5', + ); + }); + + describe('sagitta shifted goal charts', () => { + it.each<[title: string, startAngle: number, endAngle: number]>([ + // top openings + ['π/8 -> neg π', 1 / 8, -1], + ['neg π/8 -> neg π', -1 / 8, -1], + ['neg π -> π/8', -1, 1 / 8], + ['neg π -> neg π/8', -1, -1 / 8], + ['neg π/4 -> neg 3π/4', -1 / 4, -3 / 4], + ['neg 3π/4 -> neg π/4', -3 / 4, -1 / 4], + ])('should apply correct top shift (%s)', async (_, startAngle, endAngle) => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/goal-alpha--full-circle&globals=theme:light&knob-startAngle%20(%CF%80)=${startAngle}&knob-endAngle%20(%CF%80)=${endAngle}`, + ); + }); + it.each<[title: string, startAngle: number, endAngle: number]>([ + // bottom openings + ['π -> 0', 1, 0], + ['3π/4 -> 0', 3 / 4, 0], + ['neg π/8 -> π', -1 / 8, 1], + ['π/8 -> π', 1 / 8, 1], + ['3π/4 -> π/4', 3 / 4, 1 / 4], + ['π/4 -> 3π/4', 1 / 4, 3 / 4], + ])('should apply correct bottom shift (%s)', async (_, startAngle, endAngle) => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/goal-alpha--full-circle&globals=theme:light&knob-startAngle%20(%CF%80)=${startAngle}&knob-endAngle%20(%CF%80)=${endAngle}`, + ); + }); + }); }); diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index be15fa1fc7..ff76b41ad1 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -953,8 +953,10 @@ export interface GeometryValue { // @public (undocumented) export function getNodeName(node: ArrayNode): string; +// Warning: (ae-forgotten-export) The symbol "buildProps" needs to be exported by the entry point index.d.ts +// // @alpha -export const Goal: FC>; +export const Goal: (props: SFProps) => null; // @alpha (undocumented) export type GoalLabelAccessor = LabelAccessor; diff --git a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/geoms.ts b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/geoms.ts index 9037180d75..368a0c976e 100644 --- a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/geoms.ts +++ b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/geoms.ts @@ -15,7 +15,7 @@ import { Dimensions } from '../../../../utils/dimensions'; import { Theme } from '../../../../utils/themes/theme'; import { GoalSubtype } from '../../specs/constants'; import { BulletViewModel } from '../types/viewmodel_types'; -import { getSagitta, getMinSagitta } from './utils'; +import { getSagitta, getMinSagitta, getTranformDirection } from './utils'; const referenceCircularSizeCap = 360; // goal/gauge won't be bigger even if there's ample room: it'd be a waste of space const referenceBulletSizeCap = 500; // goal/gauge won't be bigger even if there's ample room: it'd be a waste of space @@ -247,8 +247,8 @@ export function geoms( labelMinor, centralMajor, centralMinor, - angleStart, angleEnd, + angleStart, } = bulletViewModel; const circular = subtype === GoalSubtype.Goal; @@ -395,7 +395,8 @@ export function geoms( if (circular) { const sagitta = getMinSagitta(angleStart, angleEnd, r); const maxSagitta = getSagitta((3 / 2) * Math.PI, r); - data.yOffset.value = sagitta >= maxSagitta ? 0 : (maxSagitta - sagitta) / 2; + const direction = getTranformDirection(angleStart, angleEnd); + data.yOffset.value = Math.abs(sagitta) >= maxSagitta ? 0 : (direction * (maxSagitta - sagitta)) / 2; } const fullSize = referenceSize; diff --git a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.test.ts b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.test.ts new file mode 100644 index 0000000000..5676e3bfa8 --- /dev/null +++ b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.test.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Radian } from './../../../../common/geometry'; +import { normalizeAngles } from './utils'; + +const normalize = (a: Radian): number => a / Math.PI; +const denormalize = (a: number): Radian => a * Math.PI; + +/** + * Units of π + */ +type AngleTuple = [start: number, end: number]; + +describe('Goal utils', () => { + type TestCase = [initial: AngleTuple, final: AngleTuple]; + describe('#normalizeAngles', () => { + const testCases: TestCase[] = [ + [ + [1.5, 2.5], + [-0.5, 0.5], + ], + [ + [-1.5, -2.5], + [0.5, -0.5], + ], + [ + [0.5, 1], + [0.5, 1], + ], + [ + [-0.5, -1], + [-0.5, -1], + ], + [ + [0, 2], + [0, 2], + ], + [ + [0, -2], + [0, -2], + ], + [ + [2, 4], + [0, 2], + ], + [ + [-2, -4], + [0, -2], + ], + [ + [20, 21], + [0, 1], + ], + [ + [-20, -21], + [0, -1], + ], + ]; + + describe('counterclockwise', () => { + it.each(testCases)('should normalize angles from %j to %j', (inital, final) => { + const initialAngles = inital.map(denormalize) as AngleTuple; + const result = normalizeAngles(...initialAngles).map(normalize); + // needed for rounding errrors with normalizing + expect(result[0]).toBeCloseTo(final[0]); + expect(result[1]).toBeCloseTo(final[1]); + }); + }); + + describe('clockwise', () => { + it.each(testCases.map((arr) => arr.map((a) => a.reverse() as AngleTuple) as TestCase))( + 'should normalize angles from %j to %j', + (inital, final) => { + const initialAngles = inital.map(denormalize) as AngleTuple; + const result = normalizeAngles(...initialAngles).map(normalize); + // needed for rounding errrors with normalizing + expect(result[0]).toBeCloseTo(final[0]); + expect(result[1]).toBeCloseTo(final[1]); + }, + ); + }); + }); +}); diff --git a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts index 03291e540b..21c82f211b 100644 --- a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts +++ b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/utils.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { TAU } from '../../../../common/constants'; import { Radian } from '../../../../common/geometry'; import { round } from '../../../../utils/common'; @@ -16,22 +17,77 @@ import { round } from '../../../../utils/common'; const LIMITING_ANGLE = Math.PI / 2; /** - * Returns limiting angle form π/2 towards 3/2π from left and right + * Angles are relative to mathematical angles of a unit circle from -2π > θ > 2π */ -const controllingAngle = (...angles: Radian[]): Radian => - angles.reduce((limitAngle, a) => { - if (a >= Math.PI / 2 && a <= (3 / 2) * Math.PI) { - const newA = Math.abs(a - Math.PI / 2); - return Math.max(limitAngle, newA); - } - if (a >= -Math.PI / 2 && a <= Math.PI / 2) { - const newA = Math.abs(a - Math.PI / 2); - return Math.max(limitAngle, newA); - } - return limitAngle; - }, LIMITING_ANGLE); +const hasTopGap = (angleStart: Radian, angleEnd: Radian): boolean => { + const [a, b] = [angleStart, angleEnd].sort(); + return a <= -Math.PI / 2 && a >= (-Math.PI * 3) / 2 && b >= -Math.PI / 2 && b <= Math.PI / 2; +}; + +/** + * Angles are relative to mathematical angles of a unit circle from -2π > θ > 2π + */ +const hasBottomGap = (angleStart: Radian, angleEnd: Radian): boolean => { + const [a, b] = [angleStart, angleEnd].sort(); + return a >= -Math.PI / 2 && a <= Math.PI / 2 && b < (Math.PI * 3) / 2 && b >= Math.PI / 2; +}; + +/** + * Angles are relative to mathematical angles of a unit circle from -2π > θ > 2π + */ +const isOnlyTopHalf = (angleStart: Radian, angleEnd: Radian): boolean => { + const [a, b] = [angleStart, angleEnd].sort(); + return a >= 0 && b <= Math.PI; +}; + +/** + * Angles are relative to mathematical angles of a unit circle from -2π > θ > 2π + */ +const isOnlyBottomHalf = (angleStart: Radian, angleEnd: Radian): boolean => { + const [a, b] = [angleStart, angleEnd].sort(); + return (a >= Math.PI && b <= 2 * Math.PI) || (a >= -Math.PI && b <= 0); +}; + +/** + * Angles are relative to mathematical angles of a unit circle from -2π > θ > 2π + */ +const isWithinLimitedDomain = (angleStart: Radian, angleEnd: Radian): boolean => { + const [a, b] = [angleStart, angleEnd].sort(); + return a > -2 * Math.PI && b < 2 * Math.PI; +}; /** @internal */ +export const getTranformDirection = (angleStart: Radian, angleEnd: Radian): 1 | -1 => + hasTopGap(angleStart, angleEnd) || isOnlyBottomHalf(angleStart, angleEnd) ? -1 : 1; + +/** + * Returns limiting angle form π/2 towards 3/2π from left and right, top and bottom + * Angles are relative to mathematical angles of a unit circle from -2π > θ > 2π + */ +const controllingAngle = (angleStart: Radian, angleEnd: Radian): number => { + if (!isWithinLimitedDomain(angleStart, angleEnd)) return LIMITING_ANGLE * 2; + if (isOnlyTopHalf(angleStart, angleEnd) || isOnlyBottomHalf(angleStart, angleEnd)) return LIMITING_ANGLE; + if (!hasTopGap(angleStart, angleEnd) && !hasBottomGap(angleStart, angleEnd)) return LIMITING_ANGLE * 2; + const offset = hasBottomGap(angleStart, angleEnd) ? -Math.PI / 2 : Math.PI / 2; + return Math.max(Math.abs(angleStart + offset), Math.abs(angleEnd + offset), LIMITING_ANGLE); +}; + +/** + * Normalize angles to minimum equivalent pair within -2π >= θ >= 2π + * Assumes angles are no more that 2π apart. + * @internal + */ +export function normalizeAngles(angleStart: Radian, angleEnd: Radian): [angleStart: Radian, angleEnd: Radian] { + const maxOffset = Math.max(Math.ceil(Math.abs(angleStart) / TAU), Math.ceil(Math.abs(angleEnd) / TAU)) - 1; + const offsetDirection = angleStart > 0 && angleEnd > 0 ? -1 : 1; + const offset = offsetDirection * maxOffset * TAU; + return [angleStart + offset, angleEnd + offset]; +} + +/** + * Angles are relative to mathmatical angles of a unit circle from -2π > θ > 2π + * @internal + */ export function getSagitta(angle: Radian, radius: number, fractionDigits: number = 1) { const arcLength = angle * radius; const halfCord = radius * Math.sin(arcLength / (2 * radius)); @@ -40,8 +96,12 @@ export function getSagitta(angle: Radian, radius: number, fractionDigits: number return round(sagitta, fractionDigits); } -/** @internal */ -export function getMinSagitta(startAngle: Radian, endAngle: Radian, radius: number, fractionDigits?: number) { - const limitingAngle = controllingAngle(startAngle, endAngle); +/** + * Angles are relative to mathmatical angles of a unit circle from -2π > θ > 2π + * @internal + */ +export function getMinSagitta(angleStart: Radian, angleEnd: Radian, radius: number, fractionDigits?: number) { + const normalizedAngles = normalizeAngles(angleStart, angleEnd); + const limitingAngle = controllingAngle(...normalizedAngles); return getSagitta(limitingAngle * 2, radius, fractionDigits); } diff --git a/packages/charts/src/chart_types/goal_chart/specs/index.ts b/packages/charts/src/chart_types/goal_chart/specs/index.ts index d45628ec03..6febeb9af4 100644 --- a/packages/charts/src/chart_types/goal_chart/specs/index.ts +++ b/packages/charts/src/chart_types/goal_chart/specs/index.ts @@ -10,10 +10,12 @@ import { ComponentProps } from 'react'; import { ChartType } from '../..'; import { Color } from '../../../common/colors'; +import { TAU } from '../../../common/constants'; import { Spec } from '../../../specs'; import { SpecType } from '../../../specs/constants'; -import { specComponentFactory } from '../../../state/spec_factory'; -import { LabelAccessor, ValueFormatter } from '../../../utils/common'; +import { buildSFProps, SFProps, useSpecFactory } from '../../../state/spec_factory'; +import { LabelAccessor, round, stripUndefined, ValueFormatter } from '../../../utils/common'; +import { Logger } from '../../../utils/logger'; import { defaultGoalSpec } from '../layout/types/viewmodel_types'; import { GoalSubtype } from './constants'; @@ -57,11 +59,7 @@ export interface GoalSpec extends Spec { tooltipValueFormatter: ValueFormatter; } -/** - * Add Goal spec to chart - * @alpha - */ -export const Goal = specComponentFactory()( +const buildProps = buildSFProps()( { specType: SpecType.Series, chartType: ChartType.Goal, @@ -71,5 +69,43 @@ export const Goal = specComponentFactory()( }, ); +/** + * Add Goal spec to chart + * @alpha + */ +export const Goal = function ( + props: SFProps< + GoalSpec, + keyof typeof buildProps['overrides'], + keyof typeof buildProps['defaults'], + keyof typeof buildProps['optionals'], + keyof typeof buildProps['requires'] + >, +) { + const { defaults, overrides } = buildProps; + const angleStart = props.angleStart ?? defaults.angleStart; + const angleEnd = props.angleEnd ?? defaults.angleEnd; + const constraints: Pick = {}; + + if (Math.abs(angleEnd - angleStart) > TAU) { + constraints.angleEnd = angleStart + TAU * Math.sign(angleEnd - angleStart); + + Logger.warn(`The total angle of the goal chart must not exceed 2π radians.\ +To prevent overlapping, the value of \`angleEnd\` will be replaced. + + original: ${angleEnd} (~${round(angleEnd / Math.PI, 3)}π) + replaced: ${constraints.angleEnd} (~${round(constraints.angleEnd / Math.PI, 3)}π) +`); + } + + useSpecFactory({ + ...defaults, + ...stripUndefined(props), + ...overrides, + ...constraints, + }); + return null; +}; + /** @public */ export type GoalProps = ComponentProps; diff --git a/storybook/stories/goal/17_total_circle.story.tsx b/storybook/stories/goal/17_total_circle.story.tsx index aa519d63cf..564631bcc0 100644 --- a/storybook/stories/goal/17_total_circle.story.tsx +++ b/storybook/stories/goal/17_total_circle.story.tsx @@ -6,17 +6,15 @@ * Side Public License, v 1. */ +import { number } from '@storybook/addon-knobs'; import React from 'react'; import { Chart, Goal, Settings } from '@elastic/charts'; -import { BandFillColorAccessorInput } from '@elastic/charts/src/chart_types/goal_chart/specs'; import { GoalSubtype } from '@elastic/charts/src/chart_types/goal_chart/specs/constants'; import { Color } from '../../../packages/charts/src/common/colors'; import { useBaseTheme } from '../../use_base_theme'; -const subtype = GoalSubtype.Goal; - const colorMap: { [k: number]: Color } = { 200: '#fc8d62', 250: 'lightgrey', @@ -25,25 +23,29 @@ const colorMap: { [k: number]: Color } = { const bandFillColor = (x: number): Color => colorMap[x]; -export const Example = () => ( - - - String(value)} - bandFillColor={({ value }: BandFillColorAccessorInput) => bandFillColor(value)} - labelMajor="" - labelMinor="" - centralMajor="280 MB/s" - centralMinor="" - angleStart={Math.PI + Math.PI / 2} - angleEnd={-Math.PI / 2} - /> - -); +export const Example = () => { + const start = number('startAngle (π)', 1.5, { min: -2, max: 2, step: 1 / 8 }); + const end = number('endAngle (π)', -0.5, { min: -2, max: 2, step: 1 / 8 }); + return ( + + + String(value)} + bandFillColor={({ value }) => bandFillColor(value)} + labelMajor="" + labelMinor="" + centralMajor="280 MB/s" + centralMinor="" + angleStart={start * Math.PI} + angleEnd={end * Math.PI} + /> + + ); +};