如何在ReCharts图表中用虚线连接断开的线段?
用ReCharts实现虚线连接断开的线段
我用React搭配ReCharts开发图表,当前图表中的线段存在断开间隔,希望用虚线连接这些分离的线段。
当前问题效果:
期望实现效果:
我希望仅通过ReCharts库完成该需求,尽量不安装额外库,以下是我的代码:
const calculateYearAndMonth = (data: LrpAnalysisPriceTrendsChartData[]) => { const yearCounter: Counter = {}; const monthCounter: Counter = {}; data.forEach((element) => { const year = element.name.slice(4, 8); const yearAndMonth = element.name; !yearCounter[year] ? (yearCounter[year] = 1) : (yearCounter[year] += 1); !monthCounter[yearAndMonth] ? (monthCounter[yearAndMonth] = 1) : (monthCounter[yearAndMonth] += 1); }); return { yearCount: yearCounter, monthCount: monthCounter }; }; const tooltipRenderer = (selectedMeasure: string, unit: string, options: { id: string; name: string; color?: string }[]) => ({ active, payload }: any) => { if (active && payload && payload[0]) { return ( <TooltipContainer> <TooltipLabel>{payload[0].payload.endDate}</TooltipLabel> {payload.map(({ dataKey, color, value }: any) => { let yearInterval: string; dataKey === 'historicalPrice' ? (yearInterval = payload[0].payload.yearInterval) : (yearInterval = ''); return ( <TooltipRow key={dataKey} color={color}> {yearInterval} {selectedMeasure} : {` $${value.toFixed(2)}${unit === '$/cwt' ? '/cwt' : unit === '$/head' ? '/head' : ''}`} </TooltipRow> ); })} </TooltipContainer> ); } }; const renderYearTick = (data: LrpAnalysisPriceTrendsChartData[]) => (tickProps: any) => { const { x, y, payload } = tickProps; const counters: { [key: string]: { count: number; lastMonth: number } } = {}; data.forEach((endorsementSummary) => { if (!counters[endorsementSummary['endMonth'].split('-')[1]]) { counters[endorsementSummary['endMonth'].split('-')[1]] = { count: 0, lastMonth: 0 }; counters[endorsementSummary['endMonth'].split('-')[1]].count = 1; counters[endorsementSummary['endMonth'].split('-')[1]].lastMonth = Number( endorsementSummary['endMonth'].split('-')[0] ); } else { counters[endorsementSummary['endMonth'].split('-')[1]].count += 1; counters[endorsementSummary['endMonth'].split('-')[1]].lastMonth = Number( endorsementSummary['endMonth'].split('-')[0] ); } }); const { value, offset } = payload; const currentMonth = Number(value.split('-')[0]); const centerCalculator = (t: number) => { switch (t) { case 1: return x + offset; case 2: return x; case 3: return x - offset; case 4: return x - offset * 2; case 5: return x - offset * 3; case 6: return x - offset * 4; case 7: return x - offset * 5; case 8: return x - offset * 6; case 9: return x - offset * 7; case 10: return x - offset * 8; case 11: return x - offset * 9; case 12: return x - offset * 10; } }; if (currentMonth === counters[value.split('-')[1]].lastMonth) { return ( <text x={centerCalculator(counters[value.split('-')[1]].count)} y={y} textAnchor="middle" fontSize={14}>{`${ value.split('-')[1] }`}</text> ); } }; const renderLineTick = (data: LrpAnalysisPriceTrendsChartData[]) => (tickProps: any) => { const { x, y, payload, index } = tickProps; const counters: { [key: string]: number } = {}; data.forEach((endorsementSummary) => { if (!counters[endorsementSummary['endMonth'].slice(2, 6)]) { counters[endorsementSummary['endMonth'].slice(2, 6)] = 1; } else { counters[endorsementSummary['endMonth'].slice(2, 6)] += 1; } }); const { value, offset } = payload; const current_month = Number(value.split('-')[0]); const isLast = index === data.length - 1; if (index === 0 || isLast || current_month === 12) { const pathX = Math.floor(isLast || current_month === 12 ? x + offset * 2 : x); return <path d={`M${pathX},${y - 13}v${-35}`} stroke="#CCCCCC" />; } return null; }; const renderMonthTick = (name: string) => name.slice(0, 3); interface Props { data: LrpAnalysisPriceTrendsChartData[]; width?: number; height?: number; gap: number; options: { id: string; name: string; color?: string }[]; unit: number; selectedMeasure: { name: string; id: string }; yearInterval: string; } const PriceTrendsChart: React.FC<Props> = ({ width, height, data, options, gap, unit, selectedMeasure, yearInterval }) => { const leftDomain = getDomain(data.map(({ currentPrice, historicalPrice }) => ({ currentPrice, historicalPrice }))); const [range, setRange] = React.useState({ startIndex: 0, endIndex: data.length - 1 }); React.useEffect(() => { setRange({ startIndex: 0, endIndex: data.length - 1 }); }, [data.length]); const onBrushChange = (e: BrushRange) => { setRange(e); }; const dataSliced = [...data.slice(range.startIndex, range.endIndex + 1)]; const monthAndYearCount = calculateYearAndMonth(dataSliced); return ( <> <ResponsiveContainer width="99%"> <LineChart width={width} height={height} data={data} margin={{ top: 5, right: 30, left: 30, bottom: 10 }} > <YAxis label={{ value: unit === Unit.CWT ? '$/cwt' : unit === Unit.HEAD ? '$/head' : '$', position: 'insideLeft', angle: -90, dy: 30, dx: -5, fontWeight: 200, fontSize: 15 }} yAxisId="left" scale="linear" tickLine={false} tick={{ fontSize: 14 }} type="number" domain={leftDomain} tickFormatter={formatValue(2)} interval="preserveStart" tickCount={13} width={99} /> <XAxis dataKey="name" height={40} tickFormatter={renderMonthTick} tick={{ fontSize: 12 }} /> <XAxis dataKey="endMonth" axisLine={false} tickLine={false} interval={0} tick={renderLineTick(data)} height={30} scale="band" xAxisId="month" /> {data[0].currentPrice !== 0 && data[0].endDate !== '' && ( <XAxis dataKey="endMonth" axisLine={false} tickLine={false} interval={0} tick={renderYearTick(data)} height={35} scale="band" xAxisId="year" /> )} <Tooltip content={tooltipRenderer( selectedMeasure.name, unit === Unit.CWT ? '$/cwt' : unit === Unit.HEAD ? '$/head' : '', options )} /> <Legend margin={{ top: 20 }} /> {data[0].currentPrice !== 0 && data[0].endDate !== '' && ( <Line key={'currentPrice'} name={selectedMeasure.name} type="linear" yAxisId={'left'} dataKey={'currentPrice'} stroke={'#0000FF'} strokeWidth={2} dot={false} /> )} <Line key={'historicalPrice'} name={yearInterval + ' ' + selectedMeasure.name} type="linear" yAxisId={'left'} dataKey={'historicalPrice'} stroke={'#909090'} strokeWidth={2} dot={false} /> </LineChart> </ResponsiveContainer> </> ); };
解决方案
直接利用ReCharts自带的属性就能实现,不需要额外依赖:
- 连接断点:给
<Line>组件添加connectNulls={true},这个属性会让ReCharts自动连接数据中空值对应的断开线段。 - 设置虚线样式:通过
strokeDasharray属性定义虚线的间隔规则,比如"5 5"表示5px实线+5px空白循环,你可以根据需求调整数值(比如"10 3"就是更长的实线加短空白)。
修改后的两个<Line>组件代码如下:
{data[0].currentPrice !== 0 && data[0].endDate !== '' && ( <Line key={'currentPrice'} name={selectedMeasure.name} type="linear" yAxisId={'left'} dataKey={'currentPrice'} stroke={'#0000FF'} strokeWidth={2} dot={false} connectNulls={true} strokeDasharray="5 5" /> )} <Line key={'historicalPrice'} name={yearInterval + ' ' + selectedMeasure.name} type="linear" yAxisId={'left'} dataKey={'historicalPrice'} stroke={'#909090'} strokeWidth={2} dot={false} connectNulls={true} strokeDasharray="5 5" />
如果你的线段断开不是因为数据中有空值,而是缺少中间的日期数据点,可以先预处理数据:补全缺失的日期条目,将对应价格字段设为null,再用上述属性就能实现虚线连接。
内容的提问来源于stack exchange,提问作者Marko Marchisio
相关产品推荐
相关产品推荐

