@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
302 lines • 14.4 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useMemo, useRef } from 'react';
import { nodeContains } from '@awsui/component-toolkit/dom';
import { useStableCallback } from '@awsui/component-toolkit/internal';
import { useHeightMeasure } from '../../internal/hooks/container-queries/use-height-measure';
import { KeyCode } from '../../internal/keycode';
import { circleIndex } from '../../internal/utils/circle-index';
import handleKey from '../../internal/utils/handle-key';
import { nodeBelongs } from '../../internal/utils/node-belongs';
import { throttle } from '../../internal/utils/throttle';
import { useReaction } from '../async-store';
import computeChartProps from './compute-chart-props';
import createSeriesDecorator from './create-series-decorator';
import InteractionsStore from './interactions-store';
import { findClosest } from './utils';
const MAX_HOVER_MARGIN = 6;
const SVG_HOVER_THROTTLE = 25;
const POPOVER_DEADZONE = 12;
// Represents the core the chart logic, including the model of all allowed user interactions.
export default function useChartModel({ isRtl, fitHeight, externalSeries: allSeries, visibleSeries: series, setVisibleSeries, highlightedSeries, setHighlightedSeries, xDomain, yDomain, xScaleType, yScaleType, height: explicitHeight, width, popoverRef, statusType, }) {
var _a;
// Chart elements refs used in handlers.
const plotRef = useRef(null);
const containerRef = useRef(null);
const verticalMarkerRef = useRef(null);
const plotMeasureRef = useRef(null);
const hasVisibleSeries = series.length > 0;
const height = (_a = useHeightMeasure(() => plotMeasureRef.current, !fitHeight, [hasVisibleSeries, statusType])) !== null && _a !== void 0 ? _a : explicitHeight;
const stableSetVisibleSeries = useStableCallback(setVisibleSeries);
const model = useMemo(() => {
// Compute scales, ticks and two-dimensional plots.
const computed = computeChartProps({
isRtl,
series,
xDomain,
yDomain,
xScaleType,
yScaleType,
height,
width,
});
// A store for chart interactions that don't require plot recomputation.
const interactions = new InteractionsStore(series, computed.plot);
const containsMultipleSeries = interactions.series.length > 1;
// A series decorator to provide extra props such as color and marker type.
const getInternalSeries = createSeriesDecorator(allSeries);
const isMouseOverPopover = (clientX, clientY) => {
var _a;
if ((_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.firstChild) {
const popoverPosition = popoverRef.current.firstChild.getBoundingClientRect();
if (clientX > popoverPosition.x - POPOVER_DEADZONE &&
clientX < popoverPosition.x + popoverPosition.width + POPOVER_DEADZONE &&
clientY > popoverPosition.y - POPOVER_DEADZONE &&
clientY < popoverPosition.y + popoverPosition.height + POPOVER_DEADZONE) {
return true;
}
}
return false;
};
// A Callback for svg mouseover to hover the plot points.
// Throttling is necessary for a substantially smoother customer experience.
const onSVGMouseMoveThrottled = throttle((clientX, clientY) => {
// No hover logic when the popover is pinned or no data available.
if (interactions.get().isPopoverPinned ||
!plotRef.current ||
interactions.plot.xy.length === 0 ||
isMouseOverPopover(clientX, clientY)) {
return;
}
const svgRect = plotRef.current.svg.getBoundingClientRect();
const offsetX = clientX - svgRect.left;
const offsetY = clientY - svgRect.top;
const closestX = findClosest(interactions.plot.xy, offsetX, xPoints => xPoints[0].scaled.x);
const closestPoint = findClosest(closestX, offsetY, point => point.scaled.y1);
// If close enough to the point - highlight the point and its column.
// If not - only highlight the closest column.
if (Math.abs(offsetX - closestPoint.scaled.x) < MAX_HOVER_MARGIN &&
Math.abs(offsetY - closestPoint.scaled.y1) < MAX_HOVER_MARGIN) {
interactions.highlightPoint(closestPoint);
}
else {
interactions.highlightX(closestX);
}
}, SVG_HOVER_THROTTLE);
const onSVGMouseMove = ({ clientX, clientY }) => onSVGMouseMoveThrottled(clientX, clientY);
// A callback for svg mouseout to clear all highlights.
const onSVGMouseOut = (event) => {
// Because the mouseover is throttled, in can occur slightly after the mouseout,
// neglecting its effect; cancelling the throttled function prevents that.
onSVGMouseMoveThrottled.cancel();
// No hover logic when the popover is pinned or mouse is over popover
if (interactions.get().isPopoverPinned || isMouseOverPopover(event.clientX, event.clientY)) {
return;
}
// Check if the target is contained within svg to allow hovering on the popover body.
if (!nodeContains(plotRef.current.svg, event.relatedTarget)) {
interactions.clearHighlightedLegend();
interactions.clearHighlight();
}
};
// A callback for svg click to pin/unpin the popover.
const onSVGMouseDown = (event) => {
interactions.togglePopoverPin();
event.preventDefault();
};
const moveWithinXAxis = (direction) => {
if (interactions.get().highlightedPoint) {
return moveWithinSeries(direction);
}
else if (containsMultipleSeries) {
const { highlightedX } = interactions.get();
if (highlightedX) {
const currentXIndex = highlightedX[0].index.x;
const nextXIndex = circleIndex(currentXIndex + direction, [0, interactions.plot.xy.length - 1]);
interactions.highlightX(interactions.plot.xy[nextXIndex]);
}
}
};
// A helper function to highlight the next or previous point within selected series.
const moveWithinSeries = (direction) => {
// Can only use motion when a particular point is highlighted.
const point = interactions.get().highlightedPoint;
if (!point) {
return;
}
// Take the index of the currently highlighted series.
const sIndex = point.index.s;
// Take the incremented(circularly) x-index of the currently highlighted point.
const xIndex = circleIndex(point.index.x + direction, [0, interactions.plot.xs.length - 1]);
// Highlight the next point using x:s grouped data.
interactions.highlightPoint(interactions.plot.xs[xIndex][sIndex]);
};
// A helper function to highlight the next or previous point within the selected column.
const moveBetweenSeries = (direction) => {
const point = interactions.get().highlightedPoint;
if (!point) {
const { highlightedX } = interactions.get();
if (highlightedX) {
const xIndex = highlightedX[0].index.x;
const points = interactions.plot.xy[xIndex];
const yIndex = direction === 1 ? 0 : points.length - 1;
interactions.highlightPoint(points[yIndex]);
}
return;
}
// Take the index of the currently highlighted column.
const xIndex = point.index.x;
const currentYIndex = point.index.y;
if (containsMultipleSeries &&
((currentYIndex === 0 && direction === -1) ||
(currentYIndex === interactions.plot.xy[xIndex].length - 1 && direction === 1))) {
interactions.highlightX(interactions.plot.xy[xIndex]);
}
else {
// Take the incremented(circularly) y-index of the currently highlighted point.
const nextYIndex = circleIndex(currentYIndex + direction, [0, interactions.plot.xy[xIndex].length - 1]);
// Highlight the next point using x:y grouped data.
interactions.highlightPoint(interactions.plot.xy[xIndex][nextYIndex]);
}
};
// A callback for svg keydown to enable motions and popover pin with the keyboard.
const onSVGKeyDown = (event) => {
const keyCode = event.keyCode;
if (keyCode !== KeyCode.up &&
keyCode !== KeyCode.right &&
keyCode !== KeyCode.down &&
keyCode !== KeyCode.left &&
keyCode !== KeyCode.space &&
keyCode !== KeyCode.enter) {
return;
}
// Preventing default fixes an issue in Safari+VO when VO additionally interprets arrow keys as its commands.
event.preventDefault();
// No keydown logic when the popover is pinned.
if (interactions.get().isPopoverPinned) {
return;
}
handleKey(event, {
onBlockEnd: () => moveBetweenSeries(-1),
onBlockStart: () => moveBetweenSeries(1),
onInlineStart: () => moveWithinXAxis(-1),
onInlineEnd: () => moveWithinXAxis(1),
onActivate: () => interactions.pinPopover(),
});
};
const highlightFirstX = () => {
interactions.highlightX(interactions.plot.xy[0]);
};
// A callback for application focus to highlight series.
const onApplicationFocus = (_event, trigger) => {
// When focus is caused by a click event nothing is expected as clicks are handled separately.
if (trigger === 'keyboard') {
const { highlightedX, highlightedPoint, highlightedSeries, legendSeries } = interactions.get();
if (containsMultipleSeries && !highlightedX && !highlightedPoint && !highlightedSeries && !legendSeries) {
highlightFirstX();
}
else if (!highlightedX) {
interactions.highlightFirstPoint();
}
}
};
// A callback for application blur to clear all highlights unless the popover is pinned.
const onApplicationBlur = (event) => {
// Pinned popover stays pinned even if the focus is lost.
// If blur is not caused by the popover, forget the previously highlighted point.
if (!nodeBelongs(containerRef.current, event.relatedTarget) && !interactions.get().isPopoverPinned) {
interactions.clearHighlight();
}
};
const onFilterSeries = (series) => {
stableSetVisibleSeries(series);
};
const onLegendHighlight = (series) => {
interactions.highlightSeries(series);
};
const onPopoverDismiss = (outsideClick) => {
interactions.unpinPopover();
// Return focus back to the application or plot (when no point is highlighted).
if (!outsideClick) {
// The delay is needed to bypass focus events caused by click or keypress needed to unpin the popover.
setTimeout(() => {
if (interactions.get().highlightedPoint || interactions.get().highlightedX) {
plotRef.current.focusApplication();
}
else {
interactions.clearHighlight();
plotRef.current.focusPlot();
}
}, 0);
}
};
const onContainerBlur = () => {
interactions.clearState();
};
const onDocumentKeyDown = (event) => {
if (event.key === 'Escape') {
interactions.clearHighlight();
interactions.clearHighlightedLegend();
}
};
const onPopoverLeave = (event) => {
if (nodeContains(plotRef.current.svg, event.relatedTarget) || interactions.get().isPopoverPinned) {
return;
}
interactions.clearHighlight();
interactions.clearHighlightedLegend();
};
return {
width,
height,
series,
allSeries,
getInternalSeries,
computed,
interactions,
handlers: {
onSVGMouseMove,
onSVGMouseOut,
onSVGMouseDown,
onSVGKeyDown,
onApplicationFocus,
onApplicationBlur,
onFilterSeries,
onLegendHighlight,
onPopoverDismiss,
onContainerBlur,
onDocumentKeyDown,
onPopoverLeave,
},
refs: {
plot: plotRef,
plotMeasure: plotMeasureRef,
container: containerRef,
verticalMarker: verticalMarkerRef,
popoverRef,
},
};
}, [
allSeries,
series,
xDomain,
yDomain,
xScaleType,
yScaleType,
height,
width,
stableSetVisibleSeries,
popoverRef,
isRtl,
]);
// Notify client when series highlight change.
useReaction(model.interactions, state => state.highlightedSeries, setHighlightedSeries);
// Update interactions store when series highlight in a controlled way.
useEffect(() => {
if (highlightedSeries !== model.interactions.get().highlightedSeries) {
model.interactions.highlightSeries(highlightedSeries);
}
}, [model, highlightedSeries]);
return model;
}
//# sourceMappingURL=use-chart-model.js.map