From 87629e23e7cc8aef125a8b6d2b87092085abe2bd Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Thu, 8 Jul 2021 06:54:20 -0600 Subject: [PATCH] feat(a11y): allow user to add optional semantic meaning to goal/gauge charts (#1218) Closes #1161 --- ...emantics-visually-looks-correct-1-snap.png | Bin 0 -> 20885 bytes packages/charts/api/charts.api.md | 2 + .../layout/types/viewmodel_types.ts | 5 +- .../goal_chart/layout/viewmodel/viewmodel.ts | 2 + .../renderer/canvas/canvas_renderers.ts | 1 - .../renderer/canvas/connected_component.tsx | 15 +++- .../src/chart_types/goal_chart/specs/index.ts | 2 + .../state/selectors/get_goal_chart_data.ts | 10 +++ .../accessibility/accessibility.test.tsx | 28 +++++++ .../goal_semantic_description.tsx | 54 ++++++++++++++ .../src/components/accessibility/index.ts | 1 + stories/goal/25_goal_semantic.tsx | 69 ++++++++++++++++++ stories/goal/goal.stories.tsx | 1 + 13 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-goal-semantics-visually-looks-correct-1-snap.png create mode 100644 packages/charts/src/components/accessibility/goal_semantic_description.tsx create mode 100644 stories/goal/25_goal_semantic.tsx diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-goal-semantics-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-goal-alpha-goal-semantics-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..dabc2a62314bca7d857ae262f69b746b1ae10e2d GIT binary patch literal 20885 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 literal 0 HcmV?d00001 diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index a50731c973..fe6d682f8b 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -783,6 +783,8 @@ export interface GoalSpec extends Spec { // (undocumented) bandFillColor: BandFillColorAccessor; // (undocumented) + bandLabels: string[]; + // (undocumented) bands: number[]; // (undocumented) base: number; diff --git a/packages/charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts b/packages/charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts index d9882a6674..e8e1b5732a 100644 --- a/packages/charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts +++ b/packages/charts/src/chart_types/goal_chart/layout/types/viewmodel_types.ts @@ -24,9 +24,11 @@ import { GoalSubtype } from '../../specs/constants'; import { config } from '../config/config'; import { Config } from './config_types'; -interface BandViewModel { +/** @internal */ +export interface BandViewModel { value: number; fillColor: string; + text: string[]; } interface TickViewModel { @@ -90,6 +92,7 @@ export const defaultGoalSpec = { labelMinor: ({}: BandFillColorAccessorInput) => 'unit', centralMajor: ({ base }: BandFillColorAccessorInput) => String(base), centralMinor: ({ target }: BandFillColorAccessorInput) => String(target), + bandLabels: [], }; /** @internal */ diff --git a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts index 06cd9e727a..e50e8d57a4 100644 --- a/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts +++ b/packages/charts/src/chart_types/goal_chart/layout/viewmodel/viewmodel.ts @@ -52,6 +52,7 @@ export function shapeViewModel(textMeasure: TextMeasure, spec: GoalSpec, config: labelMinor, centralMajor, centralMinor, + bandLabels, } = spec; const [lowestValue, highestValue] = [base, target, actual, ...bands, ...ticks].reduce( @@ -80,6 +81,7 @@ export function shapeViewModel(textMeasure: TextMeasure, spec: GoalSpec, config: bands: bands.map((value: number, index: number) => ({ value, fillColor: bandFillColor({ value, index, ...callbackArgs }), + text: bandLabels, })), ticks: ticks.map((value: number, index: number) => ({ value, diff --git a/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts b/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts index a6c1622199..1972c028a3 100644 --- a/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts +++ b/packages/charts/src/chart_types/goal_chart/renderer/canvas/canvas_renderers.ts @@ -85,7 +85,6 @@ export function renderCanvas2d( const vertical = subtype === GoalSubtype.VerticalBullet; const domain = [lowestValue, highestValue]; - const data = { base: { value: base }, ...Object.fromEntries(bands.map(({ value }, index) => [`qualitative_${index}`, { value }])), diff --git a/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx b/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx index eb3fe6c1f3..bf853d41de 100644 --- a/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx +++ b/packages/charts/src/chart_types/goal_chart/renderer/canvas/connected_component.tsx @@ -21,7 +21,7 @@ import React, { MouseEvent, RefObject } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { GoalSemanticDescription, ScreenReaderSummary } from '../../../../components/accessibility'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; import { @@ -31,8 +31,9 @@ import { } from '../../../../state/selectors/get_accessibility_config'; import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized'; import { Dimensions } from '../../../../utils/dimensions'; -import { nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; +import { BandViewModel, nullShapeViewModel, ShapeViewModel } from '../../layout/types/viewmodel_types'; import { geometries } from '../../state/selectors/geometries'; +import { getFirstTickValueSelector, getGoalChartSemanticDataSelector } from '../../state/selectors/get_goal_chart_data'; import { renderCanvas2d } from './canvas_renderers'; interface ReactiveChartStateProps { @@ -40,6 +41,8 @@ interface ReactiveChartStateProps { geometries: ShapeViewModel; chartContainerDimensions: Dimensions; a11ySettings: A11ySettings; + bandLabels: BandViewModel[]; + firstValue: number; } interface ReactiveChartDispatchProps { @@ -112,11 +115,12 @@ class Component extends React.Component { chartContainerDimensions: { width, height }, forwardStageRef, a11ySettings, + bandLabels, + firstValue, } = this.props; if (!initialized || width === 0 || height === 0) { return null; } - return (
{ role="presentation" > +
); @@ -172,6 +177,8 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { top: 0, }, a11ySettings: DEFAULT_A11Y_SETTINGS, + bandLabels: [], + firstValue: 0, }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -183,6 +190,8 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { geometries: geometries(state), chartContainerDimensions: state.parentDimensions, a11ySettings: getA11ySettingsSelector(state), + bandLabels: getGoalChartSemanticDataSelector(state), + firstValue: getFirstTickValueSelector(state), }; }; diff --git a/packages/charts/src/chart_types/goal_chart/specs/index.ts b/packages/charts/src/chart_types/goal_chart/specs/index.ts index 2451429087..cd3d9fbe63 100644 --- a/packages/charts/src/chart_types/goal_chart/specs/index.ts +++ b/packages/charts/src/chart_types/goal_chart/specs/index.ts @@ -67,6 +67,7 @@ export interface GoalSpec extends Spec { centralMajor: string | BandFillColorAccessor; centralMinor: string | BandFillColorAccessor; config: RecursivePartial; + bandLabels: string[]; } type SpecRequiredProps = Pick; @@ -83,6 +84,7 @@ export const Goal: React.FunctionComponent { return { majorLabel: geoms.bulletViewModel.labelMajor, minorLabel: geoms.bulletViewModel.labelMinor }; }); + +/** @internal */ +export const getGoalChartSemanticDataSelector = createCustomCachedSelector([geometries], (geoms) => { + return geoms.bulletViewModel.bands ?? []; +}); + +/** @internal */ +export const getFirstTickValueSelector = createCustomCachedSelector([geometries], (geoms) => { + return geoms.bulletViewModel.lowestValue; +}); diff --git a/packages/charts/src/components/accessibility/accessibility.test.tsx b/packages/charts/src/components/accessibility/accessibility.test.tsx index 9491f5bce4..66342ed7aa 100644 --- a/packages/charts/src/components/accessibility/accessibility.test.tsx +++ b/packages/charts/src/components/accessibility/accessibility.test.tsx @@ -154,10 +154,38 @@ describe('Accessibility', () => { /> , ); + + const bandLabelsAscending = ['freezing', 'chilly', 'brisk']; + const bandsAscending = [200, 250, 300]; + + const ascendingBandLabelsGoalChart = mount( + + + , + ); it('should test defaults for goal charts', () => { expect(goalChartWrapper.find('.echScreenReaderOnly').first().text()).toBe( 'Revenue 2020 YTD (thousand USD) Chart type:goal chartMinimum:0Maximum:300Target:$260Value:170', ); }); + it('should correctly render ascending semantic values', () => { + expect(ascendingBandLabelsGoalChart.find('.echGoalDescription').first().text()).toBe( + '0 - 200freezing200 - 250chilly250 - 300brisk', + ); + }); }); }); diff --git a/packages/charts/src/components/accessibility/goal_semantic_description.tsx b/packages/charts/src/components/accessibility/goal_semantic_description.tsx new file mode 100644 index 0000000000..25c73df867 --- /dev/null +++ b/packages/charts/src/components/accessibility/goal_semantic_description.tsx @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { BandViewModel } from '../../chart_types/goal_chart/layout/types/viewmodel_types'; +import { A11ySettings } from '../../state/selectors/get_accessibility_config'; + +interface GoalSemanticDescriptionProps { + bandLabels: BandViewModel[]; + firstValue: number; +} + +/** @internal */ +export const GoalSemanticDescription = ({ + bandLabels, + labelId, + firstValue, +}: A11ySettings & GoalSemanticDescriptionProps) => { + return bandLabels.length > 1 ? ( +
+ {bandLabels.map(({ value, text }, index) => { + const prevValue = bandLabels[index - 1]; + return prevValue !== undefined ? ( + <> +
{`${prevValue.value} - ${value}`}
+
{`${text[index]}`}
+ + ) : ( + <> +
{`${firstValue} - ${value}`}
+
{`${text[index]}`}
+ + ); + })} +
+ ) : null; +}; diff --git a/packages/charts/src/components/accessibility/index.ts b/packages/charts/src/components/accessibility/index.ts index d20347fcdb..4f4b864b70 100644 --- a/packages/charts/src/components/accessibility/index.ts +++ b/packages/charts/src/components/accessibility/index.ts @@ -20,3 +20,4 @@ /* @internal */ export { ScreenReaderSummary } from './screen_reader_summary'; export { ScreenReaderPartitionTable } from './partitions_data_table'; +export { GoalSemanticDescription } from './goal_semantic_description'; diff --git a/stories/goal/25_goal_semantic.tsx b/stories/goal/25_goal_semantic.tsx new file mode 100644 index 0000000000..6aff030dc5 --- /dev/null +++ b/stories/goal/25_goal_semantic.tsx @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { Chart, Goal } from '../../packages/charts/src'; +import { BandFillColorAccessorInput } from '../../packages/charts/src/chart_types/goal_chart/specs'; +import { GoalSubtype } from '../../packages/charts/src/chart_types/goal_chart/specs/constants'; +import { Color } from '../../packages/charts/src/utils/common'; + +const subtype = GoalSubtype.Goal; + +export const Example = () => { + const bandLabels = ['freezing', 'chilly', 'brisk']; + const bands = [200, 250, 300]; + + const opacityMap: { [k: string]: number } = { + '200': 0.2, + '250': 0.12, + '300': 0.05, + }; + + const colorMap: { [k: number]: Color } = bands.reduce<{ [k: number]: Color }>((acc, band) => { + const defaultValue = opacityMap[band]; + acc[band] = `rgba(0, 0, 0, ${defaultValue.toFixed(2)})`; + return acc; + }, {}); + + const bandFillColor = (x: number): Color => colorMap[x]; + + return ( + + String(value)} + bandFillColor={({ value }: BandFillColorAccessorInput) => bandFillColor(value)} + labelMajor="Revenue 2020 YTD " + labelMinor="(thousand USD) " + centralMajor="170" + centralMinor="" + config={{ angleStart: Math.PI, angleEnd: 0 }} + bandLabels={bandLabels} + /> + + ); +}; diff --git a/stories/goal/goal.stories.tsx b/stories/goal/goal.stories.tsx index 7b9e36c06a..1323318361 100644 --- a/stories/goal/goal.stories.tsx +++ b/stories/goal/goal.stories.tsx @@ -51,3 +51,4 @@ export { Example as goalNegative } from './21_goal_negative'; export { Example as horizontalPlusMinus } from './22_horizontal_plusminus'; export { Example as verticalPlusMinus } from './23_vertical_plusminus'; export { Example as goalPlusMinus } from './24_goal_plusminus'; +export { Example as goalSemantics } from './25_goal_semantic';