@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
351 lines • 21.3 kB
JavaScript
import { __rest } from "tslib";
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { getIsRtl } from '@awsui/component-toolkit/internal';
import AxisLabel from '../internal/components/cartesian-chart/axis-label';
import BlockEndLabels, { useBLockEndLabels } from '../internal/components/cartesian-chart/block-end-labels';
import { CartesianChartContainer } from '../internal/components/cartesian-chart/chart-container';
import EmphasizedBaseline from '../internal/components/cartesian-chart/emphasized-baseline';
import HighlightedPoint from '../internal/components/cartesian-chart/highlighted-point';
import InlineStartLabels from '../internal/components/cartesian-chart/inline-start-labels';
import LabelsMeasure from '../internal/components/cartesian-chart/labels-measure';
import { ChartScale, NumericChartScale } from '../internal/components/cartesian-chart/scales';
import { createXTicks, createYTicks, getXTickCount, getYTickCount } from '../internal/components/cartesian-chart/ticks';
import VerticalGridLines from '../internal/components/cartesian-chart/vertical-grid-lines';
import VerticalMarker from '../internal/components/cartesian-chart/vertical-marker';
import ChartPlot from '../internal/components/chart-plot';
import { useHeightMeasure } from '../internal/hooks/container-queries/use-height-measure';
import { useMergeRefs } from '../internal/hooks/use-merge-refs';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { nodeBelongs } from '../internal/utils/node-belongs';
import useContainerWidth from '../internal/utils/use-container-width';
import BarGroups from './bar-groups';
import MixedChartPopover from './chart-popover';
import DataSeries from './data-series';
import { computeDomainX, computeDomainY } from './domain';
import formatHighlighted from './format-highlighted';
import { useMouseHover } from './hooks/use-mouse-hover';
import { useNavigation } from './hooks/use-navigation';
import { usePopover } from './hooks/use-popover';
import makeScaledBarGroups from './make-scaled-bar-groups';
import makeScaledSeries from './make-scaled-series';
import { isXThreshold } from './utils';
const INLINE_START_LABELS_MARGIN = 16;
const BLOCK_END_LABELS_OFFSET = 12;
const fallbackContainerWidth = 500;
export default function ChartContainer(_a) {
var _b, _c;
var { fitHeight, hasFilters, height: explicitPlotHeight, series, visibleSeries, highlightedSeries, onHighlightChange, highlightedPoint, setHighlightedPoint, highlightedGroupIndex, setHighlightedGroupIndex, detailPopoverFooter, detailPopoverSize = 'medium', stackedBars = false, horizontalBars = false, xScaleType, yScaleType, xTickFormatter, yTickFormatter, emphasizeBaselineAxis, xTitle, yTitle, ariaLabel, ariaLabelledby, ariaDescription, i18nStrings = {}, detailPopoverSeriesContent } = _a, props = __rest(_a, ["fitHeight", "hasFilters", "height", "series", "visibleSeries", "highlightedSeries", "onHighlightChange", "highlightedPoint", "setHighlightedPoint", "highlightedGroupIndex", "setHighlightedGroupIndex", "detailPopoverFooter", "detailPopoverSize", "stackedBars", "horizontalBars", "xScaleType", "yScaleType", "xTickFormatter", "yTickFormatter", "emphasizeBaselineAxis", "xTitle", "yTitle", "ariaLabel", "ariaLabelledby", "ariaDescription", "i18nStrings", "detailPopoverSeriesContent"]);
const plotRef = useRef(null);
const verticalMarkerRef = useRef(null);
const [inlineStartLabelsWidth, setInlineStartLabelsWidth] = useState(0);
const [verticalMarkerX, setVerticalMarkerX] = useState(null);
const [detailsPopoverText, setDetailsPopoverText] = useState('');
const [containerWidth, containerMeasureRef] = useContainerWidth(fallbackContainerWidth);
const maxInlineStartLabelsWidth = Math.round(containerWidth / 2);
const plotWidth = containerWidth
? // Calculate the minimum between inlineStartLabelsWidth and maxInlineStartLabelsWidth for extra safety because inlineStarteLabelsWidth could be out of date
Math.max(0, containerWidth - Math.min(inlineStartLabelsWidth, maxInlineStartLabelsWidth) - INLINE_START_LABELS_MARGIN)
: fallbackContainerWidth;
const containerRefObject = useRef(null);
const containerRef = useMergeRefs(containerMeasureRef, containerRefObject);
const popoverRef = useRef(null);
const isRtl = getIsRtl(containerRefObject.current);
const xDomain = (props.xDomain || computeDomainX(series, xScaleType));
const yDomain = (props.yDomain || computeDomainY(series, yScaleType, stackedBars));
const linesOnly = series.every(({ series }) => series.type === 'line' || series.type === 'threshold');
function getXAxisProps(size, range) {
const tickCount = getXTickCount(size);
const scale = new ChartScale(xScaleType, xDomain, range, linesOnly);
const ticks = createXTicks(scale, tickCount);
return {
axis: 'x',
tickCount,
scale,
ticks,
tickFormatter: xTickFormatter,
title: xTitle,
ariaRoleDescription: i18nStrings.xAxisAriaRoleDescription,
};
}
function getYAxisProps(size, range) {
const tickCount = getYTickCount(size);
const scale = new NumericChartScale(yScaleType, yDomain, range, props.yDomain ? null : tickCount);
const ticks = createYTicks(scale, tickCount);
return {
axis: 'y',
tickCount,
scale,
ticks,
tickFormatter: yTickFormatter,
title: yTitle,
ariaRoleDescription: i18nStrings.yAxisAriaRoleDescription,
};
}
const bottomAxisProps = !horizontalBars
? getXAxisProps(plotWidth, !isRtl ? [0, plotWidth] : [plotWidth, 0])
: getYAxisProps(plotWidth, !isRtl ? [0, plotWidth] : [plotWidth, 0]);
const blockEndLabelsProps = useBLockEndLabels(Object.assign({}, bottomAxisProps));
const plotMeasureRef = useRef(null);
const measuredHeight = useHeightMeasure(() => plotMeasureRef.current, !fitHeight);
const plotHeight = fitHeight ? measuredHeight !== null && measuredHeight !== void 0 ? measuredHeight : 0 : explicitPlotHeight;
const leftAxisProps = !horizontalBars
? getYAxisProps(plotHeight, [plotHeight, 0])
: getXAxisProps(plotHeight, [0, plotHeight]);
const xAxisProps = bottomAxisProps.axis === 'x' ? bottomAxisProps : leftAxisProps.axis === 'x' ? leftAxisProps : null;
const yAxisProps = bottomAxisProps.axis === 'y' ? bottomAxisProps : leftAxisProps.axis === 'y' ? leftAxisProps : null;
if (!xAxisProps || !yAxisProps) {
throw new Error('Invariant violation: invalid axis props.');
}
/**
* Interactions
*/
const highlightedPointRef = useRef(null);
const highlightedGroupRef = useRef(null);
// Some chart components are rendered against "x" or "y" axes,
// When "horizontalBars" is enabled, the axes are inverted.
const x = !horizontalBars ? 'x' : 'y';
const y = !horizontalBars ? 'y' : 'x';
const scaledSeries = makeScaledSeries(visibleSeries, xAxisProps.scale, yAxisProps.scale);
const barGroups = makeScaledBarGroups(visibleSeries, xAxisProps.scale, plotWidth, plotHeight, y);
const { isPopoverOpen, isPopoverPinned, showPopover, pinPopover, dismissPopover } = usePopover();
// Allows to add a delay between popover is dismissed and handlers are enabled to prevent immediate popover reopening.
const [isHandlersDisabled, setHandlersDisabled] = useState(false);
useEffect(() => {
if (isPopoverPinned) {
setHandlersDisabled(true);
}
else {
const timeoutId = setTimeout(() => setHandlersDisabled(false), 25);
return () => clearTimeout(timeoutId);
}
}, [isPopoverPinned]);
const highlightSeries = useCallback((series) => {
if (series !== highlightedSeries) {
onHighlightChange(series);
}
}, [highlightedSeries, onHighlightChange]);
const highlightPoint = useCallback((point) => {
var _a, _b;
setHighlightedGroupIndex(null);
setHighlightedPoint(point);
if (point) {
highlightSeries(point.series);
setVerticalMarkerX({
scaledX: point.x,
label: (_b = (_a = point.datum) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : null,
});
}
}, [setHighlightedGroupIndex, setHighlightedPoint, highlightSeries]);
const clearAllHighlights = useCallback(() => {
setHighlightedPoint(null);
highlightSeries(null);
setHighlightedGroupIndex(null);
}, [highlightSeries, setHighlightedGroupIndex, setHighlightedPoint]);
// Highlight all points at a given X in a line chart
const highlightX = useCallback((marker) => {
if (marker) {
clearAllHighlights();
}
setVerticalMarkerX(marker);
}, [clearAllHighlights]);
// Highlight all points and bars at a given X index in a mixed line and bar chart
const highlightGroup = useCallback((groupIndex) => {
highlightSeries(null);
setHighlightedPoint(null);
setHighlightedGroupIndex(groupIndex);
}, [highlightSeries, setHighlightedPoint, setHighlightedGroupIndex]);
const clearHighlightedSeries = useCallback(() => {
clearAllHighlights();
dismissPopover();
}, [dismissPopover, clearAllHighlights]);
const _d = useNavigation({
series,
visibleSeries,
scaledSeries,
barGroups,
xScale: xAxisProps.scale,
yScale: yAxisProps.scale,
highlightedPoint,
highlightedGroupIndex,
highlightedSeries,
isHandlersDisabled,
pinPopover,
highlightSeries,
highlightGroup,
highlightPoint,
highlightX,
clearHighlightedSeries,
verticalMarkerX,
isRtl: !!isRtl,
horizontalBars,
}), { isGroupNavigation } = _d, handlers = __rest(_d, ["isGroupNavigation"]);
const { onSVGMouseMove, onSVGMouseOut, onPopoverLeave } = useMouseHover({
scaledSeries,
barGroups,
plotRef,
popoverRef,
highlightPoint,
highlightGroup,
clearHighlightedSeries,
isGroupNavigation,
isHandlersDisabled,
highlightX,
});
// There are multiple ways to indicate what X is selected.
// TODO: make a uniform verticalMarkerX state to fit all use-cases.
const highlightedX = useMemo(() => {
var _a, _b, _c;
if (highlightedGroupIndex !== null) {
return (_a = barGroups[highlightedGroupIndex]) === null || _a === void 0 ? void 0 : _a.x;
}
if (verticalMarkerX !== null) {
return verticalMarkerX.label;
}
return (_c = (_b = highlightedPoint === null || highlightedPoint === void 0 ? void 0 : highlightedPoint.datum) === null || _b === void 0 ? void 0 : _b.x) !== null && _c !== void 0 ? _c : null;
}, [highlightedPoint, verticalMarkerX, highlightedGroupIndex, barGroups]);
useEffect(() => {
const onKeyDown = (event) => {
if (event.key === 'Escape') {
dismissPopover();
}
};
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [dismissPopover]);
useLayoutEffect(() => {
if (highlightedX !== null || highlightedPoint !== null) {
showPopover();
}
}, [highlightedX, highlightedPoint, showPopover]);
const onPopoverDismiss = (outsideClick) => {
dismissPopover();
if (!outsideClick) {
// The delay is needed to bypass focus events caused by click or keypress needed to unpin the popover.
setTimeout(() => {
var _a, _b;
const isSomeElementHighlighted = !!(highlightedPoint || highlightedGroupIndex !== null || verticalMarkerX);
if (isSomeElementHighlighted) {
(_a = plotRef.current) === null || _a === void 0 ? void 0 : _a.focusApplication();
}
else {
(_b = plotRef.current) === null || _b === void 0 ? void 0 : _b.focusPlot();
}
}, 0);
}
else {
clearAllHighlights();
setVerticalMarkerX(null);
}
};
const onSVGClick = (e) => {
if (isPopoverOpen) {
if (isPopoverPinned) {
dismissPopover();
}
else {
pinPopover();
e.preventDefault();
}
}
else {
showPopover();
}
};
const onApplicationFocus = (event, trigger) => {
if (trigger === 'keyboard') {
handlers.onFocus();
}
else {
// noop: clicks are handled separately
}
};
const onApplicationBlur = (event) => {
const blurTarget = event.relatedTarget || event.target;
if (blurTarget === null ||
!(blurTarget instanceof Element) ||
!nodeBelongs(containerRefObject.current, blurTarget)) {
clearHighlightedSeries();
setVerticalMarkerX(null);
if (isPopoverOpen && !isPopoverPinned) {
dismissPopover();
}
}
};
const onSVGKeyDown = handlers.onKeyDown;
const xOffset = xAxisProps.scale.isCategorical() ? Math.max(0, xAxisProps.scale.d3Scale.bandwidth() - 1) / 2 : 0;
let verticalLineX = null;
if (verticalMarkerX !== null) {
verticalLineX = verticalMarkerX.scaledX;
}
else if (isGroupNavigation && highlightedGroupIndex !== null) {
const x = (_c = xAxisProps.scale.d3Scale((_b = barGroups[highlightedGroupIndex]) === null || _b === void 0 ? void 0 : _b.x)) !== null && _c !== void 0 ? _c : null;
if (x !== null) {
verticalLineX = xOffset + x;
}
}
const point = useMemo(() => highlightedPoint
? {
key: `${highlightedPoint.x}-${highlightedPoint.y}`,
x: highlightedPoint.x,
y: highlightedPoint.y,
color: highlightedPoint.color,
}
: null, [highlightedPoint]);
const verticalMarkers = useMemo(() => verticalLineX !== null
? scaledSeries
.filter(({ x, y }) => (x === verticalLineX || isNaN(x)) && !isNaN(y))
.map(({ x, y, color }, index) => ({
key: `${index}-${x}-${y}`,
x: !horizontalBars ? verticalLineX || 0 : y,
y: !horizontalBars ? y : verticalLineX || 0,
color: color,
}))
: [], [scaledSeries, verticalLineX, horizontalBars]);
const highlightedElementRef = isGroupNavigation
? highlightedGroupRef
: highlightedPoint
? highlightedPointRef
: verticalMarkerRef;
const highlightDetails = useMemo(() => {
if (highlightedX === null) {
return null;
}
// When series point is highlighted show the corresponding series and matching x-thresholds.
if (highlightedPoint) {
const seriesToShow = visibleSeries.filter(series => series.series === (highlightedPoint === null || highlightedPoint === void 0 ? void 0 : highlightedPoint.series) || isXThreshold(series.series));
return formatHighlighted({
position: highlightedX,
series: seriesToShow,
xTickFormatter,
detailPopoverSeriesContent,
});
}
// Otherwise - show all visible series details.
return formatHighlighted({
position: highlightedX,
series: visibleSeries,
xTickFormatter,
detailPopoverSeriesContent,
});
}, [highlightedX, highlightedPoint, visibleSeries, xTickFormatter, detailPopoverSeriesContent]);
const detailPopoverFooterContent = useMemo(() => (detailPopoverFooter && highlightedX ? detailPopoverFooter(highlightedX) : null), [detailPopoverFooter, highlightedX]);
const activeAriaLabel = highlightDetails && detailsPopoverText ? `${highlightDetails.position}, ${detailsPopoverText}` : '';
// Live region is used when nothing is focused e.g. when hovering.
const activeLiveRegion = activeAriaLabel && !highlightedPoint && highlightedGroupIndex === null ? activeAriaLabel : '';
const isLineXKeyboardFocused = !highlightedPoint && verticalMarkerX;
const isRefresh = useVisualRefresh();
return (React.createElement(CartesianChartContainer, { ref: containerRef, minHeight: explicitPlotHeight + blockEndLabelsProps.height, fitHeight: !!fitHeight, hasFilters: hasFilters, leftAxisLabel: React.createElement(AxisLabel, { axis: y, position: "left", title: leftAxisProps.title }), leftAxisLabelMeasure: React.createElement(LabelsMeasure, { ticks: leftAxisProps.ticks, scale: leftAxisProps.scale, tickFormatter: leftAxisProps.tickFormatter, autoWidth: setInlineStartLabelsWidth, maxLabelsWidth: maxInlineStartLabelsWidth }), bottomAxisLabel: React.createElement(AxisLabel, { axis: x, position: "bottom", title: bottomAxisProps.title }), chartPlot: React.createElement(ChartPlot, { ref: plotRef, width: "100%", height: fitHeight ? `calc(100% - ${blockEndLabelsProps.height}px)` : plotHeight, offsetBottom: blockEndLabelsProps.height, isClickable: isPopoverOpen && !isPopoverPinned, ariaLabel: ariaLabel, ariaLabelledby: ariaLabelledby, ariaDescription: ariaDescription, ariaRoleDescription: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.chartAriaRoleDescription, ariaLiveRegion: activeLiveRegion, activeElementRef: highlightedElementRef, activeElementKey: activeAriaLabel, activeElementFocusOffset: isGroupNavigation ? 0 : isLineXKeyboardFocused ? { x: 8, y: 0 } : 3, onMouseMove: onSVGMouseMove, onMouseOut: onSVGMouseOut, onClick: onSVGClick, onApplicationFocus: onApplicationFocus, onApplicationBlur: onApplicationBlur, onKeyDown: onSVGKeyDown },
React.createElement("line", { ref: plotMeasureRef, x1: "0", x2: "0", y1: "0", y2: "100%", stroke: "transparent", strokeWidth: 1, style: { pointerEvents: 'none' } }),
React.createElement(InlineStartLabels, { axis: y, ticks: leftAxisProps.ticks, scale: leftAxisProps.scale, tickFormatter: leftAxisProps.tickFormatter, title: leftAxisProps.title, ariaRoleDescription: leftAxisProps.ariaRoleDescription, maxLabelsWidth: maxInlineStartLabelsWidth, plotWidth: plotWidth, plotHeight: plotHeight }),
horizontalBars && (React.createElement(VerticalGridLines, { scale: yAxisProps.scale, ticks: yAxisProps.ticks, height: plotHeight })),
emphasizeBaselineAxis && linesOnly && (React.createElement(EmphasizedBaseline, { axis: x, scale: yAxisProps.scale, width: plotWidth, height: plotHeight })),
React.createElement(DataSeries, { axis: x, plotWidth: plotWidth, plotHeight: plotHeight, highlightedSeries: highlightedSeries !== null && highlightedSeries !== void 0 ? highlightedSeries : null, highlightedGroupIndex: highlightedGroupIndex, stackedBars: stackedBars, isGroupNavigation: isGroupNavigation, visibleSeries: visibleSeries, xScale: xAxisProps.scale, yScale: yAxisProps.scale, isRtl: !!isRtl }),
emphasizeBaselineAxis && !linesOnly && (React.createElement(EmphasizedBaseline, { axis: x, scale: yAxisProps.scale, width: plotWidth, height: plotHeight })),
React.createElement(VerticalMarker, { key: verticalLineX || '', height: plotHeight, showPoints: highlightedPoint === null, showLine: !isGroupNavigation, points: verticalMarkers, ref: verticalMarkerRef }),
highlightedPoint && (React.createElement(HighlightedPoint, { ref: highlightedPointRef, point: point, role: "button", ariaLabel: activeAriaLabel, ariaHasPopup: true, ariaExpanded: isPopoverPinned })),
isGroupNavigation && xAxisProps.scale.isCategorical() && (React.createElement(BarGroups, { ariaLabel: activeAriaLabel, isRefresh: isRefresh, isPopoverPinned: isPopoverPinned, barGroups: barGroups, highlightedGroupIndex: highlightedGroupIndex, highlightedGroupRef: highlightedGroupRef })),
React.createElement(BlockEndLabels, Object.assign({}, blockEndLabelsProps, { axis: x, scale: bottomAxisProps.scale, title: bottomAxisProps.title, ariaRoleDescription: bottomAxisProps.ariaRoleDescription, height: plotHeight, width: plotWidth, offsetLeft: inlineStartLabelsWidth + BLOCK_END_LABELS_OFFSET, offsetRight: BLOCK_END_LABELS_OFFSET, isRTL: isRtl }))), popover: React.createElement(MixedChartPopover, { ref: popoverRef, containerRef: containerRefObject, trackRef: highlightedElementRef, isOpen: isPopoverOpen, isPinned: isPopoverPinned, highlightDetails: highlightDetails, onDismiss: onPopoverDismiss, size: detailPopoverSize, footer: detailPopoverFooterContent, dismissAriaLabel: i18nStrings.detailPopoverDismissAriaLabel, onMouseLeave: onPopoverLeave, onBlur: onApplicationBlur, setPopoverText: setDetailsPopoverText }) }));
}
//# sourceMappingURL=chart-container.js.map