From 03f3ed53341216cbdaf8e13599ee987d04875c3e Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 11 May 2023 17:53:23 +0200 Subject: [PATCH 1/7] WIP --- .../chart_types/bullet_graph/chart_state.tsx | 56 ++++ .../bullet_graph/renderer/_header.scss | 70 ++++ .../bullet_graph/renderer/_index.scss | 65 ++++ .../bullet_graph/renderer/bullet.tsx | 300 ++++++++++++++++++ .../bullet_graph/renderer/header.tsx | 25 ++ .../bullet_graph/renderer/headertest.html | 112 +++++++ .../bullet_graph/renderer/index.tsx | 213 +++++++++++++ .../bullet_graph/renderer/test.html | 266 ++++++++++++++++ .../bullet_graph/selectors/chart_size.ts | 25 ++ .../src/chart_types/bullet_graph/spec.ts | 98 ++++++ .../src/chart_types/bullet_graph/theme.ts | 49 +++ packages/charts/src/chart_types/index.ts | 1 + packages/charts/src/chart_types/specs.ts | 2 + packages/charts/src/components/_index.scss | 3 + .../charts/src/components/chart_resizer.tsx | 2 +- .../src/components/grid/_aligned_grid.scss | 23 ++ .../charts/src/components/grid/_index.scss | 25 ++ .../src/components/grid/aligned_grid.tsx | 77 +++++ packages/charts/src/components/grid/grid.html | 117 +++++++ .../charts/src/components/grid/html_grid.tsx | 129 ++++++++ packages/charts/src/state/chart_state.ts | 2 + .../utils/bbox/canvas_text_bbox_calculator.ts | 4 +- .../charts/src/utils/themes/dark_theme.ts | 2 + .../charts/src/utils/themes/light_theme.ts | 2 + packages/charts/src/utils/themes/theme.ts | 7 + .../stories/bullet_graph/1_simple.story.tsx | 82 +++++ .../bullet_graph/bullet_graph.stories.tsx | 13 + 27 files changed, 1767 insertions(+), 3 deletions(-) create mode 100644 packages/charts/src/chart_types/bullet_graph/chart_state.tsx create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/_header.scss create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/_index.scss create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/header.tsx create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/headertest.html create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/index.tsx create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/test.html create mode 100644 packages/charts/src/chart_types/bullet_graph/selectors/chart_size.ts create mode 100644 packages/charts/src/chart_types/bullet_graph/spec.ts create mode 100644 packages/charts/src/chart_types/bullet_graph/theme.ts create mode 100644 packages/charts/src/components/grid/_aligned_grid.scss create mode 100644 packages/charts/src/components/grid/_index.scss create mode 100644 packages/charts/src/components/grid/aligned_grid.tsx create mode 100644 packages/charts/src/components/grid/grid.html create mode 100644 packages/charts/src/components/grid/html_grid.tsx create mode 100644 storybook/stories/bullet_graph/1_simple.story.tsx create mode 100644 storybook/stories/bullet_graph/bullet_graph.stories.tsx diff --git a/packages/charts/src/chart_types/bullet_graph/chart_state.tsx b/packages/charts/src/chart_types/bullet_graph/chart_state.tsx new file mode 100644 index 0000000000..75358a8c0f --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/chart_state.tsx @@ -0,0 +1,56 @@ +/* + * 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 { BulletGraphRenderer } from './renderer'; +import { ChartType } from '../..'; +import { DEFAULT_CSS_CURSOR } from '../../common/constants'; +import { LegendItem } from '../../common/legend'; +import { InternalChartState } from '../../state/chart_state'; +import { InitStatus } from '../../state/selectors/get_internal_is_intialized'; +import { LegendItemLabel } from '../../state/selectors/get_legend_items_labels'; + +const EMPTY_MAP = new Map(); +const EMPTY_LEGEND_LIST: LegendItem[] = []; +const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = []; + +/** @internal */ +export class BulletGraphState implements InternalChartState { + chartType = ChartType.BulletGraph; + getChartTypeDescription = () => 'Bullet Graph'; + chartRenderer = () => ; + isInitialized = () => InitStatus.Initialized; + isBrushAvailable = () => false; + isBrushing = () => false; + isChartEmpty = () => false; + getLegendItems = () => EMPTY_LEGEND_LIST; + getLegendItemsLabels = () => EMPTY_LEGEND_ITEM_LIST; + getLegendExtraValues = () => EMPTY_MAP; + getPointerCursor = () => DEFAULT_CSS_CURSOR; + isTooltipVisible = () => ({ + visible: false, + isExternal: false, + displayOnly: false, + isPinnable: false, + }); + + getTooltipInfo = () => undefined; + getTooltipAnchor = () => null; + eventCallbacks = () => {}; + getProjectionContainerArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); + getMainProjectionArea = () => ({ width: 0, height: 0, top: 0, left: 0 }); + getBrushArea = () => null; + getDebugState = () => ({}); + getSmallMultiplesDomains() { + return { + smHDomain: [], + smVDomain: [], + }; + } +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss b/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss new file mode 100644 index 0000000000..fcd59e8420 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss @@ -0,0 +1,70 @@ +.echBulletGraphHeader { + position: relative; + padding: 8px; + border-bottom: 1px solid #D3DAE6; + + &__title { + font-weight: bold; + font-size: 16px; + line-height: 20px; + color: $euiTextColor; + } + &__subtitle { + font-weight: normal; + font-size: 14px; + line-height: 17px; + color: $euiColorDarkShade; + } + display: flex; + flex-direction: column; + flex-wrap: nowrap; + +} + +.echBulletGraphHeader--single { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + align-items: flex-end; + width: 100%; + overflow: hidden; + .echBulletGraphHeader__title { + @include lineClamp(2); + } + padding-bottom: 4px; +} + +.echBulletGraphHeader--multi { + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: space-between; + height: 100%; + .echBulletGraphHeader__title { + width: 100%; + @include lineClamp(2); + } + .echBulletGraphHeader__subtitle { + @include lineClamp(1); + flex-grow: 1; + flex-shrink: 1; + } + padding-bottom: 4px; +} + + +.echBulletHeader__valueContainer { + white-space: nowrap; + flex-grow: 0; + flex-shrink: 0; + float:right; +} +.echBulletHeader__value { + font-weight: 700; + font-size: 22px; +} +.echBulletHeader__target { + font-weight: 500; + font-size: 16px; +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss b/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss new file mode 100644 index 0000000000..779612baa2 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss @@ -0,0 +1,65 @@ +@import "header"; + +.echBulletGraphContainer { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + align-items: stretch; + svg { + overflow: visible; + rect { + stroke: none; + } + } +} + +.echBulletGraphSVG--container { + flex:1; + padding:10px; + height: 100%; + overflow: visible; + position: relative; +} +.echBulletGraphSVG--container svg { + overflow: visible; +} +.echBulletGraph { + + + .echBullet--tickLabel { + font-size: 10px; + fill: $euiTextColor; + } + + .echBullet--tickLabel { + font-size: 10px; + fill: $euiTextColor; + } + .echBullet--target { + fill: $euiTextColor; + } + .echBullet--bar { + fill: $euiTextColor; + } + .echBullet--tick { + stroke: $euiPageBackgroundColor; + } + .echBullet--angularTick { + fill: $euiPageBackgroundColor; + } + + .echBullet--angularBar { + stroke: $euiTextColor; + } + .echBullet--angularTick { + stroke: $euiTextColor; + } + .echBullet--angularTickLabel { + fill: $euiTextColor; + } + +} + + + diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx new file mode 100644 index 0000000000..623eaea147 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx @@ -0,0 +1,300 @@ +/* + * 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 { clamp } from 'lodash'; +import React, { memo } from 'react'; + +import { Color } from '../../../common/colors'; +import { Ratio } from '../../../common/geometry'; +import { Size } from '../../../utils/dimensions'; +import { BulletDatum } from '../spec'; + +/** @internal */ +export interface BulletProps { + datum: BulletDatum; + colorBands: Array<{ color: Color; height: number; y: number }>; + + value: Ratio; + // base: Ratio; + target: Ratio; + + ticks: Ratio[]; + labels: Array<{ text: string; position: Ratio }>; + size: Size; +} + +const ratioToPercent = (value: Ratio) => `${Number(clamp(value * 100, 0, 100).toFixed(3))}%`; + +const TARGET_SIZE = 40; +const BULLET_SIZE = 32; +const BAR_SIZE = 12; + +function VerticalBulletComp(props: BulletProps) { + return ( +
+ + + {props.colorBands.map((band, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + {/* {Number.isFinite(props.base) && ( */} + {/* */} + {/* */} + {/* */} + {/* )} */} + + {props.ticks + .filter((tick) => tick > 0 && tick < 1) + .map((tick) => ( + + ))} + + + {Number.isFinite(props.target) && ( + + )} + + {props.labels.map((label) => { + return ( + + {label.text} + + ); + })} + + +
+ ); +} + +/** @internal */ +export const VerticalBullet = memo(VerticalBulletComp); + +/** @internal */ +export function HorizontalBulletComp(props: BulletProps) { + return ( +
+ + + {props.colorBands.map((band, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + {/* {Number.isFinite(props.base) && ( */} + {/* */} + {/* */} + {/* */} + {/* )} */} + + {props.ticks + .filter((tick) => tick > 0 && tick < 1) + .map((tick) => ( + + ))} + + + + {Number.isFinite(props.target) && ( + + )} + + {props.labels.map((label) => { + return ( + + {label.text} + + ); + })} + + +
+ ); +} + +/** @internal */ +export const HorizontalBullet = memo(HorizontalBulletComp); + +function polarToCartesian(centerX: number, centerY: number, radius: number, angleInDegrees: number) { + const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0; + + return { + x: centerX + radius * Math.cos(angleInRadians), + y: centerY + radius * Math.sin(angleInRadians), + }; +} + +function describeArc(x: number, y: number, radius: number, startAngle: number, endAngle: number) { + const start = polarToCartesian(x, y, radius, endAngle); + const end = polarToCartesian(x, y, radius, startAngle); + + const arcSweep = endAngle - startAngle <= 180 ? '0' : '1'; + + return ['M', start.x, start.y, 'A', radius, radius, 0, arcSweep, 0, end.x, end.y].join(' '); +} + +function scaleToAngle(unit: number): number { + return 360 / unit; +} + +/** @internal */ +export function AngularBulletComp(props: BulletProps) { + return ( +
+ + + {props.colorBands.map((band, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + {props.ticks + .filter((tick) => tick > 0 && tick < 1) + .map((tick) => ( + + ))} + + + + {Number.isFinite(props.target) && ( + + )} + + + {/* */} + {/* */} + {/* {props.labels.map((label) => { */} + {/* const coord = polarToCartesian(50, 10, 45, 240 * label.position - 120); */} + {/* return ( */} + {/* 0.5 ? 'end' : 'start'} */} + {/* > */} + {/* {label.text} */} + {/* */} + {/* ); */} + {/* })} */} + {/* */} + {/* */} +
+ ); +} + +/** @internal */ +export const AngularBullet = memo(AngularBulletComp); diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx new file mode 100644 index 0000000000..4da17fb0fa --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx @@ -0,0 +1,25 @@ +/* + * 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'; + +/** @internal */ +export function Header(props: { title: string; subtitle: string; value: string; target?: string }) { + return ( +
+

{props.title}

+
+

{props.subtitle}

+

+ {props.value} + {props.target ? / {props.target} : ''} +

+
+
+ ); +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html b/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html new file mode 100644 index 0000000000..1f06164bd5 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html @@ -0,0 +1,112 @@ + + + + + Title + + + + +
+

Some text, a header perhaps? But this is the next that will sit under the image, sometimes it's a p tag.

+ +

Some more random text that would appear in the messagebox this could go on for a few lines.

+
+ + + diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx new file mode 100644 index 0000000000..4ca4b8f720 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx @@ -0,0 +1,213 @@ +/* + * 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. + */ + +/* eslint-disable-next-line eslint-comments/disable-enable-pair */ +/* eslint-disable react/no-array-index-key */ + +import { scaleLinear } from 'd3-scale'; +import { size } from 'lodash'; +import React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { VerticalBullet, HorizontalBullet, AngularBullet } from './bullet'; +import { Header } from './header'; +import { Color } from '../../../common/colors'; +import { AlignedGrid } from '../../../components/grid/aligned_grid'; +import { BasicListener, ElementClickListener, ElementOverListener } from '../../../specs/settings'; +import { onChartRendered } from '../../../state/actions/chart'; +import { GlobalChartState } from '../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../state/selectors/get_accessibility_config'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../state/selectors/get_internal_is_intialized'; +import { Size } from '../../../utils/dimensions'; +import { Metric } from '../../metric/renderer/dom/metric'; +import { chartSize, getBulletGraphSpec } from '../selectors/chart_size'; +import { BulletDatum, BulletGraphSpec, BulletGraphSubtype } from '../spec'; + +interface StateProps { + initialized: boolean; + chartId: string; + specs: BulletGraphSpec[]; + a11y: A11ySettings; + size: Size; + onElementClick?: ElementClickListener; + onElementOut?: BasicListener; + onElementOver?: ElementOverListener; +} + +interface DispatchProps { + onChartRendered: typeof onChartRendered; +} + +const Component: React.FC = ({ + initialized, + size, + a11y, + specs: [spec], // ignoring other specs +}) => { + if (!initialized || !spec) { + return null; + } + + const { data, subtype } = spec; + const BulletComponent = + subtype === 'horizontal' ? HorizontalBullet : subtype === 'vertical' ? VerticalBullet : AngularBullet; + + return ( + + data={data} + headerComponent={({ datum, stats }) => { + return switchToMetric(size, stats.rows, stats.columns, subtype) ? null : ( +
+ ); + }} + contentComponent={({ datum, stats }) => { + // TODO move to the bullet SVG + const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; + const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, 1]); + // @ts-ignore + const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(['#D9C6EF', '#AA87D1']); + const maxTicks = + subtype === 'horizontal' + ? maxHorizontalTick(size.width, stats.columns) + : maxVerticalTick(size.height, stats.rows); + const colorTicks = scale.ticks(maxTicks - 1); + const colorBandSize = 1 / colorTicks.length; + const { colors } = colorTicks.reduce<{ + last: number; + colors: Array<{ color: Color; height: number; y: number }>; + }>( + (acc, tick) => { + return { + last: acc.last + colorBandSize, + colors: [ + ...acc.colors, + { + color: `${colorScale(tick)}`, + height: colorBandSize, + y: acc.last, + }, + ], + }; + }, + { last: 0, colors: [] }, + ); + + return switchToMetric(size, stats.rows, stats.columns, subtype) ? ( + + ) : ( + { + return { + text: datum.tickFormatter(tick), + position: scale(tick), + }; + })} + /> + ); + }} + /> + ); +}; + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: StateProps = { + initialized: false, + chartId: '', + specs: [], + size: { + width: 0, + height: 0, + }, + a11y: DEFAULT_A11Y_SETTINGS, +}; + +const mapStateToProps = (state: GlobalChartState): StateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + // const { onElementClick, onElementOut, onElementOver } = getSettingsSpecSelector(state); + return { + initialized: true, + chartId: state.chartId, + specs: getBulletGraphSpec(state), + size: chartSize(state), + a11y: getA11ySettingsSelector(state), + // onElementClick, + // onElementOver, + // onElementOut, + }; +}; + +/** @internal */ +export const BulletGraphRenderer = connect(mapStateToProps, mapDispatchToProps)(Component); + +function maxHorizontalTick(panelWidth: number, columns: number) { + return panelWidth / columns > 250 ? 4 : 3; +} +function maxVerticalTick(panelHeight: number, rows: number) { + return panelHeight / rows > 200 ? 4 : 3; +} + +function switchToMetric(size: Size, rows: number, columns: number, subtype: BulletGraphSubtype) { + switch (subtype) { + case BulletGraphSubtype.horizontal: + return size.width / columns < 200 || size.height / rows < 100; + default: + return size.width / columns < 150 || size.height / rows < 150; + } +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/test.html b/packages/charts/src/chart_types/bullet_graph/renderer/test.html new file mode 100644 index 0000000000..1209a82951 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/test.html @@ -0,0 +1,266 @@ + + + + + Title + + + + + + +
+ + + + + + + + + + + + + + + + + + +
font sizetexthtml sizecanvas textcanvas sizediff
+ + + + diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/chart_size.ts b/packages/charts/src/chart_types/bullet_graph/selectors/chart_size.ts new file mode 100644 index 0000000000..59cf3829da --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/chart_size.ts @@ -0,0 +1,25 @@ +/* + * 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 { ChartType } from '../../../chart_types'; +import { BulletGraphSpec } from '../../../chart_types/bullet_graph/spec'; +import { SpecType } from '../../../specs'; +import { GlobalChartState } from '../../../state/chart_state'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getSpecsByType } from '../../../state/selectors/get_specs_by_type'; +import { Dimensions } from '../../../utils/dimensions'; + +const getParentDimension = (state: GlobalChartState) => state.parentDimensions; + +/** @internal */ +export const chartSize = createCustomCachedSelector([getParentDimension], (container): Dimensions => { + return { ...container }; +}); + +/** @internal */ +export const getBulletGraphSpec = getSpecsByType(ChartType.BulletGraph, SpecType.Series); diff --git a/packages/charts/src/chart_types/bullet_graph/spec.ts b/packages/charts/src/chart_types/bullet_graph/spec.ts new file mode 100644 index 0000000000..295ed1e14c --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/spec.ts @@ -0,0 +1,98 @@ +/* + * 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 { ComponentProps } from 'react'; +import { $Values } from 'utility-types'; + +import { ChartType } from '../../chart_types/index'; +import { Spec } from '../../specs'; +import { SpecType } from '../../specs/constants'; +import { buildSFProps, SFProps, useSpecFactory } from '../../state/spec_factory'; +import { stripUndefined } from '../../utils/common'; + +/** @internal */ +export interface BulletDatum { + title: string; + subtitle?: string; + value: number; + valueFormatter: (value: number) => string; + target?: number; + domain: { min: number; max: number; nice: boolean }; + ticks: 'auto' | number[]; + tickFormatter: (value: number) => string; +} + +/** @public */ +export const BulletGraphSubtype = Object.freeze({ + vertical: 'vertical' as const, + horizontal: 'horizontal' as const, + angular: 'angular' as const, +}); +/** @public */ +export type BulletGraphSubtype = $Values; + +/** @alpha */ +export interface BulletGraphSpec extends Spec { + specType: typeof SpecType.Series; + chartType: typeof ChartType.BulletGraph; + data: (BulletDatum | undefined)[][]; + subtype: BulletGraphSubtype; +} + +const defaultBulletGraph = {}; + +const buildProps = buildSFProps()( + { + specType: SpecType.Series, + chartType: ChartType.BulletGraph, + }, + { + ...defaultBulletGraph, + }, +); + +/** + * Add Goal spec to chart + * @alpha + */ +export const BulletGraph = function ( + props: SFProps< + BulletGraphSpec, + 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 = {}; + // + // 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 BulletGraphProps = ComponentProps; diff --git a/packages/charts/src/chart_types/bullet_graph/theme.ts b/packages/charts/src/chart_types/bullet_graph/theme.ts new file mode 100644 index 0000000000..81553e57b0 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/theme.ts @@ -0,0 +1,49 @@ +/* + * 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 { Color } from '../../common/colors'; +import { Pixels } from '../../common/geometry'; + +/** @public */ +export interface BulletGraphStyle { + text: { + darkColor: Color; + lightColor: Color; + }; + border: Color; + background: Color; + barBackground: Color; + nonFiniteText: string; + minHeight: Pixels; +} + +/** @internal */ +export const LIGHT_THEME_BULLET_STYLE: BulletGraphStyle = { + text: { + lightColor: '#E0E5EE', + darkColor: '#343741', + }, + border: '#EDF0F5', + barBackground: '#343741', + background: '#FFFFFF', + nonFiniteText: 'N/A', + minHeight: 64, +}; + +/** @internal */ +export const DARK_THEME_BULLET_STYLE: BulletGraphStyle = { + text: { + lightColor: '#E0E5EE', + darkColor: '#343741', + }, + border: '#343741', + barBackground: '#343741', + background: '#1D1E23', + nonFiniteText: 'N/A', + minHeight: 64, +}; diff --git a/packages/charts/src/chart_types/index.ts b/packages/charts/src/chart_types/index.ts index 2726dc7ce7..d53b31f064 100644 --- a/packages/charts/src/chart_types/index.ts +++ b/packages/charts/src/chart_types/index.ts @@ -22,6 +22,7 @@ export const ChartType = Object.freeze({ Heatmap: 'heatmap' as const, Wordcloud: 'wordcloud' as const, Metric: 'metric' as const, + BulletGraph: 'bullet_graph' as const, }); /** @public */ export type ChartType = $Values; diff --git a/packages/charts/src/chart_types/specs.ts b/packages/charts/src/chart_types/specs.ts index 68ef7dd098..881ffdb9b8 100644 --- a/packages/charts/src/chart_types/specs.ts +++ b/packages/charts/src/chart_types/specs.ts @@ -43,3 +43,5 @@ export { MetricTrendShape, MetricDatum, } from './metric/specs'; + +export { BulletGraph, BulletGraphSpec, BulletGraphSubtype } from './bullet_graph/spec'; diff --git a/packages/charts/src/components/_index.scss b/packages/charts/src/components/_index.scss index ce2091babf..f752ffe4fd 100644 --- a/packages/charts/src/components/_index.scss +++ b/packages/charts/src/components/_index.scss @@ -6,7 +6,10 @@ @import 'icons/index'; @import 'legend/index'; @import 'unavailable_chart'; +@import 'grid'; +@import 'grid/aligned_grid'; @import '../chart_types/xy_chart/renderer/index'; @import '../chart_types/partition_chart/renderer/index'; @import '../chart_types/metric/renderer/index'; +@import '../chart_types/bullet_graph/renderer/index'; diff --git a/packages/charts/src/components/chart_resizer.tsx b/packages/charts/src/components/chart_resizer.tsx index b3c4ac8f0a..4c2ddd6df8 100644 --- a/packages/charts/src/components/chart_resizer.tsx +++ b/packages/charts/src/components/chart_resizer.tsx @@ -28,7 +28,7 @@ interface ResizerDispatchProps { type ResizerProps = ResizerStateProps & ResizerDispatchProps; -const DEFAULT_RESIZE_DEBOUNCE = 200; +const DEFAULT_RESIZE_DEBOUNCE = 0; class Resizer extends React.Component { private initialResizeComplete = false; diff --git a/packages/charts/src/components/grid/_aligned_grid.scss b/packages/charts/src/components/grid/_aligned_grid.scss new file mode 100644 index 0000000000..03bfc5ba11 --- /dev/null +++ b/packages/charts/src/components/grid/_aligned_grid.scss @@ -0,0 +1,23 @@ + +.echAlignedGrid { + display: grid; + align-content: stretch; + width: 100%; + height: 100%; + +} +.echAlignedGrid--header { + width: 100%; + height: 100%; + margin: 0; + padding:0; + border-right: 1px solid #EDF0F5; +} +.echAlignedGrid--content { + width: 100%; + min-height: 0; + margin: 0; + padding:0; + border-right: 1px solid #EDF0F5; + border-bottom: 1px solid #EDF0F5; +} diff --git a/packages/charts/src/components/grid/_index.scss b/packages/charts/src/components/grid/_index.scss new file mode 100644 index 0000000000..0829b28c31 --- /dev/null +++ b/packages/charts/src/components/grid/_index.scss @@ -0,0 +1,25 @@ +.echGridContainer { + display: grid; + width: 100%; + height: 100%; + align-content: start; + justify-content: stretch; + align-items: stretch; + user-select: text; +} + +.echGridCell { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + transition: background-color ease-in-out 0.1s; + + &--rightBorder { + border-right: 1px solid #343741; + } + + &--bottomBorder { + border-bottom: 1px solid #343741; + } +} diff --git a/packages/charts/src/components/grid/aligned_grid.tsx b/packages/charts/src/components/grid/aligned_grid.tsx new file mode 100644 index 0000000000..837b207775 --- /dev/null +++ b/packages/charts/src/components/grid/aligned_grid.tsx @@ -0,0 +1,77 @@ +/* + * 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 { grid } from 'charts-storybook/stories/metric/metric.stories'; +import React, { ComponentType, CSSProperties, FC, PropsWithChildren } from 'react'; + +import { Size } from '../../utils/dimensions'; + +interface AlignedGridProps { + data: Array>; + headerComponent: ComponentType<{ + datum: D; + stats: { rows: number; rowIndex: number; columns: number; columnIndex: number }; + }>; + contentComponent: ComponentType<{ + datum: D; + stats: { rows: number; rowIndex: number; columns: number; columnIndex: number }; + }>; +} + +/** @internal */ +export function AlignedGrid({ + data, + headerComponent: HeaderComponent, + contentComponent: ContentComponent, +}: AlignedGridProps) { + const rows = data.length; + const columns = data.reduce((acc, row) => { + return Math.max(acc, row.length); + }, 0); + console.log({ columnCount: columns, rowCount: rows }); + + const gridStyle: CSSProperties = { + gridTemplateColumns: `repeat(${columns}, 1fr`, + gridTemplateRows: `repeat(${rows}, max-content 1fr)`, + }; + + return ( +
+ {data.map((row, rowIndex) => + row.map((cell, columnIndex) => { + const headerStyle: CSSProperties = { + gridRow: rowIndex * 2 + 1, + gridColumn: columnIndex + 1, + }; + const contentStyle: CSSProperties = { + gridRow: rowIndex * 2 + 2, + gridColumn: columnIndex + 1, + }; + if (!cell) { + return ( + <> +
+
+ + ); + } + return ( + <> +
+ +
+
+ +
+ + ); + }), + )} +
+ ); +} diff --git a/packages/charts/src/components/grid/grid.html b/packages/charts/src/components/grid/grid.html new file mode 100644 index 0000000000..a570b42c35 --- /dev/null +++ b/packages/charts/src/components/grid/grid.html @@ -0,0 +1,117 @@ + + + + + Title + + + + + + +
+ +

CPU percentage

+
+ +
Title for 2 chart
+
2 chart
+ +
Title for 3 chart
+
3 chart
+ +
Title for 4 chart
+
4 chart
+ +
Title for 5 chart longer then chart 4
+
5 chart
+ +
+ + + diff --git a/packages/charts/src/components/grid/html_grid.tsx b/packages/charts/src/components/grid/html_grid.tsx new file mode 100644 index 0000000000..2f1a73bc62 --- /dev/null +++ b/packages/charts/src/components/grid/html_grid.tsx @@ -0,0 +1,129 @@ +/* + * 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 classNames from 'classnames'; +import React, { memo, ComponentType, forwardRef } from 'react'; + +import { highContrastColor } from '../../common/color_calcs'; +import { colorToRgba } from '../../common/color_library_wrappers'; +import { Color, Colors } from '../../common/colors'; +import { Pixels } from '../../common/geometry'; +import { A11ySettings } from '../../state/selectors/get_accessibility_config'; +import { Size } from '../../utils/dimensions'; + +/** @internal */ +export interface HTMLGridProps { + a11y: A11ySettings; + size: Size; + style: { + minHeight: Pixels; + border: Color; + background: Color; + text: { + lightColor: Color; + darkColor: Color; + }; + }; + data: Array>; + component: ComponentType<{ + rowCount: number; + columnCount: number; + datum: NonNullable; + rowIndex: number; + columnIndex: number; + panel: Size; + }>; + emptyComponent: ComponentType; +} + +/** @internal */ +export function HTMLGrid({ + size, + a11y, + style, + data, + component: Component, + emptyComponent: EmptyComponent, +}: HTMLGridProps) { + const rowCount = data.length; + const columnCount = data.reduce((acc, row) => { + return Math.max(acc, row.length); + }, 0); + const borderColor = + highContrastColor(colorToRgba(style.background)) === Colors.White.rgba + ? style.text.lightColor + : style.text.darkColor; + + const panel = { width: size.width / columnCount, height: size.height / rowCount }; + return ( + // eslint-disable-next-line jsx-a11y/no-redundant-roles +
    + {data.flatMap((columns, rowIndex) => { + return [ + ...columns.map((datum, columnIndex) => { + // fill undefined with empty panels + const emptyMetricClassName = classNames('echGridCell', { + 'echGridCell--rightBorder': columnIndex < columnCount - 1, + 'echGridCell--bottomBorder': rowIndex < rowCount - 1, + }); + const containerClassName = classNames('echGridCell', { + 'echGridCell--rightBorder': columnIndex < columnCount - 1, + 'echGridCell--bottomBorder': rowIndex < rowCount - 1, + }); + + return !datum ? ( +
  • +
    + {/* */} +
    +
  • + ) : ( +
  • + +
  • + ); + }), + // fill the grid row with empty panels + ...Array.from({ length: columnCount - columns.length }, (_, zeroBasedColumnIndex) => { + const columnIndex = zeroBasedColumnIndex + columns.length; + const emptyMetricClassName = classNames('echGridCell', { + 'echGridCell--bottomBorder': rowIndex < rowCount - 1, + }); + return ( +
  • +
    + {/* */} +
    +
  • + ); + }), + ]; + })} +
+ ); +} + +/** @internal */ +export const MemoizedGrid = memo(HTMLGrid); diff --git a/packages/charts/src/state/chart_state.ts b/packages/charts/src/state/chart_state.ts index 308347ba4b..20b6069392 100644 --- a/packages/charts/src/state/chart_state.ts +++ b/packages/charts/src/state/chart_state.ts @@ -24,6 +24,7 @@ import { LegendItemLabel } from './selectors/get_legend_items_labels'; import { DebugState } from './types'; import { getInitialPointerState, getInitialTooltipState } from './utils'; import { ChartType } from '../chart_types'; +import { BulletGraphState } from '../chart_types/bullet_graph/chart_state'; import { FlameState } from '../chart_types/flame_chart/internal_chart_state'; import { GoalState } from '../chart_types/goal_chart/state/chart_state'; import { HeatmapState } from '../chart_types/heatmap/state/chart_state'; @@ -475,6 +476,7 @@ const constructors: Record InternalChartState | null> = { [ChartType.Heatmap]: () => new HeatmapState(), [ChartType.Wordcloud]: () => new WordcloudState(), [ChartType.Metric]: () => new MetricState(), + [ChartType.BulletGraph]: () => new BulletGraphState(), [ChartType.Global]: () => null, }; // with no default, TS signals if a new chart type isn't added here too diff --git a/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts b/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts index ae79846fa1..649e16193e 100644 --- a/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts +++ b/packages/charts/src/utils/bbox/canvas_text_bbox_calculator.ts @@ -26,8 +26,8 @@ export function measureText(ctx: CanvasRenderingContext2D): TextMeasure { // TODO this is a temporary fix to make the multilayer time axis work return { width: 0, height: fontSize * lineHeight }; } - ctx.font = cssFontShorthand(font, fontSize); + ctx.font = cssFontShorthand(font, fontSize * 2); const { width } = ctx.measureText(text); - return { width, height: fontSize * lineHeight }; + return { width: width / 2, height: fontSize * lineHeight }; }; } diff --git a/packages/charts/src/utils/themes/dark_theme.ts b/packages/charts/src/utils/themes/dark_theme.ts index e18d51c727..15d8e31964 100644 --- a/packages/charts/src/utils/themes/dark_theme.ts +++ b/packages/charts/src/utils/themes/dark_theme.ts @@ -14,6 +14,7 @@ import { DEFAULT_GEOMETRY_STYLES, DEFAULT_MISSING_COLOR, } from './theme_common'; +import { DARK_THEME_BULLET_STYLE } from '../../chart_types/bullet_graph/theme'; import { Colors } from '../../common/colors'; import { GOLDEN_RATIO, TAU } from '../../common/constants'; import { ColorVariant } from '../common'; @@ -396,6 +397,7 @@ export const DARK_THEME: Theme = { nonFiniteText: 'N/A', minHeight: 64, }, + bulletGraph: DARK_THEME_BULLET_STYLE, tooltip: { maxWidth: 260, maxTableHeight: 120, diff --git a/packages/charts/src/utils/themes/light_theme.ts b/packages/charts/src/utils/themes/light_theme.ts index a873d21140..ce2ffd1c01 100644 --- a/packages/charts/src/utils/themes/light_theme.ts +++ b/packages/charts/src/utils/themes/light_theme.ts @@ -14,6 +14,7 @@ import { DEFAULT_GEOMETRY_STYLES, DEFAULT_MISSING_COLOR, } from './theme_common'; +import { LIGHT_THEME_BULLET_STYLE } from '../../chart_types/bullet_graph/theme'; import { Colors } from '../../common/colors'; import { GOLDEN_RATIO, TAU } from '../../common/constants'; import { ColorVariant } from '../common'; @@ -395,6 +396,7 @@ export const LIGHT_THEME: Theme = { nonFiniteText: 'N/A', minHeight: 64, }, + bulletGraph: LIGHT_THEME_BULLET_STYLE, tooltip: { maxWidth: 260, maxTableHeight: 120, diff --git a/packages/charts/src/utils/themes/theme.ts b/packages/charts/src/utils/themes/theme.ts index 1bdacf51fa..8299ff957e 100644 --- a/packages/charts/src/utils/themes/theme.ts +++ b/packages/charts/src/utils/themes/theme.ts @@ -17,6 +17,8 @@ import { ColorVariant, HorizontalAlignment, RecursivePartial, VerticalAlignment import { Margins, Padding, SimplePadding } from '../dimensions'; import { Point } from '../point'; +import { BulletGraphStyle } from '/@elastic/charts/src/chart_types/bullet_graph/theme'; + /** @public */ export interface Visible { visible: boolean; @@ -487,6 +489,11 @@ export interface Theme { * Theme styles for metric chart types */ metric: MetricStyle; + + /** + * Theme styles for bullet graph types + */ + bulletGraph: BulletGraphStyle; /** * Theme styles for tooltip */ diff --git a/storybook/stories/bullet_graph/1_simple.story.tsx b/storybook/stories/bullet_graph/1_simple.story.tsx new file mode 100644 index 0000000000..c84be30a47 --- /dev/null +++ b/storybook/stories/bullet_graph/1_simple.story.tsx @@ -0,0 +1,82 @@ +/* + * 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, BulletGraph, BulletGraphSubtype } from '@elastic/charts'; + +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example = () => { + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.angular); + return ( +
+ + `${d}`, + tickFormatter: (d) => `${d}`, + }, + { + ticks: 'auto', + target: 67, + value: 123, + title: 'First row second column title', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}`, + tickFormatter: (d) => `${d}`, + }, + ], + [ + { + ticks: 'auto', + target: 50, + value: 11, + title: '', + subtitle: 'Second row first column subtitle', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + { + ticks: 'auto', + target: 80, + value: 91, + title: 'Second row second column title', + + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + ], + ]} + /> + +
+ ); +}; diff --git a/storybook/stories/bullet_graph/bullet_graph.stories.tsx b/storybook/stories/bullet_graph/bullet_graph.stories.tsx new file mode 100644 index 0000000000..1d32995e27 --- /dev/null +++ b/storybook/stories/bullet_graph/bullet_graph.stories.tsx @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export default { + title: 'Bullet Graph', +}; + +export { Example as simple } from './1_simple.story'; From c7f9236deda4c4ec69238ee9dbc088812173d36c Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 11 May 2023 18:46:21 +0200 Subject: [PATCH 2/7] WIP --- .../bullet_graph/renderer/bullet.tsx | 61 ++++++++++++++++++- .../bullet_graph/renderer/index.tsx | 4 +- .../src/components/grid/_aligned_grid.scss | 10 ++- .../src/components/grid/aligned_grid.tsx | 16 +++-- 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx index 623eaea147..5dfc03b558 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx @@ -6,8 +6,10 @@ * Side Public License, v 1. */ +import { useLatest } from '@elastic/eui/src/services/hooks/useLatest'; +import { grid } from 'charts-storybook/stories/metric/metric.stories'; import { clamp } from 'lodash'; -import React, { memo } from 'react'; +import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { Color } from '../../../common/colors'; import { Ratio } from '../../../common/geometry'; @@ -215,9 +217,21 @@ function scaleToAngle(unit: number): number { /** @internal */ export function AngularBulletComp(props: BulletProps) { + // const ref = useRef(null); + const [fontSize, setFontSize] = useState(10); + + const onResize = useCallback((target, entry) => { + // Handle the resize event + const minSize = Math.min(entry.contentRect.width, entry.contentRect.height); + setFontSize(minSize / 100); + console.log(minSize); + }, []); + + const ref = useResizeObserver(onResize); + return (
- + {props.colorBands.map((band, index) => ( // eslint-disable-next-line react/no-array-index-key @@ -261,6 +275,24 @@ export function AngularBulletComp(props: BulletProps) { vectorEffect="non-scaling-stroke" /> )} + {props.labels.map((label) => { + const coord = polarToCartesian(50, 10, 35, 240 * label.position - 120); + return ( + 0.5 ? 'end' : 'start'} + > + {label.text} + + ); + })} {/* (callback: (target: T, entry: ResizeObserverEntry) => void) { + const ref = useRef(null); + + useLayoutEffect(() => { + const element = ref?.current; + + if (!element) { + return; + } + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + callback(element, entry); + } + }); + + observer.observe(element); + return () => { + observer.disconnect(); + }; + }, [callback, ref]); + + return ref; +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx index 4ca4b8f720..7c8b5fe56c 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx @@ -207,7 +207,9 @@ function switchToMetric(size: Size, rows: number, columns: number, subtype: Bull switch (subtype) { case BulletGraphSubtype.horizontal: return size.width / columns < 200 || size.height / rows < 100; - default: + case BulletGraphSubtype.vertical: return size.width / columns < 150 || size.height / rows < 150; + case BulletGraphSubtype.angular: + return size.width / columns < 200 || size.height / rows < 200; } } diff --git a/packages/charts/src/components/grid/_aligned_grid.scss b/packages/charts/src/components/grid/_aligned_grid.scss index 03bfc5ba11..a6f15acec5 100644 --- a/packages/charts/src/components/grid/_aligned_grid.scss +++ b/packages/charts/src/components/grid/_aligned_grid.scss @@ -11,13 +11,19 @@ height: 100%; margin: 0; padding:0; + +} +.echAlignedGrid__borderRight { border-right: 1px solid #EDF0F5; } +.echAlignedGrid__borderBottom { + border-bottom: 1px solid #EDF0F5; +} + .echAlignedGrid--content { width: 100%; min-height: 0; margin: 0; padding:0; - border-right: 1px solid #EDF0F5; - border-bottom: 1px solid #EDF0F5; + } diff --git a/packages/charts/src/components/grid/aligned_grid.tsx b/packages/charts/src/components/grid/aligned_grid.tsx index 837b207775..b58f8aa939 100644 --- a/packages/charts/src/components/grid/aligned_grid.tsx +++ b/packages/charts/src/components/grid/aligned_grid.tsx @@ -7,6 +7,7 @@ */ import { grid } from 'charts-storybook/stories/metric/metric.stories'; +import classNames from 'classnames'; import React, { ComponentType, CSSProperties, FC, PropsWithChildren } from 'react'; import { Size } from '../../utils/dimensions'; @@ -52,20 +53,27 @@ export function AlignedGrid({ gridRow: rowIndex * 2 + 2, gridColumn: columnIndex + 1, }; + const headerClassName = classNames('echAlignedGrid--header', { + echAlignedGrid__borderRight: columnIndex < columns - 1, + }); + const contentClassName = classNames('echAlignedGrid--content', { + echAlignedGrid__borderRight: columnIndex < columns - 1, + echAlignedGrid__borderBottom: rowIndex < rows - 1, + }); if (!cell) { return ( <> -
-
+
+
); } return ( <> -
+
-
+
From 5fdaac1f4d6748bb1629f5c42a23d923c572cb6a Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 11 May 2023 18:59:09 +0200 Subject: [PATCH 3/7] Add examples --- .../bullet_graph/2_horizontal.story.tsx | 78 +++++++++++++++++ .../stories/bullet_graph/3_vertical.story.tsx | 84 +++++++++++++++++++ .../bullet_graph/bullet_graph.stories.tsx | 4 +- 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 storybook/stories/bullet_graph/2_horizontal.story.tsx create mode 100644 storybook/stories/bullet_graph/3_vertical.story.tsx diff --git a/storybook/stories/bullet_graph/2_horizontal.story.tsx b/storybook/stories/bullet_graph/2_horizontal.story.tsx new file mode 100644 index 0000000000..29769427bd --- /dev/null +++ b/storybook/stories/bullet_graph/2_horizontal.story.tsx @@ -0,0 +1,78 @@ +/* + * 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, BulletGraph, BulletGraphSubtype } from '@elastic/charts'; + +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example = () => { + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.vertical); + return ( +
+ + `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + { + ticks: 'auto', + target: 75, + value: 98, + title: 'Memory', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + { + ticks: 'auto', + target: 25, + value: 35.5, + title: 'Network In', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + { + ticks: 'auto', + target: 25, + value: 91, + title: 'Network out', + + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + ], + ]} + /> + +
+ ); +}; diff --git a/storybook/stories/bullet_graph/3_vertical.story.tsx b/storybook/stories/bullet_graph/3_vertical.story.tsx new file mode 100644 index 0000000000..974d3c24bc --- /dev/null +++ b/storybook/stories/bullet_graph/3_vertical.story.tsx @@ -0,0 +1,84 @@ +/* + * 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, BulletGraph, BulletGraphSubtype } from '@elastic/charts'; + +import { getKnobFromEnum } from '../utils/knobs/utils'; + +export const Example = () => { + const subtype = getKnobFromEnum('subtype', BulletGraphSubtype, BulletGraphSubtype.horizontal); + return ( +
+ + `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + ], + [ + { + ticks: 'auto', + target: 75, + value: 98, + title: 'Memory', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + ], + [ + { + ticks: 'auto', + target: 25, + value: 35.5, + title: 'Network In', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + ], + [ + { + ticks: 'auto', + target: 25, + value: 91, + title: 'Network out', + + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}%`, + tickFormatter: (d) => `${d}%`, + }, + ], + ]} + /> + +
+ ); +}; diff --git a/storybook/stories/bullet_graph/bullet_graph.stories.tsx b/storybook/stories/bullet_graph/bullet_graph.stories.tsx index 1d32995e27..f1bb14a507 100644 --- a/storybook/stories/bullet_graph/bullet_graph.stories.tsx +++ b/storybook/stories/bullet_graph/bullet_graph.stories.tsx @@ -10,4 +10,6 @@ export default { title: 'Bullet Graph', }; -export { Example as simple } from './1_simple.story'; +export { Example as grid } from './1_simple.story'; +export { Example as horizontalGrid } from './2_horizontal.story'; +export { Example as verticalGrid } from './3_vertical.story'; From e3e4b697bc90a14f8a8669dda0c23ad7e2a92d4b Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Fri, 12 May 2023 10:14:14 +0200 Subject: [PATCH 4/7] small refactoring --- .../bullet_graph/renderer/_header.scss | 11 +- .../bullet_graph/renderer/_index.scss | 12 +- .../bullet_graph/renderer/bullet.tsx | 10 +- .../bullet_graph/renderer/headertest.html | 207 ++++---- .../bullet_graph/renderer/index.tsx | 5 +- .../bullet_graph/renderer/test.html | 486 +++++++++--------- .../src/components/grid/_aligned_grid.scss | 12 +- .../src/components/grid/aligned_grid.tsx | 12 +- packages/charts/src/components/grid/grid.html | 218 ++++---- .../charts/src/components/grid/html_grid.tsx | 6 +- 10 files changed, 464 insertions(+), 515 deletions(-) diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss b/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss index fcd59e8420..f88cdb4073 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss +++ b/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss @@ -1,7 +1,6 @@ .echBulletGraphHeader { position: relative; - padding: 8px; - border-bottom: 1px solid #D3DAE6; + padding: 8px 8px 0 8px; &__title { font-weight: bold; @@ -18,7 +17,6 @@ display: flex; flex-direction: column; flex-wrap: nowrap; - } .echBulletGraphHeader--single { @@ -32,7 +30,7 @@ .echBulletGraphHeader__title { @include lineClamp(2); } - padding-bottom: 4px; + //padding-bottom: 4px; } .echBulletGraphHeader--multi { @@ -50,15 +48,14 @@ flex-grow: 1; flex-shrink: 1; } - padding-bottom: 4px; + //padding-bottom: 4px; } - .echBulletHeader__valueContainer { white-space: nowrap; flex-grow: 0; flex-shrink: 0; - float:right; + float: right; } .echBulletHeader__value { font-weight: 700; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss b/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss index 779612baa2..1654836ce4 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss +++ b/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss @@ -1,4 +1,4 @@ -@import "header"; +@import 'header'; .echBulletGraphContainer { position: relative; @@ -15,8 +15,8 @@ } .echBulletGraphSVG--container { - flex:1; - padding:10px; + flex: 1; + padding: 10px; height: 100%; overflow: visible; position: relative; @@ -25,8 +25,6 @@ overflow: visible; } .echBulletGraph { - - .echBullet--tickLabel { font-size: 10px; fill: $euiTextColor; @@ -58,8 +56,4 @@ .echBullet--angularTickLabel { fill: $euiTextColor; } - } - - - diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx index 5dfc03b558..c3b88d68f8 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx @@ -6,10 +6,8 @@ * Side Public License, v 1. */ -import { useLatest } from '@elastic/eui/src/services/hooks/useLatest'; -import { grid } from 'charts-storybook/stories/metric/metric.stories'; import { clamp } from 'lodash'; -import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { memo, useCallback, useLayoutEffect, useRef, useState } from 'react'; import { Color } from '../../../common/colors'; import { Ratio } from '../../../common/geometry'; @@ -119,7 +117,7 @@ export function HorizontalBulletComp(props: BulletProps) { return (
- + {props.colorBands.map((band, index) => ( // eslint-disable-next-line react/no-array-index-key (null); diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html b/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html index 1f06164bd5..ac9a2fd8ba 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html +++ b/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html @@ -1,112 +1,121 @@ - - - Title - - - - -
-

Some text, a header perhaps? But this is the next that will sit under the image, sometimes it's a p tag.

- -

Some more random text that would appear in the messagebox this could go on for a few lines.

-
- - + .messageIconWrap { + display: inline-block; + float: right; + height: 60px; + width: 60px; + } + + + +
+

Some text, a header perhaps? But this is the next that will sit under the image, sometimes it's a p tag.

+ +

Some more random text that would appear in the messagebox this could go on for a few lines.

+
+ diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx index 7c8b5fe56c..76f3d9824b 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx @@ -10,7 +10,6 @@ /* eslint-disable react/no-array-index-key */ import { scaleLinear } from 'd3-scale'; -import { size } from 'lodash'; import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; @@ -51,7 +50,6 @@ interface DispatchProps { const Component: React.FC = ({ initialized, size, - a11y, specs: [spec], // ignoring other specs }) => { if (!initialized || !spec) { @@ -77,8 +75,9 @@ const Component: React.FC = ({ }} contentComponent={({ datum, stats }) => { // TODO move to the bullet SVG - const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; + // const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, 1]); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(['#D9C6EF', '#AA87D1']); const maxTicks = diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/test.html b/packages/charts/src/chart_types/bullet_graph/renderer/test.html index 1209a82951..c080789fc2 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/test.html +++ b/packages/charts/src/chart_types/bullet_graph/renderer/test.html @@ -1,266 +1,240 @@ - - - Title - - - - - - -
- - - - - - - - - - - - - - - - - - -
font sizetexthtml sizecanvas textcanvas sizediff
- - - + function render(textValue, testCount) { + table.innerHTML = ''; + chart.innerHTML = ''; + stats.innerHTML = ''; + + const tries = Array.from({ length: testCount }).map((test, i) => { + const fontSize = i + minFontSize; + const tableRow = document.createElement('tr'); + + const sizeCell = document.createElement('td'); + sizeCell.innerHTML = fontSize; + + tableRow.appendChild(sizeCell); + + const htmlText = document.createElement('td'); + + const textElement = document.createElement('span'); + textElement.innerHTML = textValue; + textElement.style.fontFamily = fontFamily; + textElement.style.fontSize = `${fontSize}px`; + // textElement.style.lineHeight =`${i}px`; + + htmlText.appendChild(textElement); + + const canvasCell = document.createElement('canvas'); + + const canvasCtx = canvasCell.getContext('2d'); + canvasCtx.font = `${fontSize}px ${fontFamily}`; + canvasCtx.textAlign = 'start'; + canvasCtx.textBaseline = 'hanging'; + canvasCtx.fillText(textValue, 0, 0); + htmlText.appendChild(canvasCell); + tableRow.appendChild(htmlText); + + table.appendChild(tableRow); + + const htmlSizeCell = document.createElement('td'); + const htmlSizeValue = textElement.getBoundingClientRect().width; + htmlSizeCell.innerHTML = `${htmlSizeValue.toFixed(2)}`; + tableRow.appendChild(htmlSizeCell); + + // tableRow.appendChild(canvasCell); + + const canvasWidth = measure(textValue, fontSize); + + const canvasSizeCell = document.createElement('td'); + canvasSizeCell.innerHTML = `${canvasWidth.toFixed(2)}`; + tableRow.appendChild(canvasSizeCell); + + const diffCell = document.createElement('td'); + const color = canvasWidth - htmlSizeValue < 0 ? 'background:blue' : 'background:red'; + diffCell.innerHTML = `
${Math.ceil(canvasWidth - htmlSizeValue)}
`; + + tableRow.appendChild(diffCell); + + return { fontSize, html: htmlSizeValue, canvas: canvasWidth, diff: Math.ceil(canvasWidth - htmlSizeValue) }; + }); + + const statsInfo = tries.reduce( + (acc, t) => { + if (!t) { + return acc; + } + return { + min: Math.min(acc.min, t.diff), + max: Math.max(acc.max, t.diff), + sum: acc.sum + t.diff, + }; + }, + { min: Infinity, max: -Infinity, sum: 0 }, + ); + const info = document.createElement('p'); + + info.innerHTML = `MAX: ${ + statsInfo.max + }px
MIN: ${ + statsInfo.min + }px
MEAN:${Math.ceil(statsInfo.sum / tries.length)}
FONT: ${ + ctx.font + }
CANVAS SCALE FACTOR: ${canvasScale}`; + stats.appendChild(info); + + function scale(val) { + if (statsInfo.max > 0 && statsInfo.min < 0) { + return (100 / (Math.abs(statsInfo.max) + Math.abs(statsInfo.min))) * val; + } + return (100 / Math.max(Math.abs(statsInfo.max), Math.abs(statsInfo.min))) * val; + } + + tries.forEach((text, i) => { + if (!text) { + return; + } + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + line.setAttribute('x1', `0`); + line.setAttribute('x2', `100%`); + line.setAttribute('y1', `50%`); + line.setAttribute('y2', `50%`); + line.setAttribute('stroke', `black`); + chart.appendChild(line); + + const bar = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + bar.setAttribute('x', `${i * 10}`); + bar.setAttribute('y', `${text.diff > 0 ? 50 - scale(Math.abs(text.diff)) : 50}`); + bar.setAttribute('height', `${scale(Math.abs(text.diff))}px`); + bar.setAttribute('width', `5px`); + bar.setAttribute('fill', text.diff < 0 ? 'blue' : 'red'); + chart.appendChild(bar); + + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', `${i * 10}`); + label.setAttribute('y', `5px`); + label.setAttribute('font-size', `5px`); + label.innerHTML = text.fontSize; + chart.appendChild(label); + }); + } + + diff --git a/packages/charts/src/components/grid/_aligned_grid.scss b/packages/charts/src/components/grid/_aligned_grid.scss index a6f15acec5..aa889cf353 100644 --- a/packages/charts/src/components/grid/_aligned_grid.scss +++ b/packages/charts/src/components/grid/_aligned_grid.scss @@ -1,29 +1,25 @@ - .echAlignedGrid { display: grid; align-content: stretch; width: 100%; height: 100%; - } .echAlignedGrid--header { width: 100%; height: 100%; margin: 0; - padding:0; - + padding: 0; } .echAlignedGrid__borderRight { - border-right: 1px solid #EDF0F5; + border-right: 1px solid #edf0f5; } .echAlignedGrid__borderBottom { - border-bottom: 1px solid #EDF0F5; + border-bottom: 1px solid #edf0f5; } .echAlignedGrid--content { width: 100%; min-height: 0; margin: 0; - padding:0; - + padding: 0; } diff --git a/packages/charts/src/components/grid/aligned_grid.tsx b/packages/charts/src/components/grid/aligned_grid.tsx index b58f8aa939..11b9f1300e 100644 --- a/packages/charts/src/components/grid/aligned_grid.tsx +++ b/packages/charts/src/components/grid/aligned_grid.tsx @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import { grid } from 'charts-storybook/stories/metric/metric.stories'; import classNames from 'classnames'; -import React, { ComponentType, CSSProperties, FC, PropsWithChildren } from 'react'; - -import { Size } from '../../utils/dimensions'; +import React, { ComponentType, CSSProperties } from 'react'; interface AlignedGridProps { data: Array>; @@ -34,7 +31,6 @@ export function AlignedGrid({ const columns = data.reduce((acc, row) => { return Math.max(acc, row.length); }, 0); - console.log({ columnCount: columns, rowCount: rows }); const gridStyle: CSSProperties = { gridTemplateColumns: `repeat(${columns}, 1fr`, @@ -54,11 +50,11 @@ export function AlignedGrid({ gridColumn: columnIndex + 1, }; const headerClassName = classNames('echAlignedGrid--header', { - echAlignedGrid__borderRight: columnIndex < columns - 1, + // echAlignedGrid__borderRight: columnIndex < columns - 1, }); const contentClassName = classNames('echAlignedGrid--content', { - echAlignedGrid__borderRight: columnIndex < columns - 1, - echAlignedGrid__borderBottom: rowIndex < rows - 1, + // echAlignedGrid__borderRight: columnIndex < columns - 1, + // echAlignedGrid__borderBottom: rowIndex < rows - 1, }); if (!cell) { return ( diff --git a/packages/charts/src/components/grid/grid.html b/packages/charts/src/components/grid/grid.html index a570b42c35..9be94c583d 100644 --- a/packages/charts/src/components/grid/grid.html +++ b/packages/charts/src/components/grid/grid.html @@ -1,117 +1,107 @@ - - - Title - - - - - - -
- -

CPU percentage

-
- -
Title for 2 chart
-
2 chart
- -
Title for 3 chart
-
3 chart
- -
Title for 4 chart
-
4 chart
- -
Title for 5 chart longer then chart 4
-
5 chart
- -
- - + + + Title + + + + +
+

CPU percentage

+
+ +
Title for 2 chart
+
2 chart
+ +
Title for 3 chart
+
3 chart
+ +
Title for 4 chart
+
4 chart
+ +
Title for 5 chart longer then chart 4
+
5 chart
+
+ diff --git a/packages/charts/src/components/grid/html_grid.tsx b/packages/charts/src/components/grid/html_grid.tsx index 2f1a73bc62..5235eac31a 100644 --- a/packages/charts/src/components/grid/html_grid.tsx +++ b/packages/charts/src/components/grid/html_grid.tsx @@ -7,7 +7,7 @@ */ import classNames from 'classnames'; -import React, { memo, ComponentType, forwardRef } from 'react'; +import React, { memo, ComponentType } from 'react'; import { highContrastColor } from '../../common/color_calcs'; import { colorToRgba } from '../../common/color_library_wrappers'; @@ -88,7 +88,7 @@ export function HTMLGrid({ return !datum ? (
  • - {/* */} +
  • ) : ( @@ -114,7 +114,7 @@ export function HTMLGrid({ return (
  • - {/* */} +
  • ); From d2d627eec747007acc05b7a9652d19098dfe0bad Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Fri, 12 May 2023 15:33:41 +0200 Subject: [PATCH 5/7] wip --- .../bullet_graph/renderer/_header.scss | 9 ++- .../bullet_graph/renderer/_index.scss | 2 +- .../bullet_graph/renderer/header.tsx | 14 +++- .../src/components/grid/aligned_grid.tsx | 7 +- .../stories/bullet_graph/1_simple.story.tsx | 64 +++++++++---------- .../bullet_graph/2_horizontal.story.tsx | 2 +- .../stories/bullet_graph/3_vertical.story.tsx | 5 +- 7 files changed, 58 insertions(+), 45 deletions(-) diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss b/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss index f88cdb4073..93bb5461f3 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss +++ b/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss @@ -17,12 +17,15 @@ display: flex; flex-direction: column; flex-wrap: nowrap; + &--vertical { + border-bottom: 1px solid ; + } } .echBulletGraphHeader--single { display: flex; flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; justify-content: space-between; align-items: flex-end; width: 100%; @@ -48,14 +51,14 @@ flex-grow: 1; flex-shrink: 1; } - //padding-bottom: 4px; + } .echBulletHeader__valueContainer { white-space: nowrap; flex-grow: 0; flex-shrink: 0; - float: right; + align-items: baseline; } .echBulletHeader__value { font-weight: 700; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss b/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss index 1654836ce4..beb0a932ae 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss +++ b/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss @@ -38,7 +38,7 @@ fill: $euiTextColor; } .echBullet--bar { - fill: $euiTextColor; + fill: #333333;//$euiTextColor; } .echBullet--tick { stroke: $euiPageBackgroundColor; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx index 4da17fb0fa..f514eba433 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx @@ -10,16 +10,24 @@ import React from 'react'; /** @internal */ export function Header(props: { title: string; subtitle: string; value: string; target?: string }) { - return ( + return props.subtitle.trim().length > 0 ? (

    {props.title}

    {props.subtitle}

    - {props.value} - {props.target ? / {props.target} : ''} + {props.value}K + {props.target ? / {props.target}M : ''}

    + ) : ( +
    +

    {props.title}

    +

    + {props.value}K + {props.target ? / {props.target}M : ''} +

    +
    ); } diff --git a/packages/charts/src/components/grid/aligned_grid.tsx b/packages/charts/src/components/grid/aligned_grid.tsx index 11b9f1300e..95b029c362 100644 --- a/packages/charts/src/components/grid/aligned_grid.tsx +++ b/packages/charts/src/components/grid/aligned_grid.tsx @@ -50,11 +50,12 @@ export function AlignedGrid({ gridColumn: columnIndex + 1, }; const headerClassName = classNames('echAlignedGrid--header', { - // echAlignedGrid__borderRight: columnIndex < columns - 1, + echAlignedGrid__borderRight: columnIndex < columns - 1, + // echAlignedGrid__borderBottom: true, }); const contentClassName = classNames('echAlignedGrid--content', { - // echAlignedGrid__borderRight: columnIndex < columns - 1, - // echAlignedGrid__borderBottom: rowIndex < rows - 1, + echAlignedGrid__borderRight: columnIndex < columns - 1, + echAlignedGrid__borderBottom: rowIndex < rows - 1, }); if (!cell) { return ( diff --git a/storybook/stories/bullet_graph/1_simple.story.tsx b/storybook/stories/bullet_graph/1_simple.story.tsx index c84be30a47..8b14c69fb1 100644 --- a/storybook/stories/bullet_graph/1_simple.story.tsx +++ b/storybook/stories/bullet_graph/1_simple.story.tsx @@ -37,43 +37,43 @@ export const Example = () => { target: 10, value: 23, title: 'First row first column title', - subtitle: 'First row first column subtitle', + // subtitle: 'First row first column subtitle', domain: { min: 0, max: 100, nice: false }, valueFormatter: (d) => `${d}`, tickFormatter: (d) => `${d}`, }, - { - ticks: 'auto', - target: 67, - value: 123, - title: 'First row second column title', - domain: { min: 0, max: 100, nice: false }, - valueFormatter: (d) => `${d}`, - tickFormatter: (d) => `${d}`, - }, - ], - [ - { - ticks: 'auto', - target: 50, - value: 11, - title: '', - subtitle: 'Second row first column subtitle', - domain: { min: 0, max: 100, nice: false }, - valueFormatter: (d) => `${d}%`, - tickFormatter: (d) => `${d}%`, - }, - { - ticks: 'auto', - target: 80, - value: 91, - title: 'Second row second column title', - - domain: { min: 0, max: 100, nice: false }, - valueFormatter: (d) => `${d}%`, - tickFormatter: (d) => `${d}%`, - }, + // { + // ticks: 'auto', + // target: 67, + // value: 123, + // title: 'First row second column title', + // domain: { min: 0, max: 100, nice: false }, + // valueFormatter: (d) => `${d}`, + // tickFormatter: (d) => `${d}`, + // }, ], + // [ + // { + // ticks: 'auto', + // target: 50, + // value: 11, + // title: '', + // subtitle: 'Second row first column subtitle', + // domain: { min: 0, max: 100, nice: false }, + // valueFormatter: (d) => `${d}`, + // tickFormatter: (d) => `${d}`, + // }, + // { + // ticks: 'auto', + // target: 80, + // value: 91, + // title: 'Second row second column title', + // subtitle: 'percentage', + // domain: { min: 0, max: 100, nice: false }, + // valueFormatter: (d) => `${d}`, + // tickFormatter: (d) => `${d}`, + // }, + // ], ]} /> diff --git a/storybook/stories/bullet_graph/2_horizontal.story.tsx b/storybook/stories/bullet_graph/2_horizontal.story.tsx index 29769427bd..e1deb59546 100644 --- a/storybook/stories/bullet_graph/2_horizontal.story.tsx +++ b/storybook/stories/bullet_graph/2_horizontal.story.tsx @@ -54,7 +54,7 @@ export const Example = () => { ticks: 'auto', target: 25, value: 35.5, - title: 'Network In', + title: 'Network In very long title two lines', domain: { min: 0, max: 100, nice: false }, valueFormatter: (d) => `${d}%`, tickFormatter: (d) => `${d}%`, diff --git a/storybook/stories/bullet_graph/3_vertical.story.tsx b/storybook/stories/bullet_graph/3_vertical.story.tsx index 974d3c24bc..6794481b20 100644 --- a/storybook/stories/bullet_graph/3_vertical.story.tsx +++ b/storybook/stories/bullet_graph/3_vertical.story.tsx @@ -58,7 +58,8 @@ export const Example = () => { ticks: 'auto', target: 25, value: 35.5, - title: 'Network In', + title: 'Network In with a longer title', + subtitle: ' longer title', domain: { min: 0, max: 100, nice: false }, valueFormatter: (d) => `${d}%`, tickFormatter: (d) => `${d}%`, @@ -69,7 +70,7 @@ export const Example = () => { ticks: 'auto', target: 25, value: 91, - title: 'Network out', + title: 'Network outtage', domain: { min: 0, max: 100, nice: false }, valueFormatter: (d) => `${d}%`, From 2886652577d6117bfd7d67243ef302d8a072b237 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 15 May 2023 15:24:48 +0200 Subject: [PATCH 6/7] WIP --- packages/charts/src/_reset.scss | 6 + .../chart_types/bullet_graph/chart_state.tsx | 11 +- .../renderer/canvas/bullet_graph.ts | 349 ++++++++++++++++++ .../bullet_graph/renderer/canvas/index.tsx | 224 +++++++++++ .../renderer/{ => dom}/_header.scss | 0 .../renderer/{ => dom}/_index.scss | 0 .../renderer/{ => dom}/bullet.tsx | 8 +- .../renderer/{ => dom}/header.tsx | 0 .../renderer/{ => dom}/headertest.html | 0 .../bullet_graph/renderer/{ => dom}/index.tsx | 21 +- .../bullet_graph/renderer/{ => dom}/test.html | 0 .../bullet_graph/selectors/layout.ts | 191 ++++++++++ .../src/chart_types/bullet_graph/theme.ts | 73 +++- .../src/common/default_theme_attributes.ts | 2 +- packages/charts/src/components/_index.scss | 2 +- .../stories/bullet_graph/1_simple.story.tsx | 68 ++-- .../bullet_graph/2_horizontal.story.tsx | 2 +- .../stories/bullet_graph/3_vertical.story.tsx | 11 +- storybook/style.scss | 7 + 19 files changed, 901 insertions(+), 74 deletions(-) create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts create mode 100644 packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/_header.scss (100%) rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/_index.scss (100%) rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/bullet.tsx (98%) rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/header.tsx (100%) rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/headertest.html (100%) rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/index.tsx (90%) rename packages/charts/src/chart_types/bullet_graph/renderer/{ => dom}/test.html (100%) create mode 100644 packages/charts/src/chart_types/bullet_graph/selectors/layout.ts diff --git a/packages/charts/src/_reset.scss b/packages/charts/src/_reset.scss index b82f6aa906..45b27b7950 100644 --- a/packages/charts/src/_reset.scss +++ b/packages/charts/src/_reset.scss @@ -9,3 +9,9 @@ svg text { letter-spacing: normal !important; } + + +html, body { + // font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' + font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !important; +} \ No newline at end of file diff --git a/packages/charts/src/chart_types/bullet_graph/chart_state.tsx b/packages/charts/src/chart_types/bullet_graph/chart_state.tsx index 75358a8c0f..8c5fa24af1 100644 --- a/packages/charts/src/chart_types/bullet_graph/chart_state.tsx +++ b/packages/charts/src/chart_types/bullet_graph/chart_state.tsx @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { RefObject } from 'react'; -import { BulletGraphRenderer } from './renderer'; +import { BulletGraphRenderer } from './renderer/canvas'; import { ChartType } from '../..'; import { DEFAULT_CSS_CURSOR } from '../../common/constants'; import { LegendItem } from '../../common/legend'; -import { InternalChartState } from '../../state/chart_state'; +import { BackwardRef, InternalChartState } from '../../state/chart_state'; import { InitStatus } from '../../state/selectors/get_internal_is_intialized'; import { LegendItemLabel } from '../../state/selectors/get_legend_items_labels'; @@ -24,7 +24,10 @@ const EMPTY_LEGEND_ITEM_LIST: LegendItemLabel[] = []; export class BulletGraphState implements InternalChartState { chartType = ChartType.BulletGraph; getChartTypeDescription = () => 'Bullet Graph'; - chartRenderer = () => ; + chartRenderer = (backwordRef: BackwardRef, forwardStageRef: RefObject) => ( + + ); + isInitialized = () => InitStatus.Initialized; isBrushAvailable = () => false; isBrushing = () => false; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts new file mode 100644 index 0000000000..af9b994a3c --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts @@ -0,0 +1,349 @@ +/* + * 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 { scaleLinear } from 'd3-scale'; + +import { Color } from '../../../../common/colors'; +import { Ratio } from '../../../../common/geometry'; +import { cssFontShorthand } from '../../../../common/text_utils'; +import { A11ySettings } from '../../../../state/selectors/get_accessibility_config'; +import { measureText } from '../../../../utils/bbox/canvas_text_bbox_calculator'; +import { isFiniteNumber } from '../../../../utils/common'; +import { Size } from '../../../../utils/dimensions'; +import { Point } from '../../../../utils/point'; +import { wrapText } from '../../../../utils/text/wrap'; +import { BulletGraphLayout, layout } from '../../selectors/layout'; +import { BulletDatum, BulletGraphSpec, BulletGraphSubtype } from '../../spec'; +import { + BulletGraphStyle, + GRAPH_PADDING, + HEADER_PADDING, + SUBTITLE_FONT, + SUBTITLE_FONT_SIZE, + SUBTITLE_LINE_HEIGHT, + TARGET_FONT, + TARGET_FONT_SIZE, + TICK_FONT, + TICK_FONT_SIZE, + TITLE_FONT, + TITLE_FONT_SIZE, + TITLE_LINE_HEIGHT, + VALUE_FONT, + VALUE_FONT_SIZE, + VALUE_LINE_HEIGHT, +} from '../../theme'; + +/** @internal */ +export function renderBulletGraph( + ctx: CanvasRenderingContext2D, + dpr: Ratio, + props: { + spec: BulletGraphSpec | undefined; + a11y: A11ySettings; + size: Size; + layout: BulletGraphLayout; + style: BulletGraphStyle; + }, +) { + const { style, layout, spec } = props; + if (!spec) { + return; + } + ctx.save(); + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + console.log(props.style.background); + ctx.fillStyle = props.style.background; + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + if (props.layout.shouldRenderMetric) { + return; + } + ctx.fillStyle = props.style.background; + //@ts-expect-error + ctx.letterSpacing = 'normal'; + + layout.headerLayout.forEach((row, rowIndex) => + row.forEach((cell, columnIndex) => { + if (!cell) return; + + const verticalAlignment = layout.layoutAlignment[rowIndex]!; + const panelY = cell.panel.height * rowIndex; + const panelX = cell.panel.width * columnIndex; + + ctx.save(); + ctx.strokeStyle = style.border; + if (row.length > 1 && columnIndex < row.length - 1) { + ctx.beginPath(); + ctx.moveTo(panelX + cell.panel.width, panelY); + ctx.lineTo(panelX + cell.panel.width, panelY + cell.panel.height); + ctx.stroke(); + } + + ctx.translate(panelX + HEADER_PADDING[3], panelY + HEADER_PADDING[0]); + + ctx.fillStyle = props.style.textColor; + // TITLE + ctx.textBaseline = 'top'; + ctx.textAlign = 'start'; + ctx.font = cssFontShorthand(TITLE_FONT, TITLE_FONT_SIZE); + cell.title.forEach((line, lineIndex) => { + ctx.fillText(line, 0, lineIndex * TITLE_LINE_HEIGHT); + }); + + // SUBTITLE + if (cell.subtitle) { + const y = verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT; + ctx.font = cssFontShorthand(SUBTITLE_FONT, SUBTITLE_FONT_SIZE); + ctx.fillText(cell.subtitle, 0, y); + } + + // VALUE + ctx.textBaseline = 'alphabetic'; + ctx.font = cssFontShorthand(VALUE_FONT, VALUE_FONT_SIZE); + if (!cell.multiline) ctx.textAlign = 'end'; + + const valueY = + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (cell.multiline ? TARGET_FONT_SIZE : 0); + const valueX = cell.multiline ? 0 : cell.header.width - cell.targetWidth; + ctx.fillText(cell.value, valueX, valueY); + + // TARGET + ctx.font = cssFontShorthand(TARGET_FONT, TARGET_FONT_SIZE); + if (!cell.multiline) ctx.textAlign = 'end'; + const targetX = cell.multiline ? cell.valueWidth : cell.header.width; + const targetY = + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (cell.multiline ? TARGET_FONT_SIZE : 0); + ctx.fillText(cell.target, targetX, targetY); + + const graphSize = { + width: cell.panel.width - (GRAPH_PADDING[1] + GRAPH_PADDING[3]), + height: + cell.panel.height - + (HEADER_PADDING[0] + + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (cell.multiline ? VALUE_LINE_HEIGHT : 0) + + GRAPH_PADDING[0] + + GRAPH_PADDING[2]), + }; + const graphOrigin = { + x: GRAPH_PADDING[3], + y: + HEADER_PADDING[0] + + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (cell.multiline ? VALUE_LINE_HEIGHT : 0) + + GRAPH_PADDING[0], + }; + + if (spec.subtype === 'vertical') { + ctx.strokeStyle = style.border; + ctx.beginPath(); + ctx.moveTo(panelX, graphOrigin.y - HEADER_PADDING[0]); + ctx.lineTo(panelX + cell.panel.width, graphOrigin.y - HEADER_PADDING[0]); + ctx.stroke(); + } + + ctx.restore(); + ctx.save(); + + ctx.translate(panelX, panelY); + if (spec.subtype === 'horizontal') { + horizontalBullet(ctx, cell.datum, graphOrigin, graphSize, style); + } else { + verticalBullet(ctx, cell.datum, graphOrigin, graphSize, style); + } + + ctx.restore(); + }), + ); + ctx.restore(); +} + +///////////////////////////// + +const TARGET_SIZE = 40; +const BULLET_SIZE = 32; +const BAR_SIZE = 12; + +function horizontalBullet( + ctx: CanvasRenderingContext2D, + + datum: BulletDatum, + origin: Point, + graphSize: Size, + style: BulletGraphStyle, +) { + ctx.save(); + ctx.translate(origin.x, origin.y); + + // TODO: add BASE + // const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; + const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, graphSize.width]).clamp(true); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(['#D9C6EF', '#AA87D1']); + const maxTicks = maxHorizontalTick(graphSize.width); + const colorTicks = scale.ticks(maxTicks - 1); + const colorBandSize = graphSize.width / colorTicks.length; + const { colors } = colorTicks.reduce<{ + last: number; + colors: Array<{ color: Color; size: number; position: number }>; + }>( + (acc, tick) => { + return { + last: acc.last + colorBandSize, + colors: [ + ...acc.colors, + { + color: `${colorScale(tick)}`, + size: colorBandSize, + position: acc.last, + }, + ], + }; + }, + { last: 0, colors: [] }, + ); + + // color bands + const verticalAlignment = TARGET_SIZE / 2 + 6; + colors.forEach((band, index) => { + ctx.fillStyle = band.color; + ctx.fillRect(band.position, verticalAlignment - BULLET_SIZE / 2, band.size, BULLET_SIZE); + }); + + // Ticks + ctx.beginPath(); + ctx.strokeStyle = style.background; + colorTicks + .filter((tick) => tick > datum.domain.min && tick < datum.domain.max) + .forEach((tick) => { + console.log(tick); + ctx.moveTo(scale(tick), verticalAlignment - BULLET_SIZE / 2); + ctx.lineTo(scale(tick), verticalAlignment + BULLET_SIZE / 2); + }); + ctx.stroke(); + + // Bar + ctx.fillStyle = style.barBackground; + ctx.fillRect(0, verticalAlignment - BAR_SIZE / 2, scale(datum.value), 12); + + // TARGET + if (isFiniteNumber(datum.target)) { + ctx.fillRect(scale(datum.target) - 1.5, verticalAlignment - TARGET_SIZE / 2, 3, TARGET_SIZE); + } + // Tick labels + // TODO: add text measurement + ctx.fillStyle = style.textColor; + ctx.textBaseline = 'top'; + ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + colorTicks.forEach((tick, i) => { + ctx.textAlign = i === colorTicks.length - 1 ? 'end' : 'start'; + ctx.fillText(datum.tickFormatter(tick), scale(tick), verticalAlignment + TARGET_SIZE / 2); + }); + + ctx.restore(); +} + +function verticalBullet( + ctx: CanvasRenderingContext2D, + datum: BulletDatum, + origin: Point, + graphSize: Size, + style: BulletGraphStyle, +) { + ctx.save(); + ctx.translate(origin.x, origin.y); + + // TODO: add BASE + // const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; + const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, graphSize.height]).clamp(true); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(['#D9C6EF', '#AA87D1']); + const maxTicks = 5; + const colorTicks = scale.ticks(maxTicks - 1); + const colorBandSize = graphSize.height / colorTicks.length; + const { colors } = colorTicks.reduce<{ + last: number; + colors: Array<{ color: Color; size: number; position: number }>; + }>( + (acc, tick) => { + return { + last: acc.last + colorBandSize, + colors: [ + ...acc.colors, + { + color: `${colorScale(tick)}`, + size: colorBandSize, + position: acc.last, + }, + ], + }; + }, + { last: 0, colors: [] }, + ); + + // color bands + + colors.forEach((band, index) => { + ctx.fillStyle = band.color; + ctx.fillRect( + graphSize.width / 2 - BULLET_SIZE / 2, + graphSize.height - band.position - band.size, + BULLET_SIZE, + band.size, + ); + }); + + // Ticks + ctx.beginPath(); + ctx.strokeStyle = style.background; + colorTicks + .filter((tick) => tick > datum.domain.min && tick < datum.domain.max) + .forEach((tick) => { + ctx.moveTo(graphSize.width / 2 - BULLET_SIZE / 2, graphSize.height - scale(tick)); + ctx.lineTo(graphSize.width / 2 + BULLET_SIZE / 2, graphSize.height - scale(tick)); + }); + ctx.stroke(); + + // Bar + ctx.fillStyle = style.barBackground; + ctx.fillRect(graphSize.width / 2 - BAR_SIZE / 2, graphSize.height - scale(datum.value), BAR_SIZE, scale(datum.value)); + + // target + if (isFiniteNumber(datum.target)) { + ctx.fillRect(graphSize.width / 2 - TARGET_SIZE / 2, graphSize.height - scale(datum.target) - 1.5, TARGET_SIZE, 3); + } + + // Tick labels + // TODO: add text measurement + ctx.textBaseline = 'top'; + ctx.fillStyle = style.textColor; + ctx.font = cssFontShorthand(TICK_FONT, TICK_FONT_SIZE); + colorTicks.forEach((tick, i) => { + ctx.textAlign = 'end'; + ctx.textBaseline = i === colorTicks.length - 1 ? 'hanging' : 'bottom'; + ctx.fillText(datum.tickFormatter(tick), graphSize.width / 2 - TARGET_SIZE / 2, graphSize.height - scale(tick)); + }); + + ctx.restore(); + return; +} + +function maxHorizontalTick(panelWidth: number) { + return panelWidth > 250 ? 4 : 3; +} +function maxVerticalTick(panelHeight: number) { + return panelHeight > 200 ? 4 : 3; +} diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx new file mode 100644 index 0000000000..9d2f018e3a --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx @@ -0,0 +1,224 @@ +/* + * 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. + */ + +/* eslint-disable-next-line eslint-comments/disable-enable-pair */ +/* eslint-disable react/no-array-index-key */ + +import { scaleLinear } from 'd3-scale'; +import React, { RefObject } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { renderBulletGraph } from './bullet_graph'; +import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { AlignedGrid } from '../../../../components/grid/aligned_grid'; +import { ElementClickListener, BasicListener, ElementOverListener } from '../../../../specs'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { + A11ySettings, + DEFAULT_A11Y_SETTINGS, + getA11ySettingsSelector, +} from '../../../../state/selectors/get_accessibility_config'; +import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { Size } from '../../../../utils/dimensions'; +import { deepEqual } from '../../../../utils/fast_deep_equal'; +import { Metric } from '../../../metric/renderer/dom/metric'; +import { getBulletGraphSpec, chartSize } from '../../selectors/chart_size'; +import { BulletGraphLayout, layout } from '../../selectors/layout'; +import { BulletDatum, BulletGraphSpec } from '../../spec'; +import { BulletGraphStyle, LIGHT_THEME_BULLET_STYLE } from '../../theme'; + +interface StateProps { + initialized: boolean; + chartId: string; + spec: BulletGraphSpec | undefined; + a11y: A11ySettings; + size: Size; + layout: BulletGraphLayout; + style: BulletGraphStyle; + onElementClick?: ElementClickListener; + onElementOut?: BasicListener; + onElementOver?: ElementOverListener; +} + +interface DispatchProps { + onChartRendered: typeof onChartRendered; +} + +interface OwnProps { + forwardStageRef: RefObject; +} + +type Props = DispatchProps & StateProps & OwnProps; + +class Component extends React.Component { + static displayName = 'BulletGraph'; + private ctx: CanvasRenderingContext2D | null; + private readonly devicePixelRatio: number; + + constructor(props: Readonly) { + super(props); + this.ctx = null; + this.devicePixelRatio = window.devicePixelRatio; + } + + componentDidMount() { + this.tryCanvasContext(); + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + shouldComponentUpdate(nextProps: Props) { + return !deepEqual(this.props, nextProps); + } + + componentDidUpdate() { + if (!this.ctx) { + this.tryCanvasContext(); + } + if (this.props.initialized) { + this.drawCanvas(); + this.props.onChartRendered(); + } + } + + private tryCanvasContext() { + const canvas = this.props.forwardStageRef.current; + this.ctx = canvas && canvas.getContext('2d'); + } + + private drawCanvas() { + if (this.ctx) { + renderBulletGraph(this.ctx, this.devicePixelRatio, this.props); + } + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + render() { + const { initialized, size, forwardStageRef, a11y, layout, spec } = this.props; + if (!initialized || size.width === 0 || size.height === 0) { + return null; + } + + return ( +
    + + {layout.shouldRenderMetric && ( +
    + + data={spec.data} + headerComponent={({ datum, stats }) => { + return null; + }} + contentComponent={({ datum, stats }) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const colorScale = scaleLinear() + .domain([datum.domain.min, datum.domain.max]) + .range(['#D9C6EF', '#AA87D1']); + return ( + + ); + }} + /> + ); +
    + )} +
    + ); + } +} + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => + bindActionCreators( + { + onChartRendered, + }, + dispatch, + ); + +const DEFAULT_PROPS: StateProps = { + initialized: false, + chartId: '', + spec: undefined, + size: { + width: 0, + height: 0, + }, + a11y: DEFAULT_A11Y_SETTINGS, + + layout: {}, + style: LIGHT_THEME_BULLET_STYLE, +}; + +const mapStateToProps = (state: GlobalChartState): StateProps => { + if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { + return DEFAULT_PROPS; + } + // const { onElementClick, onElementOut, onElementOver } = getSettingsSpecSelector(state); + return { + initialized: true, + chartId: state.chartId, + spec: getBulletGraphSpec(state)[0], + size: chartSize(state), + a11y: getA11ySettingsSelector(state), + layout: layout(state), + style: getChartThemeSelector(state).bulletGraph, + // onElementClick, + // onElementOver, + // onElementOut, + }; +}; + +/** @internal */ +export const BulletGraphRenderer = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_header.scss b/packages/charts/src/chart_types/bullet_graph/renderer/dom/_header.scss similarity index 100% rename from packages/charts/src/chart_types/bullet_graph/renderer/_header.scss rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/_header.scss diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/_index.scss b/packages/charts/src/chart_types/bullet_graph/renderer/dom/_index.scss similarity index 100% rename from packages/charts/src/chart_types/bullet_graph/renderer/_index.scss rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/_index.scss diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/dom/bullet.tsx similarity index 98% rename from packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/bullet.tsx index c3b88d68f8..d60fb9b885 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/bullet.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/dom/bullet.tsx @@ -9,10 +9,10 @@ import { clamp } from 'lodash'; import React, { memo, useCallback, useLayoutEffect, useRef, useState } from 'react'; -import { Color } from '../../../common/colors'; -import { Ratio } from '../../../common/geometry'; -import { Size } from '../../../utils/dimensions'; -import { BulletDatum } from '../spec'; +import { Color } from '../../../../common/colors'; +import { Ratio } from '../../../../common/geometry'; +import { Size } from '../../../../utils/dimensions'; +import { BulletDatum } from '../../spec'; /** @internal */ export interface BulletProps { diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/header.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/dom/header.tsx similarity index 100% rename from packages/charts/src/chart_types/bullet_graph/renderer/header.tsx rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/header.tsx diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/headertest.html b/packages/charts/src/chart_types/bullet_graph/renderer/dom/headertest.html similarity index 100% rename from packages/charts/src/chart_types/bullet_graph/renderer/headertest.html rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/headertest.html diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx similarity index 90% rename from packages/charts/src/chart_types/bullet_graph/renderer/index.tsx rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx index 76f3d9824b..609e2e5abf 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/index.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx @@ -16,21 +16,20 @@ import { bindActionCreators, Dispatch } from 'redux'; import { VerticalBullet, HorizontalBullet, AngularBullet } from './bullet'; import { Header } from './header'; -import { Color } from '../../../common/colors'; -import { AlignedGrid } from '../../../components/grid/aligned_grid'; -import { BasicListener, ElementClickListener, ElementOverListener } from '../../../specs/settings'; -import { onChartRendered } from '../../../state/actions/chart'; -import { GlobalChartState } from '../../../state/chart_state'; +import { AlignedGrid } from '../../../../components/grid/aligned_grid'; +import { ElementClickListener, BasicListener, ElementOverListener } from '../../../../specs'; +import { onChartRendered } from '../../../../state/actions/chart'; +import { GlobalChartState } from '../../../../state/chart_state'; import { A11ySettings, DEFAULT_A11Y_SETTINGS, getA11ySettingsSelector, -} from '../../../state/selectors/get_accessibility_config'; -import { getInternalIsInitializedSelector, InitStatus } from '../../../state/selectors/get_internal_is_intialized'; -import { Size } from '../../../utils/dimensions'; -import { Metric } from '../../metric/renderer/dom/metric'; -import { chartSize, getBulletGraphSpec } from '../selectors/chart_size'; -import { BulletDatum, BulletGraphSpec, BulletGraphSubtype } from '../spec'; +} from '../../../../state/selectors/get_accessibility_config'; +import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; +import { Size } from '../../../../utils/dimensions'; +import { Metric } from '../../../metric/renderer/dom/metric'; +import { getBulletGraphSpec, chartSize } from '../../selectors/chart_size'; +import { BulletGraphSpec, BulletDatum, BulletGraphSubtype } from '../../spec'; interface StateProps { initialized: boolean; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/test.html b/packages/charts/src/chart_types/bullet_graph/renderer/dom/test.html similarity index 100% rename from packages/charts/src/chart_types/bullet_graph/renderer/test.html rename to packages/charts/src/chart_types/bullet_graph/renderer/dom/test.html diff --git a/packages/charts/src/chart_types/bullet_graph/selectors/layout.ts b/packages/charts/src/chart_types/bullet_graph/selectors/layout.ts new file mode 100644 index 0000000000..1609ab6bb9 --- /dev/null +++ b/packages/charts/src/chart_types/bullet_graph/selectors/layout.ts @@ -0,0 +1,191 @@ +/* + * 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 { chartSize, getBulletGraphSpec } from './chart_size'; +import { ChartType } from '../../../chart_types'; +import { BulletDatum, BulletGraphSpec } from '../../../chart_types/bullet_graph/spec'; +import { SpecType } from '../../../specs'; +import { GlobalChartState } from '../../../state/chart_state'; +import { createCustomCachedSelector } from '../../../state/create_selector'; +import { getSpecsByType } from '../../../state/selectors/get_specs_by_type'; +import { withTextMeasure } from '../../../utils/bbox/canvas_text_bbox_calculator'; +import { Dimensions, Size } from '../../../utils/dimensions'; +import { wrapText } from '../../../utils/text/wrap'; +import { + GRAPH_PADDING, + HEADER_PADDING, + SUBTITLE_FONT, + SUBTITLE_FONT_SIZE, + SUBTITLE_LINE_HEIGHT, + TARGET_FONT, + TARGET_FONT_SIZE, + TITLE_FONT, + TITLE_FONT_SIZE, + TITLE_LINE_HEIGHT, + VALUE_FONT, + VALUE_FONT_SIZE, + VALUE_LINE_HEIGHT, +} from '../theme'; + +/** @internal */ +export interface BulletGraphLayout { + headerLayout: Array< + Array< + | { + panel: Size; + header: Size; + title: string[]; + subtitle: string | undefined; + value: string; + target: string; + multiline: boolean; + valueWidth: number; + targetWidth: number; + sizes: { title: number; subtitle: number; value: number; target: number }; + datum: BulletDatum; + } + | undefined + > + >; + layoutAlignment: Array<{ + maxTitleRows: number; + maxSubtitleRows: number; + multiline: boolean; + minHeight: number; + minWidth: number; + }>; + shouldRenderMetric: boolean; +} + +/** @internal */ +export const layout = createCustomCachedSelector([getBulletGraphSpec, chartSize], (specs, size): BulletGraphLayout => { + const spec = specs[0]!; + + const { data } = spec; + + const rows = data.length; + const columns = data.reduce((acc, row) => { + return Math.max(acc, row.length); + }, 0); + + const panel: Size = { width: size.width / columns, height: size.height / rows }; + const headerSize: Size = { + width: panel.width - HEADER_PADDING[1] - HEADER_PADDING[3], + height: panel.height - HEADER_PADDING[0] - HEADER_PADDING[2], + }; + + return withTextMeasure((textMeasurer) => { + // collect header elements title, subtitles and values + const header = data.map((row) => + row.map((cell) => { + if (!cell) { + return undefined; + } + const content = { + title: cell.title.trim(), + subtitle: cell.subtitle?.trim(), + value: cell.valueFormatter(cell.value), + target: cell.target ? `/ ${cell.valueFormatter(cell.target)}` : '', + datum: cell, + }; + const size = { + title: textMeasurer(content.title.trim(), TITLE_FONT, TITLE_FONT_SIZE).width, + subtitle: content.subtitle ? textMeasurer(content.subtitle, TITLE_FONT, TITLE_FONT_SIZE).width : 0, + value: textMeasurer(content.value, VALUE_FONT, VALUE_FONT_SIZE).width, + target: textMeasurer(content.target, TARGET_FONT, TARGET_FONT_SIZE).width, + }; + return { content, size }; + }), + ); + + const goesToMultiline = header.some((row) => + row.some((cell) => { + if (!cell) return false; + const valuesWidth = cell.size.value + cell.size.target; + return cell.content.subtitle + ? cell.size.subtitle + valuesWidth > headerSize.width || cell.size.title > headerSize.width + : cell.size.title + valuesWidth > headerSize.width; + }), + ); + + const headerLayout = header.map((row) => { + return row.map((cell) => { + if (!cell) return undefined; + if (goesToMultiline) { + // wrap only title if necessary + return { + panel, + header: headerSize, + title: wrapText(cell.content.title, TITLE_FONT, TITLE_FONT_SIZE, headerSize.width, 2, textMeasurer), + subtitle: cell.content.subtitle + ? wrapText(cell.content.subtitle, SUBTITLE_FONT, SUBTITLE_FONT_SIZE, headerSize.width, 1, textMeasurer)[0] + : undefined, + value: cell.content.value, + target: cell.content.target, + multiline: true, + valueWidth: cell.size.value, + targetWidth: cell.size.target, + sizes: cell.size, + datum: cell.content.datum, + }; + } + // wrap only title if necessary + return { + panel, + header: headerSize, + title: [cell.content.title], + subtitle: cell.content.subtitle ? cell.content.subtitle : undefined, + value: cell.content.value, + target: cell.content.target, + multiline: false, + valueWidth: cell.size.value, + targetWidth: cell.size.target, + sizes: cell.size, + datum: cell.content.datum, + }; + }); + }); + const layoutAlignment = headerLayout.map((curr) => { + return curr.reduce( + (rowStats, cell) => { + const maxTitleRows = Math.max(rowStats.maxTitleRows, cell?.title.length ?? 0); + const maxSubtitleRows = Math.max(rowStats.maxSubtitleRows, cell?.subtitle ? 1 : 0); + return { + maxTitleRows, + maxSubtitleRows, + multiline: cell?.multiline ?? false, + minHeight: + maxTitleRows * TITLE_LINE_HEIGHT + + maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (cell?.multiline ? VALUE_LINE_HEIGHT : 0) + + HEADER_PADDING[0] + + HEADER_PADDING[2] + + (spec.subtype === 'horizontal' ? 50 : 100), // chart height + minWidth: spec.subtype === 'horizontal' ? 140 : 140, + }; + }, + { maxTitleRows: 0, maxSubtitleRows: 0, multiline: false, minHeight: 0, minWidth: 0 }, + ); + }); + + const totalHeight = layoutAlignment.reduce((acc, curr) => { + return acc + curr.minHeight; + }, 0); + + const totalWidth = layoutAlignment.reduce((acc, curr) => { + return Math.max(acc, curr.minWidth); + }, 0); + const shouldRenderMetric = size.height <= totalHeight || size.width <= totalWidth * columns; + + return { + headerLayout, + layoutAlignment, + shouldRenderMetric, + }; + }); +}); diff --git a/packages/charts/src/chart_types/bullet_graph/theme.ts b/packages/charts/src/chart_types/bullet_graph/theme.ts index 81553e57b0..cb66f1a23a 100644 --- a/packages/charts/src/chart_types/bullet_graph/theme.ts +++ b/packages/charts/src/chart_types/bullet_graph/theme.ts @@ -7,14 +7,13 @@ */ import { Color } from '../../common/colors'; +import { DEFAULT_FONT_FAMILY } from '../../common/default_theme_attributes'; import { Pixels } from '../../common/geometry'; +import { Font } from '../../common/text_utils'; /** @public */ export interface BulletGraphStyle { - text: { - darkColor: Color; - lightColor: Color; - }; + textColor: Color; border: Color; background: Color; barBackground: Color; @@ -24,10 +23,7 @@ export interface BulletGraphStyle { /** @internal */ export const LIGHT_THEME_BULLET_STYLE: BulletGraphStyle = { - text: { - lightColor: '#E0E5EE', - darkColor: '#343741', - }, + textColor: '#343741', border: '#EDF0F5', barBackground: '#343741', background: '#FFFFFF', @@ -37,13 +33,64 @@ export const LIGHT_THEME_BULLET_STYLE: BulletGraphStyle = { /** @internal */ export const DARK_THEME_BULLET_STYLE: BulletGraphStyle = { - text: { - lightColor: '#E0E5EE', - darkColor: '#343741', - }, + textColor: '#E0E5EE', border: '#343741', - barBackground: '#343741', + barBackground: '#FFF', background: '#1D1E23', nonFiniteText: 'N/A', minHeight: 64, }; + +/** @internal */ +export const TITLE_FONT: Font = { + fontStyle: 'normal', + fontFamily: DEFAULT_FONT_FAMILY, + fontVariant: 'normal', + fontWeight: 'bold', + textColor: 'black', +}; +/** @internal */ +export const TITLE_FONT_SIZE = 16; +/** @internal */ +export const TITLE_LINE_HEIGHT = 19; + +/** @internal */ +export const SUBTITLE_FONT: Font = { + ...TITLE_FONT, + fontWeight: 'normal', +}; +/** @internal */ +export const SUBTITLE_FONT_SIZE = 14; +/** @internal */ +export const SUBTITLE_LINE_HEIGHT = 16; + +/** @internal */ +export const VALUE_FONT: Font = { + ...TITLE_FONT, +}; +/** @internal */ +export const VALUE_FONT_SIZE = 22; +/** @internal */ +export const VALUE_LINE_HEIGHT = 22; + +/** @internal */ +export const TARGET_FONT: Font = { + ...SUBTITLE_FONT, +}; +/** @internal */ +export const TARGET_FONT_SIZE = 16; +/** @internal */ +export const TARGET_LINE_HEIGHT = 16; + +/** @internal */ +export const TICK_FONT: Font = { + ...TITLE_FONT, + fontWeight: 'normal', +}; +/** @internal */ +export const TICK_FONT_SIZE = 10; + +/** @internal */ +export const HEADER_PADDING: [number, number, number, number] = [8, 8, 8, 8]; +/** @internal */ +export const GRAPH_PADDING: [number, number, number, number] = [8, 8, 8, 8]; diff --git a/packages/charts/src/common/default_theme_attributes.ts b/packages/charts/src/common/default_theme_attributes.ts index ac9a518571..4415fc8185 100644 --- a/packages/charts/src/common/default_theme_attributes.ts +++ b/packages/charts/src/common/default_theme_attributes.ts @@ -8,4 +8,4 @@ /** @internal */ export const DEFAULT_FONT_FAMILY = - '"Inter UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; + '"Inter UI", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; //'Inter UI, -apple-system, Segoe UI, Helvetica, Arial, sans-serif'; diff --git a/packages/charts/src/components/_index.scss b/packages/charts/src/components/_index.scss index f752ffe4fd..3998c4b950 100644 --- a/packages/charts/src/components/_index.scss +++ b/packages/charts/src/components/_index.scss @@ -12,4 +12,4 @@ @import '../chart_types/xy_chart/renderer/index'; @import '../chart_types/partition_chart/renderer/index'; @import '../chart_types/metric/renderer/index'; -@import '../chart_types/bullet_graph/renderer/index'; +@import '../chart_types/bullet_graph/renderer/dom/index'; diff --git a/storybook/stories/bullet_graph/1_simple.story.tsx b/storybook/stories/bullet_graph/1_simple.story.tsx index 8b14c69fb1..06790abe9f 100644 --- a/storybook/stories/bullet_graph/1_simple.story.tsx +++ b/storybook/stories/bullet_graph/1_simple.story.tsx @@ -8,8 +8,9 @@ import React from 'react'; -import { Chart, BulletGraph, BulletGraphSubtype } from '@elastic/charts'; +import { Chart, BulletGraph, BulletGraphSubtype, Settings } from '@elastic/charts'; +import { useBaseTheme } from '../../use_base_theme'; import { getKnobFromEnum } from '../utils/knobs/utils'; export const Example = () => { @@ -18,7 +19,7 @@ export const Example = () => {
    { }} > + { valueFormatter: (d) => `${d}`, tickFormatter: (d) => `${d}`, }, - // { - // ticks: 'auto', - // target: 67, - // value: 123, - // title: 'First row second column title', - // domain: { min: 0, max: 100, nice: false }, - // valueFormatter: (d) => `${d}`, - // tickFormatter: (d) => `${d}`, - // }, + { + ticks: 'auto', + target: 67, + value: 123, + title: 'First row second column title', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}`, + tickFormatter: (d) => `${d}`, + }, + ], + [ + { + ticks: 'auto', + target: 50, + value: 11, + title: 'dsads', + subtitle: 'Second row first column subtitle', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}`, + tickFormatter: (d) => `${d}`, + }, + { + ticks: 'auto', + target: 80, + value: 92, + title: 'Second row second column title', + subtitle: 'percentage', + domain: { min: 0, max: 100, nice: false }, + valueFormatter: (d) => `${d}`, + tickFormatter: (d) => `${d}`, + }, ], - // [ - // { - // ticks: 'auto', - // target: 50, - // value: 11, - // title: '', - // subtitle: 'Second row first column subtitle', - // domain: { min: 0, max: 100, nice: false }, - // valueFormatter: (d) => `${d}`, - // tickFormatter: (d) => `${d}`, - // }, - // { - // ticks: 'auto', - // target: 80, - // value: 91, - // title: 'Second row second column title', - // subtitle: 'percentage', - // domain: { min: 0, max: 100, nice: false }, - // valueFormatter: (d) => `${d}`, - // tickFormatter: (d) => `${d}`, - // }, - // ], ]} /> diff --git a/storybook/stories/bullet_graph/2_horizontal.story.tsx b/storybook/stories/bullet_graph/2_horizontal.story.tsx index e1deb59546..e3880e4bf1 100644 --- a/storybook/stories/bullet_graph/2_horizontal.story.tsx +++ b/storybook/stories/bullet_graph/2_horizontal.story.tsx @@ -54,7 +54,7 @@ export const Example = () => { ticks: 'auto', target: 25, value: 35.5, - title: 'Network In very long title two lines', + title: 'Network In verydss', domain: { min: 0, max: 100, nice: false }, valueFormatter: (d) => `${d}%`, tickFormatter: (d) => `${d}%`, diff --git a/storybook/stories/bullet_graph/3_vertical.story.tsx b/storybook/stories/bullet_graph/3_vertical.story.tsx index 6794481b20..4425afb548 100644 --- a/storybook/stories/bullet_graph/3_vertical.story.tsx +++ b/storybook/stories/bullet_graph/3_vertical.story.tsx @@ -17,14 +17,13 @@ export const Example = () => { return (
    { [ { ticks: 'auto', - target: 25, + target: 80, value: 91, - title: 'Network outtage', - + title: '', + subtitle: ' longer title', domain: { min: 0, max: 100, nice: false }, valueFormatter: (d) => `${d}%`, tickFormatter: (d) => `${d}%`, diff --git a/storybook/style.scss b/storybook/style.scss index 5f17506f4d..73e5659d7c 100644 --- a/storybook/style.scss +++ b/storybook/style.scss @@ -168,3 +168,10 @@ body { .euiPopover__anchor { width: 100%; } + +.resizable { + resize: both; + overflow: auto; + width: 500px; + height: 600px; +} From 2f171268d668da9701982a391e10d2eb60ff5dd3 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 15 May 2023 19:32:33 +0200 Subject: [PATCH 7/7] WIP --- .../renderer/canvas/bullet_graph.ts | 310 +++++++++--------- .../bullet_graph/renderer/canvas/index.tsx | 34 +- .../bullet_graph/renderer/dom/index.tsx | 1 + .../bullet_graph/renderer/dom/test.html | 2 +- .../bullet_graph/selectors/layout.ts | 28 +- .../src/chart_types/bullet_graph/theme.ts | 15 +- .../src/common/default_theme_attributes.ts | 4 +- .../stories/bullet_graph/1_simple.story.tsx | 17 +- .../stories/bullet_graph/1_single.story.tsx | 62 ++++ .../bullet_graph/2_horizontal.story.tsx | 22 +- .../stories/bullet_graph/3_vertical.story.tsx | 52 ++- .../bullet_graph/bullet_graph.stories.tsx | 5 +- .../goal/3_horizontal_bullet.story.tsx | 50 +-- .../stories/goal/4_vertical_bullet.story.tsx | 50 +-- 14 files changed, 387 insertions(+), 265 deletions(-) create mode 100644 storybook/stories/bullet_graph/1_single.story.tsx diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts index af9b994a3c..71015e1d83 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/bullet_graph.ts @@ -11,9 +11,10 @@ import { scaleLinear } from 'd3-scale'; import { Color } from '../../../../common/colors'; import { Ratio } from '../../../../common/geometry'; import { cssFontShorthand } from '../../../../common/text_utils'; +import { withContext, clearCanvas } from '../../../../renderers/canvas'; import { A11ySettings } from '../../../../state/selectors/get_accessibility_config'; import { measureText } from '../../../../utils/bbox/canvas_text_bbox_calculator'; -import { isFiniteNumber } from '../../../../utils/common'; +import { clamp, isFiniteNumber } from '../../../../utils/common'; import { Size } from '../../../../utils/dimensions'; import { Point } from '../../../../utils/point'; import { wrapText } from '../../../../utils/text/wrap'; @@ -48,125 +49,140 @@ export function renderBulletGraph( size: Size; layout: BulletGraphLayout; style: BulletGraphStyle; + bandColors: [string, string]; }, ) { - const { style, layout, spec } = props; - if (!spec) { - return; - } - ctx.save(); - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - console.log(props.style.background); - ctx.fillStyle = props.style.background; - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - if (props.layout.shouldRenderMetric) { - return; - } - ctx.fillStyle = props.style.background; - //@ts-expect-error - ctx.letterSpacing = 'normal'; - - layout.headerLayout.forEach((row, rowIndex) => - row.forEach((cell, columnIndex) => { - if (!cell) return; - - const verticalAlignment = layout.layoutAlignment[rowIndex]!; - const panelY = cell.panel.height * rowIndex; - const panelX = cell.panel.width * columnIndex; - - ctx.save(); - ctx.strokeStyle = style.border; - if (row.length > 1 && columnIndex < row.length - 1) { - ctx.beginPath(); - ctx.moveTo(panelX + cell.panel.width, panelY); - ctx.lineTo(panelX + cell.panel.width, panelY + cell.panel.height); - ctx.stroke(); - } - - ctx.translate(panelX + HEADER_PADDING[3], panelY + HEADER_PADDING[0]); - - ctx.fillStyle = props.style.textColor; - // TITLE - ctx.textBaseline = 'top'; - ctx.textAlign = 'start'; - ctx.font = cssFontShorthand(TITLE_FONT, TITLE_FONT_SIZE); - cell.title.forEach((line, lineIndex) => { - ctx.fillText(line, 0, lineIndex * TITLE_LINE_HEIGHT); - }); - - // SUBTITLE - if (cell.subtitle) { - const y = verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT; - ctx.font = cssFontShorthand(SUBTITLE_FONT, SUBTITLE_FONT_SIZE); - ctx.fillText(cell.subtitle, 0, y); - } - - // VALUE - ctx.textBaseline = 'alphabetic'; - ctx.font = cssFontShorthand(VALUE_FONT, VALUE_FONT_SIZE); - if (!cell.multiline) ctx.textAlign = 'end'; - - const valueY = - verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + - verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + - (cell.multiline ? TARGET_FONT_SIZE : 0); - const valueX = cell.multiline ? 0 : cell.header.width - cell.targetWidth; - ctx.fillText(cell.value, valueX, valueY); - - // TARGET - ctx.font = cssFontShorthand(TARGET_FONT, TARGET_FONT_SIZE); - if (!cell.multiline) ctx.textAlign = 'end'; - const targetX = cell.multiline ? cell.valueWidth : cell.header.width; - const targetY = - verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + - verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + - (cell.multiline ? TARGET_FONT_SIZE : 0); - ctx.fillText(cell.target, targetX, targetY); - - const graphSize = { - width: cell.panel.width - (GRAPH_PADDING[1] + GRAPH_PADDING[3]), - height: - cell.panel.height - - (HEADER_PADDING[0] + - verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + - verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + - (cell.multiline ? VALUE_LINE_HEIGHT : 0) + - GRAPH_PADDING[0] + - GRAPH_PADDING[2]), - }; - const graphOrigin = { - x: GRAPH_PADDING[3], - y: - HEADER_PADDING[0] + - verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + - verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + - (cell.multiline ? VALUE_LINE_HEIGHT : 0) + - GRAPH_PADDING[0], - }; - - if (spec.subtype === 'vertical') { - ctx.strokeStyle = style.border; - ctx.beginPath(); - ctx.moveTo(panelX, graphOrigin.y - HEADER_PADDING[0]); - ctx.lineTo(panelX + cell.panel.width, graphOrigin.y - HEADER_PADDING[0]); - ctx.stroke(); - } - - ctx.restore(); - ctx.save(); - - ctx.translate(panelX, panelY); - if (spec.subtype === 'horizontal') { - horizontalBullet(ctx, cell.datum, graphOrigin, graphSize, style); - } else { - verticalBullet(ctx, cell.datum, graphOrigin, graphSize, style); - } - - ctx.restore(); - }), - ); - ctx.restore(); + const { style, layout, spec, bandColors } = props; + withContext(ctx, (ctx) => { + ctx.scale(dpr, dpr); + clearCanvas(ctx, props.style.background); + + // clear only if need to render metric or no spec available + if (!spec || layout.shouldRenderMetric) { + return; + } + + // render each Small multiple + ctx.fillStyle = props.style.background; + //@ts-expect-error + ctx.letterSpacing = 'normal'; + + layout.headerLayout.forEach((row, rowIndex) => + row.forEach((bulletGraph, columnIndex) => { + if (!bulletGraph) return; + const { panel, multiline } = bulletGraph; + withContext(ctx, (ctx) => { + const verticalAlignment = layout.layoutAlignment[rowIndex]!; + const panelY = panel.height * rowIndex; + const panelX = panel.width * columnIndex; + + // ctx.strokeRect(panelX, panelY, panel.width, panel.height); + + // move into the panel position + ctx.translate(panelX, panelY); + + // paint right border + // TODO: check paddings + ctx.strokeStyle = style.border; + if (row.length > 1 && columnIndex < row.length - 1) { + ctx.beginPath(); + ctx.moveTo(panel.width, 0); + ctx.lineTo(panel.width, panel.height); + ctx.stroke(); + } + + if (layout.headerLayout.length > 1 && columnIndex < layout.headerLayout.length) { + ctx.beginPath(); + ctx.moveTo(0, panel.height); + ctx.lineTo(panel.width, panel.height); + ctx.stroke(); + } + + // this helps render the header without considering paddings + ctx.translate(HEADER_PADDING.left, HEADER_PADDING.top); + + // TITLE + ctx.fillStyle = props.style.textColor; + ctx.textBaseline = 'top'; + ctx.textAlign = 'start'; + ctx.font = cssFontShorthand(TITLE_FONT, TITLE_FONT_SIZE); + bulletGraph.title.forEach((titleLine, lineIndex) => { + const y = lineIndex * TITLE_LINE_HEIGHT; + ctx.fillText(titleLine, 0, y); + }); + + // SUBTITLE + if (bulletGraph.subtitle) { + const y = verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT; + ctx.font = cssFontShorthand(SUBTITLE_FONT, SUBTITLE_FONT_SIZE); + ctx.fillText(bulletGraph.subtitle, 0, y); + } + + // VALUE + ctx.textBaseline = 'alphabetic'; + ctx.font = cssFontShorthand(VALUE_FONT, VALUE_FONT_SIZE); + if (!multiline) ctx.textAlign = 'end'; + { + const y = + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (multiline ? TARGET_FONT_SIZE : 0); + const x = multiline ? 0 : bulletGraph.header.width - bulletGraph.targetWidth; + ctx.fillText(bulletGraph.value, x, y); + } + + // TARGET + ctx.font = cssFontShorthand(TARGET_FONT, TARGET_FONT_SIZE); + if (!multiline) ctx.textAlign = 'end'; + { + const x = multiline ? bulletGraph.valueWidth : bulletGraph.header.width; + const y = + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT + + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT + + (multiline ? TARGET_FONT_SIZE : 0); + ctx.fillText(bulletGraph.target, x, y); + } + + const graphSize = { + width: panel.width, + height: + panel.height - + HEADER_PADDING.top - + verticalAlignment.maxTitleRows * TITLE_LINE_HEIGHT - + verticalAlignment.maxSubtitleRows * SUBTITLE_LINE_HEIGHT - + (multiline ? VALUE_LINE_HEIGHT : 0) - + HEADER_PADDING.bottom, + }; + const graphOrigin = { + x: 0, + y: panel.height - graphSize.height, + }; + ctx.translate(-HEADER_PADDING.left, -HEADER_PADDING.top); + + if (spec.subtype === 'vertical') { + ctx.strokeStyle = style.border; + ctx.beginPath(); + ctx.moveTo(HEADER_PADDING.left, graphOrigin.y); + ctx.lineTo(panel.width - HEADER_PADDING.right, graphOrigin.y); + ctx.stroke(); + } + + withContext(ctx, (ctx) => { + ctx.translate(graphOrigin.x, graphOrigin.y); + + //DEBUG + //ctx.strokeRect(0, 0, graphSize.width, graphSize.height); + + if (spec.subtype === 'horizontal') { + horizontalBullet(ctx, bulletGraph.datum, graphOrigin, graphSize, style, bandColors); + } else { + verticalBullet(ctx, bulletGraph.datum, graphOrigin, graphSize, style, bandColors); + } + }); + }); + }), + ); + }); } ///////////////////////////// @@ -182,19 +198,19 @@ function horizontalBullet( origin: Point, graphSize: Size, style: BulletGraphStyle, + bandColors: [string, string], ) { - ctx.save(); - ctx.translate(origin.x, origin.y); - + ctx.translate(GRAPH_PADDING.left, 0); // TODO: add BASE // const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; - const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, graphSize.width]).clamp(true); + const paddedWidth = graphSize.width - GRAPH_PADDING.left - GRAPH_PADDING.right; + const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, paddedWidth]); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(['#D9C6EF', '#AA87D1']); + const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(bandColors); const maxTicks = maxHorizontalTick(graphSize.width); const colorTicks = scale.ticks(maxTicks - 1); - const colorBandSize = graphSize.width / colorTicks.length; + const colorBandSize = paddedWidth / colorTicks.length; const { colors } = colorTicks.reduce<{ last: number; colors: Array<{ color: Color; size: number; position: number }>; @@ -216,8 +232,8 @@ function horizontalBullet( ); // color bands - const verticalAlignment = TARGET_SIZE / 2 + 6; - colors.forEach((band, index) => { + const verticalAlignment = TARGET_SIZE / 2; + colors.forEach((band) => { ctx.fillStyle = band.color; ctx.fillRect(band.position, verticalAlignment - BULLET_SIZE / 2, band.size, BULLET_SIZE); }); @@ -236,10 +252,10 @@ function horizontalBullet( // Bar ctx.fillStyle = style.barBackground; - ctx.fillRect(0, verticalAlignment - BAR_SIZE / 2, scale(datum.value), 12); + ctx.fillRect(0, verticalAlignment - BAR_SIZE / 2, scale(clamp(datum.value, datum.domain.min, datum.domain.max)), 12); // TARGET - if (isFiniteNumber(datum.target)) { + if (isFiniteNumber(datum.target) && (datum.target <= datum.domain.max || datum.target >= datum.domain.min)) { ctx.fillRect(scale(datum.target) - 1.5, verticalAlignment - TARGET_SIZE / 2, 3, TARGET_SIZE); } // Tick labels @@ -251,8 +267,6 @@ function horizontalBullet( ctx.textAlign = i === colorTicks.length - 1 ? 'end' : 'start'; ctx.fillText(datum.tickFormatter(tick), scale(tick), verticalAlignment + TARGET_SIZE / 2); }); - - ctx.restore(); } function verticalBullet( @@ -261,19 +275,21 @@ function verticalBullet( origin: Point, graphSize: Size, style: BulletGraphStyle, + bandColors: [string, string], ) { - ctx.save(); - ctx.translate(origin.x, origin.y); - + ctx.translate(0, GRAPH_PADDING.top); + const graphPaddedHeight = graphSize.height - GRAPH_PADDING.bottom - GRAPH_PADDING.top; // TODO: add BASE // const base = datum.domain.min < 0 && datum.domain.max > 0 ? 0 : NaN; - const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, graphSize.height]).clamp(true); + const scale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range([0, graphPaddedHeight]).clamp(true); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(['#D9C6EF', '#AA87D1']); - const maxTicks = 5; + //#6092C0, #3F4E61 + + const colorScale = scaleLinear().domain([datum.domain.min, datum.domain.max]).range(bandColors); + const maxTicks = 4; const colorTicks = scale.ticks(maxTicks - 1); - const colorBandSize = graphSize.height / colorTicks.length; + const colorBandSize = graphPaddedHeight / colorTicks.length; const { colors } = colorTicks.reduce<{ last: number; colors: Array<{ color: Color; size: number; position: number }>; @@ -300,7 +316,7 @@ function verticalBullet( ctx.fillStyle = band.color; ctx.fillRect( graphSize.width / 2 - BULLET_SIZE / 2, - graphSize.height - band.position - band.size, + graphPaddedHeight - band.position - band.size, BULLET_SIZE, band.size, ); @@ -312,18 +328,23 @@ function verticalBullet( colorTicks .filter((tick) => tick > datum.domain.min && tick < datum.domain.max) .forEach((tick) => { - ctx.moveTo(graphSize.width / 2 - BULLET_SIZE / 2, graphSize.height - scale(tick)); - ctx.lineTo(graphSize.width / 2 + BULLET_SIZE / 2, graphSize.height - scale(tick)); + ctx.moveTo(graphSize.width / 2 - BULLET_SIZE / 2, graphPaddedHeight - scale(tick)); + ctx.lineTo(graphSize.width / 2 + BULLET_SIZE / 2, graphPaddedHeight - scale(tick)); }); ctx.stroke(); // Bar ctx.fillStyle = style.barBackground; - ctx.fillRect(graphSize.width / 2 - BAR_SIZE / 2, graphSize.height - scale(datum.value), BAR_SIZE, scale(datum.value)); + ctx.fillRect( + graphSize.width / 2 - BAR_SIZE / 2, + graphPaddedHeight - scale(datum.value), + BAR_SIZE, + scale(datum.value), + ); // target if (isFiniteNumber(datum.target)) { - ctx.fillRect(graphSize.width / 2 - TARGET_SIZE / 2, graphSize.height - scale(datum.target) - 1.5, TARGET_SIZE, 3); + ctx.fillRect(graphSize.width / 2 - TARGET_SIZE / 2, graphPaddedHeight - scale(datum.target) - 1.5, TARGET_SIZE, 3); } // Tick labels @@ -334,11 +355,8 @@ function verticalBullet( colorTicks.forEach((tick, i) => { ctx.textAlign = 'end'; ctx.textBaseline = i === colorTicks.length - 1 ? 'hanging' : 'bottom'; - ctx.fillText(datum.tickFormatter(tick), graphSize.width / 2 - TARGET_SIZE / 2, graphSize.height - scale(tick)); + ctx.fillText(datum.tickFormatter(tick), graphSize.width / 2 - TARGET_SIZE / 2 - 6, graphPaddedHeight - scale(tick)); }); - - ctx.restore(); - return; } function maxHorizontalTick(panelWidth: number) { diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx index 9d2f018e3a..d670205d24 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/canvas/index.tsx @@ -15,7 +15,6 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { renderBulletGraph } from './bullet_graph'; -import { ScreenReaderSummary } from '../../../../components/accessibility'; import { AlignedGrid } from '../../../../components/grid/aligned_grid'; import { ElementClickListener, BasicListener, ElementOverListener } from '../../../../specs'; import { onChartRendered } from '../../../../state/actions/chart'; @@ -43,6 +42,7 @@ interface StateProps { size: Size; layout: BulletGraphLayout; style: BulletGraphStyle; + bandColors: [string, string]; onElementClick?: ElementClickListener; onElementOut?: BasicListener; onElementOver?: ElementOverListener; @@ -104,8 +104,8 @@ class Component extends React.Component { // eslint-disable-next-line @typescript-eslint/member-ordering render() { - const { initialized, size, forwardStageRef, a11y, layout, spec } = this.props; - if (!initialized || size.width === 0 || size.height === 0) { + const { initialized, size, forwardStageRef, a11y, layout, spec, style } = this.props; + if (!initialized || size.width === 0 || size.height === 0 || !spec) { return null; } @@ -134,29 +134,34 @@ class Component extends React.Component { return null; }} contentComponent={({ datum, stats }) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore const colorScale = scaleLinear() .domain([datum.domain.min, datum.domain.max]) - .range(['#D9C6EF', '#AA87D1']); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + .range(this.props.bandColors); return ( + target: {datum.valueFormatter(datum.target)} + + ) : undefined, }} totalRows={stats.rows} totalColumns={stats.columns} columnIndex={stats.columnIndex} rowIndex={stats.rowIndex} style={{ - background: 'white', + background: style.background, barBackground: `${colorScale(datum.value)}`, border: 'gray', minHeight: 0, @@ -197,14 +202,21 @@ const DEFAULT_PROPS: StateProps = { }, a11y: DEFAULT_A11Y_SETTINGS, - layout: {}, + layout: { + headerLayout: [], + layoutAlignment: [], + shouldRenderMetric: false, + }, style: LIGHT_THEME_BULLET_STYLE, + bandColors: ['#D9C6EF', '#AA87D1'], }; const mapStateToProps = (state: GlobalChartState): StateProps => { if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { return DEFAULT_PROPS; } + const theme = getChartThemeSelector(state); + // const { onElementClick, onElementOut, onElementOver } = getSettingsSpecSelector(state); return { initialized: true, @@ -213,7 +225,9 @@ const mapStateToProps = (state: GlobalChartState): StateProps => { size: chartSize(state), a11y: getA11ySettingsSelector(state), layout: layout(state), - style: getChartThemeSelector(state).bulletGraph, + style: theme.bulletGraph, + bandColors: theme.background.fallbackColor === 'black' ? ['#6092C0', '#3F4E61'] : ['#D9C6EF', '#AA87D1'], //['#6092C0', '#3F4E61'] + //.range(['#D9C6EF', '#AA87D1']); // onElementClick, // onElementOver, // onElementOut, diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx b/packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx index 609e2e5abf..06f6d392a3 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx +++ b/packages/charts/src/chart_types/bullet_graph/renderer/dom/index.tsx @@ -16,6 +16,7 @@ import { bindActionCreators, Dispatch } from 'redux'; import { VerticalBullet, HorizontalBullet, AngularBullet } from './bullet'; import { Header } from './header'; +import { Color } from '../../../../common/colors'; import { AlignedGrid } from '../../../../components/grid/aligned_grid'; import { ElementClickListener, BasicListener, ElementOverListener } from '../../../../specs'; import { onChartRendered } from '../../../../state/actions/chart'; diff --git a/packages/charts/src/chart_types/bullet_graph/renderer/dom/test.html b/packages/charts/src/chart_types/bullet_graph/renderer/dom/test.html index c080789fc2..0afc023bec 100644 --- a/packages/charts/src/chart_types/bullet_graph/renderer/dom/test.html +++ b/packages/charts/src/chart_types/bullet_graph/renderer/dom/test.html @@ -2,7 +2,7 @@ - Title + Canvas vs HTML rendering