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 0000000000..9580a35d13 Binary files /dev/null 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/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';