wix-style-react
Version:
wix-style-react
210 lines • 11.7 kB
JavaScript
import React, { useState, useRef, useMemo, useEffect, memo } from 'react';
import PropTypes from 'prop-types';
import { scaleLinear } from 'd3-scale';
import { select } from 'd3-selection';
import { lineRadial, curveCardinalClosed } from 'd3-shape';
import { dataHooks } from './constants';
import { st, classes } from './RadarChart.st.css';
import { stVars as colorsStVars } from '../Foundation/stylable/colors.st.css';
import Tooltip from '../Tooltip';
const BLUE_COLOR = '#3899ec';
const DISABLED_COLOR = '#879cae';
const DOT_RADIUS = 2;
const LINE_DY = 0.35;
const LINE_HEIGHT = 1.4;
const PADDING = 12;
const AXIS_LABEL_DY = 12;
const getXY = (value, index, angle, rScale, shift = 0) => ({
x: (rScale(value) + shift) * Math.cos(angle * index - Math.PI / 2),
y: (rScale(value) + shift) * Math.sin(angle * index - Math.PI / 2),
});
const CirclesSVG = memo(({ scale, rScale }) => {
return scale
.map(({ value }, index) => (React.createElement("circle", { key: index, "data-hook": dataHooks.radarChartScaleLine, className: st(classes.circles, {
isTransparent: index !== scale.length - 1,
}), r: rScale(value), cx: 0, cy: 0 })))
.reverse();
});
const WebChartSVG = memo(({ disabled, dots }) => {
if (disabled || dots.length === 0) {
return;
}
return (React.createElement("defs", null,
React.createElement("linearGradient", { id: "gradient", x1: "0", x2: "1", y1: "0", y2: "0" },
React.createElement("stop", { offset: "0", stopColor: dots[dots.length - 1].color }),
React.createElement("stop", { offset: "1", stopColor: dots[0].color }))));
});
const OuterLabels = memo(({ data, maxScaleValue, angle, rScale, labelDistance, disabled, internalHoverIndex, width, setInternalHoverIndex, }) => {
const outerCircleDots = data.map(({ label }, index) => ({
label,
labelPoint: getXY(maxScaleValue, index, angle, rScale, labelDistance),
linePoint: getXY(maxScaleValue, index, angle, rScale),
}));
return outerCircleDots.map(({ label, labelPoint, linePoint }, index) => (React.createElement("g", { key: index, "data-hook": dataHooks.radarChartDataItem },
React.createElement("line", { className: classes.axis, x1: 0, y1: 0, x2: linePoint.x, y2: linePoint.y }),
React.createElement("text", { className: st(classes.axisLabel, {
hovered: index === internalHoverIndex,
size: width < 360 ? 'small' : 'large',
disabled,
}), "data-hook": `axisName${index}`, x: labelPoint.x, y: labelPoint.y, dy: `${LINE_DY}em`, textAnchor: "middle", onMouseEnter: () => !disabled && setInternalHoverIndex(index), onMouseLeave: () => !disabled && setInternalHoverIndex(null) }, label))));
});
const ScaleValues = memo(({ scale, rScale }) => {
const scaleValues = scale.map(({ value, label }) => ({
value,
label,
...getXY(value, 0, 0, rScale),
}));
return scaleValues.map(({ label, y }, index) => (React.createElement("text", { key: index, className: classes.scaleLabel, x: 0, y: y, dy: AXIS_LABEL_DY, textAnchor: "middle" }, label)));
});
const DataPoints = memo(({ dots, disabled, internalHoverIndex, setInternalHoverIndex }) => dots.map((dot, index) => (React.createElement("g", { key: index },
React.createElement("circle", { r: !disabled && index === internalHoverIndex
? DOT_RADIUS * 2
: DOT_RADIUS, cx: dot.x, cy: dot.y, fill: disabled ? DISABLED_COLOR : dots[index].color }),
React.createElement("circle", { className: st(classes.dataPoint, { disabled }), r: DOT_RADIUS * 4, cx: dot.x, cy: dot.y, fillOpacity: 0, onMouseEnter: () => !disabled && setInternalHoverIndex(index), onMouseLeave: () => !disabled && setInternalHoverIndex(null), ...(!disabled && {
focusable: true,
tabIndex: 0,
onFocus: () => !disabled && setInternalHoverIndex(index),
onBlur: () => !disabled && setInternalHoverIndex(null),
}) })))));
const DataPointsTooltips = memo(({ dots, containerWidth, internalHoverIndex, disabled }) => dots.map(({ x, y, tooltipContent, value }, index) => (React.createElement("div", { key: index, style: {
position: 'absolute',
left: x + containerWidth / 2,
top: y + containerWidth / 2 - 7,
} },
React.createElement(Tooltip, { className: classes.tooltip, shown: index === internalHoverIndex, content: tooltipContent ?? value, disabled: disabled },
React.createElement("div", null))))));
const WebChartLine = memo(({ disabled, data, angle, rScale }) => {
const ref = useRef(null);
useEffect(() => {
if (ref.current && data.length) {
select(ref.current).selectAll("[data-hook='curvePart']").remove();
const drawCurve = lineRadial()
.curve(curveCardinalClosed)
.radius(d => rScale(d.value))
.angle((_d, i) => i * angle);
const blob = select(ref.current)
.selectAll("[data-hook='curvePart']")
.data([data])
.enter()
.append('g')
.attr('data-hook', 'curvePart');
blob
.append('path')
.attr('d', d => drawCurve(d))
.style('stroke', BLUE_COLOR)
.style('stroke-width', '1px')
.style('stroke', disabled ? DISABLED_COLOR : 'url(#gradient)')
.style('fill', disabled ? DISABLED_COLOR : 'url(#gradient)')
.style('stroke-dasharray', disabled ? 6 : null)
.style('fill-opacity', 0.3);
}
}, [ref, data, disabled, rScale, angle]);
return React.createElement("g", { ref: ref });
});
const RadarChart = ({ dataHook, data = [], scale = [
{ value: 50, label: '50%' },
{ value: 100, label: '100%' },
], disabled, width = 150, labelDistance = 50, hoverIndex, labelWidth = 100, onDataPointHover = () => { }, }) => {
const [internalHoverIndex, setInternalHoverIndex] = useState(hoverIndex);
const containerWidth = useMemo(() => width + labelDistance * 4 + PADDING * 2, [width, labelDistance]);
const maxScaleValue = useMemo(() => Math.max(...scale.map(({ value }) => value)), [scale]);
const rScale = useMemo(() => scaleLinear()
.range([0, width / 2])
.domain([0, maxScaleValue]), [width, maxScaleValue]);
const angle = useMemo(() => (Math.PI * 2) / data.length, [data]);
const dots = useMemo(() => data.map(({ value, label, color, tooltipContent }, index) => ({
value,
label,
color: colorsStVars[color] || color || colorsStVars['A1'],
tooltipContent,
...getXY(value, index, angle, rScale),
})), [data, angle, rScale]);
useEffect(() => setInternalHoverIndex(hoverIndex), [hoverIndex]);
useEffect(() => {
if (internalHoverIndex === null) {
return;
}
onDataPointHover(data[internalHoverIndex], internalHoverIndex);
}, [internalHoverIndex, onDataPointHover, data]);
useEffect(() => {
data.forEach(({ label }, index) => {
const text = select(containerRef.current).select(`[data-hook='axisName${index}']`);
const words = label.split(/\s+/);
let word;
let line = [];
let lineNumber = 0;
let tspan = text
.text(null)
.append('tspan')
.attr('x', text.attr('x'))
.attr('y', text.attr('y'))
.attr('dy', `${LINE_DY}em`);
while (words.length) {
word = words.shift();
line.push(word);
tspan.text(line.join(' '));
if (tspan.node &&
tspan.node().getComputedTextLength &&
tspan.node().getComputedTextLength() > labelWidth) {
line.pop();
tspan.text(line.join(' '));
line = [word];
tspan = text
.append('tspan')
.attr('x', text.attr('x'))
.attr('y', text.attr('y'))
.attr('dy', `${++lineNumber * LINE_HEIGHT + LINE_DY}em`)
.text(word);
}
}
});
});
const containerRef = useRef(null);
return (React.createElement("div", { "data-hook": dataHook, className: classes.root, style: { width: containerWidth, height: containerWidth }, "data-disabled": disabled, "data-hover-index": internalHoverIndex },
React.createElement("svg", { width: containerWidth, height: containerWidth, ref: containerRef },
React.createElement("g", { transform: `translate(${containerWidth / 2}, ${containerWidth / 2})` },
React.createElement(WebChartSVG, { dots: dots, disabled: disabled }),
React.createElement(CirclesSVG, { scale: scale, rScale: rScale }),
React.createElement(OuterLabels, { data: data, maxScaleValue: maxScaleValue, angle: angle, rScale: rScale, labelDistance: labelDistance, disabled: disabled, internalHoverIndex: internalHoverIndex, width: width, setInternalHoverIndex: setInternalHoverIndex }),
React.createElement(ScaleValues, { scale: scale, rScale: rScale }),
React.createElement(WebChartLine, { disabled: disabled, data: data, angle: angle, rScale: rScale }),
React.createElement(DataPoints, { dots: dots, disabled: disabled, internalHoverIndex: internalHoverIndex, setInternalHoverIndex: setInternalHoverIndex }))),
React.createElement(DataPointsTooltips, { dots: dots, disabled: disabled, containerWidth: containerWidth, internalHoverIndex: internalHoverIndex, setInternalHoverIndex: setInternalHoverIndex })));
};
RadarChart.displayName = 'RadarChart';
RadarChart.propTypes = {
/** Applies a data-hook HTML attribute that can be used in tests */
dataHook: PropTypes.string,
/** Defines data points that construct spider web-like charts. Available properties for array items:<br />
 - `value` - a number that represents value in chart<br />
 - `label` - a label that represents value description around the chart<br />
 - `color` - data point color.<br />
 - `tooltipContent` - data point tooltip content */
data: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.number,
label: PropTypes.string,
color: PropTypes.string,
tooltipContent: PropTypes.node,
})),
/** Defines a number of scale circles in the chart. Available properties for each scale:<br />
 - `value` - a number representing scale<br />
 - `suffix` - a string that represents value meaning (i.e. % or $) */
scale: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.number,
label: PropTypes.string,
})),
/** Specifies whether graph is disabled */
disabled: PropTypes.bool,
/** Controls the width of a graph. Minimum width is 150 pixels. */
width: PropTypes.number,
/** Controls label distance from a chart */
labelDistance: PropTypes.number,
/** Defines the index of a data point in hover state */
hoverIndex: PropTypes.number,
/** Defines maximum width of data labels */
labelWidth: PropTypes.number,
/** Defines a callback function which is called every time user hovers over a data point. Includes all data point data as first argument and index as second. */
onDataPointHover: PropTypes.func,
};
export default RadarChart;
//# sourceMappingURL=RadarChart.js.map