@actinc/dls
Version:
Design Language System (DLS) for ACT & Encoura front-end projects.
235 lines (234 loc) • 16.9 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
/**
* Copyright (c) ACT, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Button } from '@mui/material';
import { common } from '@mui/material/colors';
import { useTheme, useThemeProps } from '@mui/material/styles';
import debounce from 'lodash/debounce';
import numeral from 'numeral';
import React from 'react';
import { CartesianGrid, Label, LabelList, ReferenceLine, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis, ZAxis, } from 'recharts';
import MagnifyMinusOutline from "../../icons/MagnifyMinusOutline";
import DLS_COMPONENT_NAMES from "../../constants/DLS_COMPONENT_NAMES";
import CustomizedCell from './CustomizedCell';
import CustomizedLabel from './CustomizedLabel';
import CustomTooltip from './CustomTooltip';
import { buildDataPoints, calculateDomainAfterZoom, consolidateDomain, dragDomain, evaluateLabels, filterDataByDomain, fixDecimal, getDomain, getMinMax, } from './processing';
import RankSummary from './RankSummary';
var OPACITY_NOT_HIGHLIGHTED = 0.2;
var DOUBLE_CLICK_ZOOM_AMOUNT = 0.4;
var WHEEL_ZOOM_AMOUNT = 0.15;
export var ScatterPlot = function (inProps) {
var _a = useThemeProps({ name: DLS_COMPONENT_NAMES.SCATTER_PLOT, props: inProps }), averageLineXLabelUnit = _a.averageLineXLabelUnit, cartesianGridProps = _a.cartesianGridProps, chartProps = _a.chartProps, children = _a.children, color = _a.color, CustomTooltipContent = _a.CustomTooltipContent, data = _a.data, height = _a.height, hideSummary = _a.hideSummary, idSubstring = _a.idSubstring, responsiveContainerProps = _a.responsiveContainerProps, scatterLabelColor = _a.scatterLabelColor, scatterProps = _a.scatterProps, _b = _a.showAverageLine, showAverageLine = _b === void 0 ? true : _b, tooltipProps = _a.tooltipProps, xAverageLineLabelProps = _a.xAverageLineLabelProps, xAverageLineProps = _a.xAverageLineProps, xAxisProps = _a.xAxisProps, xLabelProps = _a.xLabelProps, xLabelValue = _a.xLabelValue, yAverageLineLabelProps = _a.yAverageLineLabelProps, yAverageLineProps = _a.yAverageLineProps, yAxisProps = _a.yAxisProps, yLabelProps = _a.yLabelProps, yLabelValue = _a.yLabelValue, zAxisProps = _a.zAxisProps, _c = _a.zoomOptions, zoomOptions = _c === void 0 ? {} : _c;
var _d = useTheme(), palette = _d.palette, spacing = _d.spacing, typography = _d.typography;
var _e = React.useState(data), filteredData = _e[0], setFilteredData = _e[1];
var _f = React.useState(false), isMouseOverScatter = _f[0], setIsMouseOverScatter = _f[1];
var _g = React.useState(), selectedPoint = _g[0], setSelectedPoint = _g[1];
var _h = React.useState(), xLineCoordinates = _h[0], setXLineCoordinates = _h[1];
var _j = React.useState(), yLineCoordinates = _j[0], setYLineCoordinates = _j[1];
var _k = React.useState(undefined), dragAnchor = _k[0], setDragAnchor = _k[1];
var _l = React.useState(false), isDragging = _l[0], setIsDragging = _l[1];
var _m = React.useState(false), showSummary = _m[0], setShowSummary = _m[1];
var _o = React.useState({
x: (xAxisProps === null || xAxisProps === void 0 ? void 0 : xAxisProps.domain) ? consolidateDomain(xAxisProps.domain, data) : [0, 0],
y: (yAxisProps === null || yAxisProps === void 0 ? void 0 : yAxisProps.domain)
? consolidateDomain(yAxisProps.domain, data, true)
: [0, 0],
}), domain = _o[0], setDomain = _o[1];
var _p = React.useState(undefined), initialDomain = _p[0], setInitialDomain = _p[1];
var _q = React.useState(undefined), plotDimensions = _q[0], setPlotDimensions = _q[1];
var _r = React.useState(false), isZoomBlockingHover = _r[0], setIsZoomBlockingHover = _r[1];
var wrapperRef = React.useRef(null);
var pxConversionDimensions = React.useMemo(function () { return ({
x: (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.width)
? (domain.x[1] - domain.x[0]) / plotDimensions.width
: 1,
y: (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.height)
? (domain.y[1] - domain.y[0]) / plotDimensions.height
: 1,
}); }, [plotDimensions, domain]);
var debouncedUnblockHovers = React.useCallback(debounce(function () {
setIsZoomBlockingHover(false);
}, 600), []);
var wheelHandler = function (e) {
e.stopPropagation();
e.preventDefault();
var rect = wrapperRef.current.getBoundingClientRect();
var isZoomIn = e.deltaY < 0;
setIsZoomBlockingHover(true);
debouncedUnblockHovers();
var zoomAmount = zoomOptions.wheelZoom || WHEEL_ZOOM_AMOUNT;
var newDomain = calculateDomainAfterZoom(domain, isZoomIn ? zoomAmount : -zoomAmount, isZoomIn ? domain : initialDomain, e.clientX - rect.left, e.clientY - rect.top, pxConversionDimensions, (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.height) || 0, (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.width) || 0, plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.offset);
var dataPointsInRange = filterDataByDomain(isZoomIn ? filteredData : data, newDomain);
setFilteredData(dataPointsInRange);
setDomain(newDomain);
};
var isBlockingOnHovers = isZoomBlockingHover || isDragging;
/*
If we use React's native onWheel prop to attach the function to the div, we can't use stopPropagation.
React attaches the onWheel listener as "passive" by default, which then ignores the request to stop bubbling.
So wheelHandler has to be attached to the div manually, by an event listener.
However, wheelHandler's values change as the state changes.
We could create a ref for each of the state values we're tracking, or (what we did here), reattach a listener on each
value change.
*/
React.useEffect(function () {
if (wrapperRef.current) {
wrapperRef.current.addEventListener('wheel', wheelHandler);
}
return function () {
if (wrapperRef.current) {
wrapperRef.current.removeEventListener('wheel', wheelHandler);
}
};
}, [
wrapperRef.current,
domain,
plotDimensions,
pxConversionDimensions,
data,
filteredData,
]);
var measuredRef = function (node) {
var _a, _b;
if (((_b = (_a = node === null || node === void 0 ? void 0 : node.state) === null || _a === void 0 ? void 0 : _a.offset) === null || _b === void 0 ? void 0 : _b.height) && node.state.offset.width) {
var nodeHeight = node.state.offset.height;
var nodeWidth = node.state.offset.width;
var left = node.state.offset.left || 0;
var right = node.state.offset.right || 0;
var top_1 = node.state.offset.top || 0;
var bottom = node.state.offset.bottom || 0;
if (nodeHeight !== (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.height) ||
nodeWidth !== (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.width)) {
setPlotDimensions({
height: nodeHeight,
offset: {
bottom: bottom,
left: left,
right: right,
top: top_1,
},
width: nodeWidth,
});
}
}
};
var dataMaxSpread = React.useMemo(function () {
if (!(data === null || data === void 0 ? void 0 : data.length)) {
return [0, 0];
}
var _a = getMinMax(data), xMin = _a.xMin, xMax = _a.xMax, yMin = _a.yMin, yMax = _a.yMax;
var xSpread = xMax - xMin;
var ySpread = yMax - yMin;
return [xSpread, ySpread];
}, [data]);
function resetDomain(resetInitial) {
if (!initialDomain || resetInitial) {
var calcDomain = getDomain(data, dataMaxSpread);
var newInitialDomain = {
x: (xAxisProps === null || xAxisProps === void 0 ? void 0 : xAxisProps.domain)
? consolidateDomain(xAxisProps.domain, data)
: calcDomain.x,
y: (yAxisProps === null || yAxisProps === void 0 ? void 0 : yAxisProps.domain)
? consolidateDomain(yAxisProps.domain, data, true)
: calcDomain.y,
};
setInitialDomain(newInitialDomain);
setDomain(newInitialDomain);
}
else {
setDomain(initialDomain);
}
}
function compareDomainToInitial() {
return (domain.x[0] !== (initialDomain === null || initialDomain === void 0 ? void 0 : initialDomain.x[0]) ||
domain.x[1] !== (initialDomain === null || initialDomain === void 0 ? void 0 : initialDomain.x[1]) ||
domain.y[0] !== (initialDomain === null || initialDomain === void 0 ? void 0 : initialDomain.y[0]) ||
domain.y[1] !== (initialDomain === null || initialDomain === void 0 ? void 0 : initialDomain.y[1]));
}
var isZoomed = compareDomainToInitial();
var findAverageLinesCoordinates = function (array, lenght) {
return array.reduce(function (partialSum, item) { return partialSum + item; }, 0) / lenght;
};
React.useEffect(function () {
var length = data.length;
var xArray = data.map(function (d) { return d.x; });
var yArray = data.map(function (d) { return d.y; });
setXLineCoordinates(findAverageLinesCoordinates(xArray, length));
setYLineCoordinates(findAverageLinesCoordinates(yArray, length));
setFilteredData(data);
resetDomain(true);
}, [data]);
var handleZoomOut = function () {
setFilteredData(data);
resetDomain();
};
var handleMouseDown = function (e) {
setIsDragging(true);
var _a = e || {}, xValue = _a.xValue, yValue = _a.yValue;
setDragAnchor([xValue || 0, yValue || 0]);
setIsZoomBlockingHover(true);
};
var handleMouseUp = function () {
if (isDragging) {
setIsDragging(false);
setDragAnchor(undefined);
}
debouncedUnblockHovers();
};
var handleDoubleClick = function (e) {
var rect = wrapperRef.current.getBoundingClientRect();
var zoomAmount = zoomOptions.doubleClickZoom || DOUBLE_CLICK_ZOOM_AMOUNT;
var newDomain = calculateDomainAfterZoom(domain, zoomAmount, domain, e.clientX - rect.left, e.clientY - rect.top, pxConversionDimensions, (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.height) || 0, (plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.width) || 0, plotDimensions === null || plotDimensions === void 0 ? void 0 : plotDimensions.offset);
var dataPointsInRange = filterDataByDomain(filteredData, newDomain);
setFilteredData(dataPointsInRange);
setDomain(newDomain);
};
var handleMouseMove = function (e) {
if (isDragging && e) {
var xValue = e.xValue, yValue = e.yValue;
var newDomain = dragDomain(dragAnchor, [xValue, yValue], domain, initialDomain);
var dataPointsInRange = filterDataByDomain(data, newDomain);
setFilteredData(dataPointsInRange);
setDomain(newDomain);
}
};
var builtData = React.useMemo(function () { return buildDataPoints(filteredData, pxConversionDimensions); }, [filteredData, pxConversionDimensions]);
var shouldHideLabel = evaluateLabels(builtData, pxConversionDimensions);
var ResetButtonEndIcon = _jsx(MagnifyMinusOutline, {});
var formatAverageLineLabel = function (value) {
return numeral(value).format('0,0[.]00');
};
return (_jsxs("div", { className: "plot-container", onDoubleClick: handleDoubleClick, ref: wrapperRef, style: { position: 'relative', width: '100%' }, children: [isZoomed && (_jsx(Button, { color: "secondary", endIcon: ResetButtonEndIcon, onClick: handleZoomOut, sx: { position: 'absolute', right: spacing(1), zIndex: 1000 }, variant: "contained", children: "Reset" })), _jsx(ResponsiveContainer, __assign({ debounce: 50, height: height || 400, width: "100%" }, responsiveContainerProps, { children: _jsxs(ScatterChart, __assign({ height: height || 400, margin: {
bottom: parseInt(String(spacing(4)), 10),
left: parseInt(String(spacing(0)), 10),
right: parseInt(String(spacing(2)), 10),
top: parseInt(String(spacing(4)), 10),
}, onClick: function () {
if (!isMouseOverScatter)
setSelectedPoint(undefined);
}, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, ref: measuredRef, style: {
cursor: isDragging ? 'grabbing' : 'grab',
fontFamily: typography.fontFamily,
} }, chartProps, { children: [_jsx(CartesianGrid, __assign({ horizontal: false, stroke: palette.grey[300] }, cartesianGridProps)), _jsx(XAxis, __assign({ allowDecimals: false, dataKey: "x", interval: 0, tickLine: false, type: "number" }, xAxisProps, { domain: domain.x.map(function (a) { return fixDecimal(a, 2); }), children: xLabelValue && (_jsx(Label, __assign({ position: "bottom", style: { userSelect: 'none' }, value: xLabelValue }, xLabelProps))) })), _jsx(YAxis, __assign({ allowDecimals: false, axisLine: { stroke: palette.grey[400] }, dataKey: "y", interval: 0, tickLine: false, type: "number", unit: "%", width: 100 }, yAxisProps, { domain: domain.y.map(function (a) { return fixDecimal(a, 2); }), children: _jsx(Label, __assign({ angle: -90, offset: -20, position: "center", style: { userSelect: 'none' }, value: yLabelValue }, yLabelProps)) })), _jsx(ZAxis, __assign({ dataKey: "x", range: [300, 300] }, zAxisProps)), _jsx(Tooltip, __assign({ content: _jsx(CustomTooltip, { CustomTooltipContent: CustomTooltipContent, isBlockingOnHovers: isBlockingOnHovers }), cursor: { strokeDasharray: '3 3' }, wrapperStyle: { outline: 'none' } }, tooltipProps)), selectedPoint && (_jsxs(_Fragment, { children: [_jsx(ReferenceLine, { strokeDasharray: "3 3", x: selectedPoint.x }), _jsx(ReferenceLine, { strokeDasharray: "3 3", y: selectedPoint.y })] })), showAverageLine && (_jsxs(_Fragment, { children: [_jsx(ReferenceLine, __assign({ opacity: selectedPoint ? OPACITY_NOT_HIGHLIGHTED : 1, strokeDasharray: "3 3", x: xLineCoordinates }, xAverageLineProps, { children: _jsx(Label, __assign({ position: "insideBottom", value: "Top N Average: ".concat(formatAverageLineLabel(Number(xLineCoordinates))).concat(averageLineXLabelUnit || '') }, xAverageLineLabelProps)) })), _jsx(ReferenceLine, __assign({ opacity: selectedPoint ? OPACITY_NOT_HIGHLIGHTED : 1, strokeDasharray: "3 3", y: yLineCoordinates }, yAverageLineProps, { children: _jsx(Label, __assign({ position: "insideTopLeft", value: "Top N Average: ".concat(formatAverageLineLabel(Number(yLineCoordinates)), "%") }, yAverageLineLabelProps)) }))] })), _jsx(Scatter, __assign({ data: builtData, isAnimationActive: false, onClick: function (e) {
if (isMouseOverScatter)
setSelectedPoint(e.payload);
}, onMouseEnter: function () { return setIsMouseOverScatter(true); }, onMouseLeave: function () { return setIsMouseOverScatter(false); }, shape: _jsx(CustomizedCell, { color: color, selectedPoint: selectedPoint }) }, scatterProps, { children: _jsx(LabelList, { content: _jsx(CustomizedLabel, { isBlockingOnHovers: isBlockingOnHovers, selectedPoint: selectedPoint, shouldHideLabel: shouldHideLabel }), dataKey: "label", fill: scatterLabelColor || common.black, position: "top" }) })), children] })) })), !hideSummary && (_jsx(RankSummary, { data: filteredData, idSubstring: idSubstring, selectedPoint: selectedPoint, setSelectedPoint: setSelectedPoint, setShowSummary: setShowSummary, showSummary: showSummary }))] }));
};
export default ScatterPlot;
//# sourceMappingURL=index.js.map