UNPKG

wix-style-react

Version:
210 lines • 11.7 kB
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 /> &emsp;- `value` - a number that represents value in chart<br /> &emsp;- `label` - a label that represents value description around the chart<br /> &emsp;- `color` - data point color.<br /> &emsp;- `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 /> &emsp;- `value` - a number representing scale<br /> &emsp;- `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