Skip to content

Commit

Permalink
feat(a11y): allow user to add optional semantic meaning to goal/gauge…
Browse files Browse the repository at this point in the history
… charts (elastic#1218)

Closes elastic#1161
  • Loading branch information
rshen91 authored Jul 8, 2021
1 parent f8a7111 commit 87629e2
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 5 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@ export interface GoalSpec extends Spec {
// (undocumented)
bandFillColor: BandFillColorAccessor;
// (undocumented)
bandLabels: string[];
// (undocumented)
bands: number[];
// (undocumented)
base: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -90,6 +92,7 @@ export const defaultGoalSpec = {
labelMinor: ({}: BandFillColorAccessorInput) => 'unit',
centralMajor: ({ base }: BandFillColorAccessorInput) => String(base),
centralMinor: ({ target }: BandFillColorAccessorInput) => String(target),
bandLabels: [],
};

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }])),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -31,15 +31,18 @@ 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 {
initialized: boolean;
geometries: ShapeViewModel;
chartContainerDimensions: Dimensions;
a11ySettings: A11ySettings;
bandLabels: BandViewModel[];
firstValue: number;
}

interface ReactiveChartDispatchProps {
Expand Down Expand Up @@ -112,11 +115,12 @@ class Component extends React.Component<Props> {
chartContainerDimensions: { width, height },
forwardStageRef,
a11ySettings,
bandLabels,
firstValue,
} = this.props;
if (!initialized || width === 0 || height === 0) {
return null;
}

return (
<figure aria-labelledby={a11ySettings.labelId} aria-describedby={a11ySettings.descriptionId}>
<canvas
Expand All @@ -133,6 +137,7 @@ class Component extends React.Component<Props> {
role="presentation"
>
<ScreenReaderSummary />
<GoalSemanticDescription bandLabels={bandLabels} firstValue={firstValue} {...a11ySettings} />
</canvas>
</figure>
);
Expand Down Expand Up @@ -172,6 +177,8 @@ const DEFAULT_PROPS: ReactiveChartStateProps = {
top: 0,
},
a11ySettings: DEFAULT_A11Y_SETTINGS,
bandLabels: [],
firstValue: 0,
};

const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
Expand All @@ -183,6 +190,8 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => {
geometries: geometries(state),
chartContainerDimensions: state.parentDimensions,
a11ySettings: getA11ySettingsSelector(state),
bandLabels: getGoalChartSemanticDataSelector(state),
firstValue: getFirstTickValueSelector(state),
};
};

Expand Down
2 changes: 2 additions & 0 deletions packages/charts/src/chart_types/goal_chart/specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface GoalSpec extends Spec {
centralMajor: string | BandFillColorAccessor;
centralMinor: string | BandFillColorAccessor;
config: RecursivePartial<Config>;
bandLabels: string[];
}

type SpecRequiredProps = Pick<GoalSpec, 'id' | 'actual'>;
Expand All @@ -83,6 +84,7 @@ export const Goal: React.FunctionComponent<SpecRequiredProps & SpecOptionalProps
| 'target'
| 'actual'
| 'bands'
| 'bandLabels'
| 'ticks'
| 'bandFillColor'
| 'tickValueFormatter'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@ export const getGoalChartDataSelector = createCustomCachedSelector(
export const getGoalChartLabelsSelector = createCustomCachedSelector([geometries], (geoms) => {
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;
});
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,38 @@ describe('Accessibility', () => {
/>
</Chart>,
);

const bandLabelsAscending = ['freezing', 'chilly', 'brisk'];
const bandsAscending = [200, 250, 300];

const ascendingBandLabelsGoalChart = mount(
<Chart className="story-chart">
<Goal
id="spec_1"
subtype={GoalSubtype.Goal}
base={0}
target={260}
actual={170}
bands={bandsAscending}
ticks={[0, 50, 100, 150, 200, 250, 300]}
labelMajor="Revenue 2020 YTD "
labelMinor="(thousand USD) "
centralMajor="170"
centralMinor=""
config={{ angleStart: Math.PI, angleEnd: 0 }}
bandLabels={bandLabelsAscending}
/>
</Chart>,
);
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',
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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 ? (
<dl className="echScreenReaderOnly echGoalDescription" key={`goalChart--${labelId}`}>
{bandLabels.map(({ value, text }, index) => {
const prevValue = bandLabels[index - 1];
return prevValue !== undefined ? (
<>
<dt key={`dt--${index}`}>{`${prevValue.value} - ${value}`}</dt>
<dd key={`dd--${index}`}>{`${text[index]}`}</dd>
</>
) : (
<>
<dt key={`dt--${index}`}>{`${firstValue} - ${value}`}</dt>
<dd key={`dd--${index}`}>{`${text[index]}`}</dd>
</>
);
})}
</dl>
) : null;
};
1 change: 1 addition & 0 deletions packages/charts/src/components/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
/* @internal */
export { ScreenReaderSummary } from './screen_reader_summary';
export { ScreenReaderPartitionTable } from './partitions_data_table';
export { GoalSemanticDescription } from './goal_semantic_description';
69 changes: 69 additions & 0 deletions stories/goal/25_goal_semantic.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Chart className="story-chart">
<Goal
id="spec_1"
subtype={subtype}
base={0}
target={260}
actual={170}
// doesn't mess with canvas_renderers.ts
bands={bands}
ticks={[0, 50, 100, 150, 200, 250, 300]}
tickValueFormatter={({ value }: BandFillColorAccessorInput) => 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}
/>
</Chart>
);
};
1 change: 1 addition & 0 deletions stories/goal/goal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

0 comments on commit 87629e2

Please sign in to comment.