From a96cbb44feefc5e7ccfa249b8aecdfab81249fdd Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 4 Aug 2021 19:30:24 +0200 Subject: [PATCH] feat(bullet): the tooltip shows up around the drawn part of the chart only (#1278) --- .eslintrc.js | 2 +- ...inverted-visually-looks-correct-1-snap.png | Bin 0 -> 13735 bytes .../goal_chart/layout/viewmodel/geoms.ts | 96 +++++++++++++++-- .../goal_chart/layout/viewmodel/viewmodel.ts | 3 +- .../renderer/canvas/canvas_renderers.ts | 4 +- .../renderer/canvas/connected_component.tsx | 16 ++- .../state/selectors/picked_shapes.ts | 43 ++++++-- .../goal_chart/state/selectors/scenegraph.ts | 10 +- .../heatmap/layout/types/viewmodel_types.ts | 16 ++- .../partition_chart/layout/utils/sunburst.ts | 3 +- .../wordcloud/layout/types/viewmodel_types.ts | 8 +- .../rect/dimensions.integration.test.ts | 8 +- .../xy_chart/annotations/rect/types.ts | 12 +-- packages/charts/src/common/constants.ts | 12 +-- packages/charts/src/common/geometry.ts | 12 +++ packages/charts/src/common/text_utils.ts | 98 ++++++------------ .../18_side_gauge_inverted_angle_relation.tsx | 46 ++++++++ storybook/stories/goal/goal.stories.tsx | 1 + 18 files changed, 250 insertions(+), 140 deletions(-) create mode 100644 integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-side-gauge-inverted-visually-looks-correct-1-snap.png create mode 100644 storybook/stories/goal/18_side_gauge_inverted_angle_relation.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 8a76ca3c5f..9dd38f5505 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,6 +55,7 @@ module.exports = { 'unicorn/no-nested-ternary': 0, '@typescript-eslint/lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 'no-extra-parens': 'off', // it was already off by default; this line addition is just for documentation purposes + '@typescript-eslint/restrict-template-expressions': 0, // it's OK to use numbers etc. in string templates /** ***************************************** @@ -65,7 +66,6 @@ module.exports = { '@typescript-eslint/no-unsafe-member-access': 0, '@typescript-eslint/no-unsafe-return': 0, '@typescript-eslint/explicit-module-boundary-types': 0, - '@typescript-eslint/restrict-template-expressions': 1, '@typescript-eslint/restrict-plus-operands': 0, // rule is broken '@typescript-eslint/no-unsafe-call': 1, '@typescript-eslint/unbound-method': 1, 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 new file mode 100644 index 0000000000000000000000000000000000000000..9580a35d13939cae0231717c100e293e1778d27b GIT binary patch literal 13735 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 literal 0 HcmV?d00001 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 5e045eaa52..53be8cde02 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 @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { GOLDEN_RATIO } from '../../../../common/constants'; -import { PointObject } from '../../../../common/geometry'; +import { GOLDEN_RATIO, TAU } from '../../../../common/constants'; +import { PointObject, Radian, Rectangle } from '../../../../common/geometry'; import { cssFontShorthand, Font } from '../../../../common/text_utils'; import { GoalSubtype } from '../../specs/constants'; import { Config } from '../types/config_types'; @@ -22,9 +22,12 @@ const marginRatio = 0.05; // same ratio on each side const maxTickFontSize = 24; const maxLabelFontSize = 32; const maxCentralFontSize = 38; +const arcBoxSamplePitch: Radian = (5 / 360) * TAU; // 5-degree pitch ie. a circle is 72 steps +const capturePad = 16; // mouse hover is detected in the padding too; eg. for Fitts law /** @internal */ export interface Mark { + boundingBoxes: (ctx: CanvasRenderingContext2D) => Rectangle[]; render: (ctx: CanvasRenderingContext2D) => void; } @@ -46,6 +49,22 @@ export class Section implements Mark { this.strokeStyle = strokeStyle; } + boundingBoxes() { + // modifying with half the line width is a simple yet imprecise method for ensuring that the + // entire ink is in the bounding box; depending on orientation and line ending, the bounding + // box may overstate the data ink bounding box, which is preferable to understating it + return this.lineWidth === 0 + ? [] + : [ + { + x0: Math.min(this.x, this.xTo) - this.lineWidth / 2 - capturePad, + y0: Math.min(this.y, this.yTo) - this.lineWidth / 2 - capturePad, + x1: Math.max(this.x, this.xTo) + this.lineWidth / 2 + capturePad, + y1: Math.max(this.y, this.yTo) + this.lineWidth / 2 + capturePad, + }, + ]; + } + render(ctx: CanvasRenderingContext2D) { ctx.beginPath(); ctx.lineWidth = this.lineWidth; @@ -56,13 +75,16 @@ export class Section implements Mark { } } +/** @internal */ +export const initialBoundingBox = (): Rectangle => ({ x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity }); + /** @internal */ export class Arc implements Mark { protected readonly x: number; protected readonly y: number; protected readonly radius: number; - protected readonly startAngle: number; - protected readonly endAngle: number; + protected readonly startAngle: Radian; + protected readonly endAngle: Radian; protected readonly anticlockwise: boolean; protected readonly lineWidth: number; protected readonly strokeStyle: string; @@ -87,6 +109,49 @@ export class Arc implements Mark { this.strokeStyle = strokeStyle; } + boundingBoxes() { + if (this.lineWidth === 0) return []; + + const box = initialBoundingBox(); + + // instead of an analytical solution, we approximate with a GC-free grid sampler + + // full circle rotations such that `startAngle' and `endAngle` are positive + const rotationCount = Math.ceil(Math.max(0, -this.startAngle, -this.endAngle) / TAU); + const startAngle = this.startAngle + rotationCount * TAU; + const endAngle = this.endAngle + rotationCount * TAU; + + // snapping to the closest `arcBoxSamplePitch` increment + const angleFrom: Radian = Math.round(startAngle / arcBoxSamplePitch) * arcBoxSamplePitch; + const angleTo: Radian = Math.round(endAngle / arcBoxSamplePitch) * arcBoxSamplePitch; + const signedIncrement = arcBoxSamplePitch * Math.sign(angleTo - angleFrom); + + for (let angle: Radian = angleFrom; angle <= angleTo; angle += signedIncrement) { + // unit vector for the angle direction + const vx = Math.cos(angle); + const vy = Math.sin(angle); + const innerRadius = this.radius - this.lineWidth / 2; + const outerRadius = this.radius + this.lineWidth / 2; + + // inner point of the sector + const innerX = this.x + vx * innerRadius; + const innerY = this.y + vy * innerRadius; + + // outer point of the sector + const outerX = this.x + vx * outerRadius; + const outerY = this.y + vy * outerRadius; + + box.x0 = Math.min(box.x0, innerX - capturePad, outerX - capturePad); + box.y0 = Math.min(box.y0, innerY - capturePad, outerY - capturePad); + box.x1 = Math.max(box.x1, innerX + capturePad, outerX + capturePad); + box.y1 = Math.max(box.y1, innerY + capturePad, outerY + capturePad); + + if (signedIncrement === 0) break; // happens if fromAngle === toAngle + } + + return Number.isFinite(box.x0) ? [box] : []; + } + render(ctx: CanvasRenderingContext2D) { ctx.beginPath(); ctx.lineWidth = this.lineWidth; @@ -124,11 +189,30 @@ export class Text implements Mark { this.fontSize = fontSize; } - render(ctx: CanvasRenderingContext2D) { - ctx.beginPath(); + setCanvasTextState(ctx: CanvasRenderingContext2D) { ctx.textAlign = this.textAlign; ctx.textBaseline = this.textBaseline; ctx.font = cssFontShorthand(this.fontShape, this.fontSize); + } + + boundingBoxes(ctx: CanvasRenderingContext2D) { + if (this.text.length === 0) return []; + + this.setCanvasTextState(ctx); + const box = ctx.measureText(this.text); + return [ + { + x0: -box.actualBoundingBoxLeft + this.x - capturePad, + y0: -box.actualBoundingBoxAscent + this.y - capturePad, + x1: box.actualBoundingBoxRight + this.x + capturePad, + y1: box.actualBoundingBoxDescent + this.y + capturePad, + }, + ]; + } + + render(ctx: CanvasRenderingContext2D) { + this.setCanvasTextState(ctx); + ctx.beginPath(); ctx.fillText(this.text, this.x, this.y); } } diff --git a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts index f8c62f8027..b10bd049c1 100644 --- a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ -import { TextMeasure } from '../../../../common/text_utils'; import { GoalSpec } from '../../specs'; import { Config } from '../types/config_types'; import { BulletViewModel, PickFunction, ShapeViewModel } from '../types/viewmodel_types'; /** @internal */ -export function shapeViewModel(textMeasure: TextMeasure, spec: GoalSpec, config: Config): ShapeViewModel { +export function shapeViewModel(spec: GoalSpec, config: Config): ShapeViewModel { const { width, height, margin } = config; const innerWidth = width * (1 - Math.min(1, margin.left + margin.right)); diff --git a/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts b/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts index 43bed0d66b..273e718a12 100644 --- a/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts +++ b/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts @@ -33,9 +33,7 @@ export function renderCanvas2d(ctx: CanvasRenderingContext2D, dpr: number, geomO (context: CanvasRenderingContext2D) => clearCanvas(context, 200000, 200000), (context: CanvasRenderingContext2D) => - withContext(context, (ctx) => { - geomObjects.forEach((obj) => withContext(ctx, (ctx) => obj.render(ctx))); - }), + withContext(context, (ctx) => geomObjects.forEach((obj) => withContext(ctx, (ctx) => obj.render(ctx)))), ]); }); } diff --git a/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx index cd58887503..aa0f96b739 100644 --- a/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx @@ -10,6 +10,7 @@ import React, { MouseEvent, RefObject } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; +import { Rectangle } from '../../../../common/geometry'; import { GoalSemanticDescription, ScreenReaderSummary } from '../../../../components/accessibility'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; @@ -21,9 +22,10 @@ import { import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { Dimensions } from '../../../../utils/dimensions'; import { BandViewModel, nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; -import { Mark } from '../../layout/viewmodel/geoms'; +import { initialBoundingBox, Mark } from '../../layout/viewmodel/geoms'; import { geometries, getPrimitiveGeoms } from '../../state/selectors/geometries'; import { getFirstTickValueSelector, getGoalChartSemanticDataSelector } from '../../state/selectors/get_goal_chart_data'; +import { getCaptureBoundingBox } from '../../state/selectors/picked_shapes'; import { renderCanvas2d } from './canvas_renderers'; interface ReactiveChartStateProps { @@ -34,6 +36,7 @@ interface ReactiveChartStateProps { a11ySettings: A11ySettings; bandLabels: BandViewModel[]; firstValue: number; + captureBoundingBox: Rectangle; } interface ReactiveChartDispatchProps { @@ -89,6 +92,7 @@ class Component extends React.Component { chartContainerDimensions: { width, height }, forwardStageRef, geometries, + captureBoundingBox: capture, } = this.props; if (!forwardStageRef.current || !this.ctx || !initialized || width === 0 || height === 0) { return; @@ -96,9 +100,11 @@ class Component extends React.Component { const picker = geometries.pickQuads; const box = forwardStageRef.current.getBoundingClientRect(); const { chartCenter } = geometries; - const x = e.clientX - box.left - chartCenter.x; - const y = e.clientY - box.top - chartCenter.y; - return picker(x, y); + const x = e.clientX - box.left; + const y = e.clientY - box.top; + if (capture.x0 <= x && x <= capture.x1 && capture.y0 <= y && y <= capture.y1) { + return picker(x - chartCenter.x, y - chartCenter.y); + } } render() { @@ -168,6 +174,7 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { a11ySettings: DEFAULT_A11Y_SETTINGS, bandLabels: [], firstValue: 0, + captureBoundingBox: initialBoundingBox(), }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -182,6 +189,7 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { bandLabels: getGoalChartSemanticDataSelector(state), firstValue: getFirstTickValueSelector(state), geoms: getPrimitiveGeoms(state), + captureBoundingBox: getCaptureBoundingBox(state), }; }; diff --git a/packages/charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts b/packages/charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts index 84127eabfe..37a3fda004 100644 --- a/packages/charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts +++ b/packages/charts/src/chart_types/goal_chart/state/selectors/picked_shapes.ts @@ -6,25 +6,53 @@ * Side Public License, v 1. */ +import { Rectangle } from '../../../../common/geometry'; import { LayerValue } from '../../../../specs'; import { GlobalChartState } from '../../../../state/chart_state'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { BulletViewModel } from '../../layout/types/viewmodel_types'; -import { geometries } from './geometries'; +import { initialBoundingBox, Mark } from '../../layout/viewmodel/geoms'; +import { geometries, getPrimitiveGeoms } from './geometries'; function getCurrentPointerPosition(state: GlobalChartState) { return state.interactions.pointer.current.position; } +function fullBoundingBox(ctx: CanvasRenderingContext2D | null, geoms: Mark[]) { + const box = initialBoundingBox(); + if (ctx) { + for (const g of geoms) { + for (const { x0, y0, x1, y1 } of g.boundingBoxes(ctx)) { + box.x0 = Math.min(box.x0, x0, x1); + box.y0 = Math.min(box.y0, y0, y1); + box.x1 = Math.max(box.x1, x0, x1); + box.y1 = Math.max(box.y1, y0, y1); + } + } + } + return box; +} + +/** @internal */ +export const getCaptureBoundingBox = createCustomCachedSelector( + [getPrimitiveGeoms], + (geoms): Rectangle => { + const textMeasurer = document.createElement('canvas'); + const ctx = textMeasurer.getContext('2d'); + return fullBoundingBox(ctx, geoms); + }, +); + /** @internal */ export const getPickedShapes = createCustomCachedSelector( - [geometries, getCurrentPointerPosition], - (geoms, pointerPosition): BulletViewModel[] => { + [geometries, getCurrentPointerPosition, getCaptureBoundingBox], + (geoms, pointerPosition, capture): BulletViewModel[] => { const picker = geoms.pickQuads; const { chartCenter } = geoms; - const x = pointerPosition.x - chartCenter.x; - const y = pointerPosition.y - chartCenter.y; - return picker(x, y); + const { x, y } = pointerPosition; + return capture.x0 <= x && x <= capture.x1 && capture.y0 <= y && y <= capture.y1 + ? picker(x - chartCenter.x, y - chartCenter.y) + : []; }, ); @@ -32,7 +60,7 @@ export const getPickedShapes = createCustomCachedSelector( export const getPickedShapesLayerValues = createCustomCachedSelector( [getPickedShapes], (pickedShapes): Array> => { - const elements = pickedShapes.map>((model) => { + return pickedShapes.map>((model) => { const values: Array = []; values.push({ smAccessorValue: '', @@ -44,6 +72,5 @@ export const getPickedShapesLayerValues = createCustomCachedSelector( }); return values.reverse(); }); - return elements; }, ); diff --git a/packages/charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts b/packages/charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts index 1c9f10020a..6118628902 100644 --- a/packages/charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts +++ b/packages/charts/src/chart_types/goal_chart/state/selectors/scenegraph.ts @@ -6,12 +6,11 @@ * Side Public License, v 1. */ -import { measureText } from '../../../../common/text_utils'; import { mergePartial, RecursivePartial } from '../../../../utils/common'; import { Dimensions } from '../../../../utils/dimensions'; import { config as defaultConfig } from '../../layout/config/config'; import { Config } from '../../layout/types/config_types'; -import { ShapeViewModel, nullShapeViewModel } from '../../layout/types/viewmodel_types'; +import { ShapeViewModel } from '../../layout/types/viewmodel_types'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { GoalSpec } from '../../specs'; @@ -19,12 +18,7 @@ import { GoalSpec } from '../../specs'; export function render(spec: GoalSpec, parentDimensions: Dimensions): ShapeViewModel { const { width, height } = parentDimensions; const { config: specConfig } = spec; - const textMeasurer = document.createElement('canvas'); - const textMeasurerCtx = textMeasurer.getContext('2d'); const partialConfig: RecursivePartial = { ...specConfig, width, height }; const config: Config = mergePartial(defaultConfig, partialConfig, { mergeOptionalPartialValues: true }); - if (!textMeasurerCtx) { - return nullShapeViewModel(config, { x: width / 2, y: height / 2 }); - } - return shapeViewModel(measureText(textMeasurerCtx), spec, config); + return shapeViewModel(spec, config); } diff --git a/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts b/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts index 579859e4db..5210767e7e 100644 --- a/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts +++ b/packages/charts/src/chart_types/heatmap/layout/types/viewmodel_types.ts @@ -9,7 +9,7 @@ import { ChartType } from '../../..'; import { Pixels } from '../../../../common/geometry'; import { Box } from '../../../../common/text_utils'; -import { Fill, Line, Stroke } from '../../../../geoms/types'; +import { Fill, Line, Rect, Stroke } from '../../../../geoms/types'; import { Point } from '../../../../utils/point'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; import { config } from '../config/config'; @@ -27,9 +27,9 @@ export interface Value { export interface Cell { x: number; y: number; - yIndex: number; width: number; height: number; + yIndex: number; fill: Fill; stroke: Stroke; value: number; @@ -63,7 +63,7 @@ export interface HeatmapViewModel { } /** @internal */ -export function isPickedCells(v: any): v is Cell[] { +export function isPickedCells(v: unknown): v is Cell[] { return Array.isArray(v); } @@ -74,9 +74,7 @@ export type PickFunction = (x: Pixels, y: Pixels) => Cell[] | TextBox; export type PickDragFunction = (points: [Point, Point]) => HeatmapBrushEvent; /** @internal */ -export type PickDragShapeFunction = ( - points: [Point, Point], -) => { x: number; y: number; width: number; height: number } | null; +export type PickDragShapeFunction = (points: [Point, Point]) => Rect | null; /** * From x and y coordinates in the data domain space to a canvas projected rectangle @@ -86,9 +84,9 @@ export type PickDragShapeFunction = ( * @internal */ export type PickHighlightedArea = ( - x: any[], - y: any[], -) => { x: number; y: number; width: number; height: number } | null; + x: Array>, + y: Array>, +) => Rect | null; /** @internal */ export type DragShape = ReturnType; diff --git a/packages/charts/src/chart_types/partition_chart/layout/utils/sunburst.ts b/packages/charts/src/chart_types/partition_chart/layout/utils/sunburst.ts index e739c2b933..bc7576af54 100644 --- a/packages/charts/src/chart_types/partition_chart/layout/utils/sunburst.ts +++ b/packages/charts/src/chart_types/partition_chart/layout/utils/sunburst.ts @@ -6,7 +6,8 @@ * Side Public License, v 1. */ -import { Origin, Part } from '../../../../common/text_utils'; +import { Origin } from '../../../../common/geometry'; +import { Part } from '../../../../common/text_utils'; import { ArrayEntry, childrenAccessor, HierarchyOfArrays } from './group_by_rollup'; /** @internal */ diff --git a/packages/charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts b/packages/charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts index 7424b98007..a73d3a10d4 100644 --- a/packages/charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts +++ b/packages/charts/src/chart_types/wordcloud/layout/types/viewmodel_types.ts @@ -8,7 +8,7 @@ import { $Values as Values } from 'utility-types'; -import { Pixels, PointObject } from '../../../../common/geometry'; +import { Pixels, PointObject, Rectangle } from '../../../../common/geometry'; import { Color } from '../../../../utils/common'; import { config } from '../config/config'; import { Config } from './config_types'; @@ -32,7 +32,7 @@ export const WeightFn = Object.freeze({ export type WeightFn = Values; /** @internal */ -export interface Word { +export interface Word extends Rectangle { color: string; font: string; fontFamily: string; @@ -46,12 +46,8 @@ export interface Word { text: string; weight: number; x: number; - x0: number; - x1: number; xoff: number; y: number; - y0: number; - y1: number; yoff: number; datum: WordModel; } diff --git a/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts b/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts index 2e22f95b38..1b81edf12f 100644 --- a/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts +++ b/packages/charts/src/chart_types/xy_chart/annotations/rect/dimensions.integration.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Rect } from '../../../../geoms/types'; import { MockSeriesSpec, MockAnnotationSpec, MockGlobalSpec } from '../../../../mocks/specs'; import { MockStore } from '../../../../mocks/store'; import { ScaleType } from '../../../../scales/constants'; @@ -17,12 +18,7 @@ function expectAnnotationAtPosition( data: Array<{ x: number; y: number }>, type: 'line' | 'bar' | 'histogram', dataValues: RectAnnotationDatum[], - expectedRect: { - x: number; - y: number; - width: number; - height: number; - }, + expectedRect: Rect, numOfSpecs = 1, xScaleType: typeof ScaleType.Ordinal | typeof ScaleType.Linear | typeof ScaleType.Time = ScaleType.Linear, ) { diff --git a/packages/charts/src/chart_types/xy_chart/annotations/rect/types.ts b/packages/charts/src/chart_types/xy_chart/annotations/rect/types.ts index 359c59e6e8..399c8f9bd0 100644 --- a/packages/charts/src/chart_types/xy_chart/annotations/rect/types.ts +++ b/packages/charts/src/chart_types/xy_chart/annotations/rect/types.ts @@ -6,19 +6,13 @@ * Side Public License, v 1. */ +import { Rect } from '../../../../geoms/types'; import { Dimensions } from '../../../../utils/dimensions'; import { RectAnnotationDatum } from '../../utils/specs'; -/** - * @internal - */ +/** @internal */ export interface AnnotationRectProps { datum: RectAnnotationDatum; - rect: { - x: number; - y: number; - width: number; - height: number; - }; + rect: Rect; panel: Dimensions; } diff --git a/packages/charts/src/common/constants.ts b/packages/charts/src/common/constants.ts index f729af2814..7528b1613e 100644 --- a/packages/charts/src/common/constants.ts +++ b/packages/charts/src/common/constants.ts @@ -8,17 +8,11 @@ /** @internal */ export const DEFAULT_CSS_CURSOR = 'default'; -/** - * @internal - */ +/** @internal */ export const TAU = 2 * Math.PI; -/** - * @internal - */ +/** @internal */ export const RIGHT_ANGLE = TAU / 4; -/** - * @internal - */ +/** @internal */ export const GOLDEN_RATIO = 1.618; /** @public */ diff --git a/packages/charts/src/common/geometry.ts b/packages/charts/src/common/geometry.ts index 1572f23c83..5f2c17d3f0 100644 --- a/packages/charts/src/common/geometry.ts +++ b/packages/charts/src/common/geometry.ts @@ -96,3 +96,15 @@ export function meanAngle(a: Radian, b: Radian) { export function trueBearingToStandardPositionAngle(alphaIn: number) { return wrapToTau(RIGHT_ANGLE - alphaIn); } + +/** @internal */ +export interface Origin { + x0: number; + y0: number; +} + +/** @internal */ +export interface Rectangle extends Origin { + x1: number; + y1: number; +} diff --git a/packages/charts/src/common/text_utils.ts b/packages/charts/src/common/text_utils.ts index 8f59108922..6843b4c048 100644 --- a/packages/charts/src/common/text_utils.ts +++ b/packages/charts/src/common/text_utils.ts @@ -11,39 +11,46 @@ import { $Values as Values } from 'utility-types'; import { ArrayEntry } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; import { integerSnap, monotonicHillClimb } from '../solvers/monotonic_hill_climb'; import { Datum } from '../utils/common'; -import { Pixels } from './geometry'; +import { Pixels, Rectangle } from './geometry'; +const FONT_WEIGHTS_NUMERIC = [100, 200, 300, 400, 500, 600, 700, 800, 900]; +const FONT_WEIGHTS_ALPHA = ['normal', 'bold', 'lighter', 'bolder', 'inherit', 'initial', 'unset']; + +/** @public */ +export type TextContrast = boolean | number; +/** + * todo consider doing tighter control for permissible font families, eg. as in Kibana Canvas - expression language + * - though the same applies for permissible (eg. known available or loaded) font weights, styles, variants... + * @public + */ +export type FontFamily = string; +/** @public */ +export const FONT_WEIGHTS = Object.freeze([...FONT_WEIGHTS_NUMERIC, ...FONT_WEIGHTS_ALPHA] as const); /** @public */ export const FONT_VARIANTS = Object.freeze(['normal', 'small-caps'] as const); /** @public */ export type FontVariant = typeof FONT_VARIANTS[number]; /** @public */ -export const FONT_WEIGHTS = Object.freeze([ - 100, - 200, - 300, - 400, - 500, - 600, - 700, - 800, - 900, - 'normal', - 'bold', - 'lighter', - 'bolder', - 'inherit', - 'initial', - 'unset', -] as const); -/** @public */ export type FontWeight = typeof FONT_WEIGHTS[number]; -/** @internal */ -export type NumericFontWeight = number & typeof FONT_WEIGHTS[number]; /** @public */ export const FONT_STYLES = Object.freeze(['normal', 'italic', 'oblique', 'inherit', 'initial', 'unset'] as const); /** @public */ export type FontStyle = typeof FONT_STYLES[number]; +/** @public */ +export type PartialFont = Partial; +/** @public */ +export const TEXT_ALIGNS = Object.freeze(['start', 'end', 'left', 'right', 'center'] as const); +/** @public */ +export type TextAlign = typeof TEXT_ALIGNS[number]; +/** @public */ +export type TextBaseline = typeof TEXT_BASELINE[number]; + +/** @internal */ +export type VerticalAlignments = Values; +/** @internal */ +export type Relation = Array; +/** @internal */ +export type TextMeasure = (fontSize: number, boxes: Box[]) => TextMetrics[]; /** * this doesn't include the font size, so it's more like a font face (?) - unfortunately all vague terms @@ -58,12 +65,6 @@ export interface Font { textOpacity: number; } -/** @public */ -export type PartialFont = Partial; -/** @public */ -export const TEXT_ALIGNS = Object.freeze(['start', 'end', 'left', 'right', 'center'] as const); -/** @public */ -export type TextAlign = typeof TEXT_ALIGNS[number]; /** @public */ export const TEXT_BASELINE = Object.freeze([ 'top', @@ -73,41 +74,17 @@ export const TEXT_BASELINE = Object.freeze([ 'ideographic', 'bottom', ] as const); -/** @public */ -export type TextBaseline = typeof TEXT_BASELINE[number]; -/** - * @internal - */ +/** @internal */ export interface Box extends Font { text: string; } -/** @internal */ -export type Relation = Array; - -/** @internal */ -export interface Origin { - x0: number; - y0: number; -} - -/** @internal */ -export interface Rectangle extends Origin { - x1: number; - y1: number; -} - /** @internal */ export interface Part extends Rectangle { node: ArrayEntry; } -/** - * @internal - */ -export type TextMeasure = (fontSize: number, boxes: Box[]) => TextMetrics[]; - /** @internal */ export function cssFontShorthand({ fontStyle, fontVariant, fontWeight, fontFamily }: Font, fontSize: Pixels) { return `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}px ${fontFamily}`; @@ -122,18 +99,6 @@ export function measureText(ctx: CanvasRenderingContext2D): TextMeasure { }); } -/** - * todo consider doing tighter control for permissible font families, eg. as in Kibana Canvas - expression language - * - though the same applies for permissible (eg. known available or loaded) font weights, styles, variants... - * @public - */ -export type FontFamily = string; - -/** - * @public - */ -export type TextContrast = boolean | number; - /** @internal */ export const VerticalAlignments = Object.freeze({ top: 'top' as const, @@ -144,9 +109,6 @@ export const VerticalAlignments = Object.freeze({ ideographic: 'ideographic' as const, }); -/** @internal */ -export type VerticalAlignments = Values; - /** @internal */ export function measureOneBoxWidth(measure: TextMeasure, fontSize: number, box: Box) { return measure(fontSize, [box])[0].width; diff --git a/storybook/stories/goal/18_side_gauge_inverted_angle_relation.tsx b/storybook/stories/goal/18_side_gauge_inverted_angle_relation.tsx new file mode 100644 index 0000000000..0e62be76e2 --- /dev/null +++ b/storybook/stories/goal/18_side_gauge_inverted_angle_relation.tsx @@ -0,0 +1,46 @@ +/* + * 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 React from 'react'; + +import { Chart, Goal, Color, BandFillColorAccessorInput, Settings } from '@elastic/charts'; +import { GoalSubtype } from '@elastic/charts/src/chart_types/goal_chart/specs/constants'; + +import { useBaseTheme } from '../../use_base_theme'; + +const subtype = GoalSubtype.Goal; + +const colorMap: { [k: number]: Color } = { + 200: '#fc8d62', + 250: 'lightgrey', + 300: '#66c2a5', +}; + +const bandFillColor = (x: number): Color => colorMap[x]; + +export const Example = () => ( + + + String(value)} + bandFillColor={({ value }: BandFillColorAccessorInput) => bandFillColor(value)} + labelMajor="" + labelMinor="" + centralMajor="280 MB/s" + centralMinor="" + config={{ angleEnd: -(Math.PI - (2 * Math.PI) / 3) / 2, angleStart: (Math.PI - (2 * Math.PI) / 3) / 2 }} + /> + +); diff --git a/storybook/stories/goal/goal.stories.tsx b/storybook/stories/goal/goal.stories.tsx index 532ead7144..519d9a2ce6 100644 --- a/storybook/stories/goal/goal.stories.tsx +++ b/storybook/stories/goal/goal.stories.tsx @@ -34,6 +34,7 @@ export { Example as threeQuarters } from './17_three_quarters'; export { Example as fullCircle } from './17_total_circle'; export { Example as smallGap } from './17_very_small_gap'; export { Example as sideGauge } from './18_side_gauge'; +export { Example as sideGaugeInverted } from './18_side_gauge_inverted_angle_relation'; export { Example as horizontalNegative } from './19_horizontal_negative'; export { Example as verticalNegative } from './20_vertical_negative'; export { Example as goalNegative } from './21_goal_negative';