圖表功能
- 拖曳數據點可以修改y值
- 雙擊左鍵新增數據點
- 右鍵移除數據點
左鍵長按拖曳
import React, { useEffect, useRef, useState } from 'react'
import { Box } from '@chakra-ui/react'
import { Group } from '@visx/group'
import { scaleLinear } from '@visx/scale'
import { Axis } from '@visx/axis'
import { Grid } from '@visx/grid'
import { LinePath } from '@visx/shape'
const Chart = ({ onChange }) => {
const chartRef = useRef<HTMLDivElement>(null)
const [dragIndex, setDragIndex] = useState<number | null>(null)
const margin = { top: 20, right: 20, bottom: 45, left: 50 }
const width = 1024
const height = 768
const domain = [0, 100]
const xScale = scaleLinear({
domain,
range: [margin.left, width - margin.right],
})
const yScale = scaleLinear({
domain,
range: [height - margin.bottom, margin.top],
})
const handleDragStart = (index: number, event: React.MouseEvent) => {
if (event.button !== 0) return // 只允許左鍵拖曳, 0: 通常是左鍵 1: 通常是滾輪或中鍵 2: 通常是右鍵
setDragIndex(index)
}
const handleDragMove = (event: React.MouseEvent) => {
if (dragIndex === null) return
if (event.buttons !== 1) return // 只允許左鍵拖曳
const chart = chartRef.current
if (!chart) return
const svg = chart.querySelector('svg')
if (!svg) return
const rect = svg.getBoundingClientRect()
const y = event.clientY - rect.top // 計算滑鼠在 SVG 內的 y 座標
const newY = Math.max(0, Math.min(100, Math.round(yScale.invert(y)))) // 把滑鼠在 SVG 上的 y 像素座標,轉換成對應的數據值
const newData = data.map((pt, i) =>
i === dragIndex ? { ...pt, y: newY } : pt,
)
onChange?.(newData)
}
const handleDragEnd = () => {
setDragIndex(null)
}
return (
<Box
w="100%"
h={height}
ref={chartRef}
userSelect="none"
bg="transparent"
overflow="hidden"
onMouseMove={handleDragMove}
onMouseUp={handleDragEnd}
onMouseLeave={handleDragEnd}
>
<svg width={width} height={height}>
<Group>
<Grid
xScale={xScale}
yScale={yScale}
width={width}
height={height}
stroke="#444"
strokeDasharray="3 3"
numTicksRows={10}
numTicksColumns={10}
/>
<Axis
orientation="bottom"
scale={xScale}
top={height - margin.bottom}
tickFormat={(value) => `${value}`}
stroke="#fff"
tickStroke="#fff"
tickLabelProps={() => ({
fill: "#fff",
fontSize: 14,
textAnchor: 'middle',
})}
/>
<Axis
orientation="left"
scale={yScale}
left={margin.left}
tickFormat={(value) => `${Number(value)}%`}
stroke="#fff"
tickStroke="#fff"
tickLabelProps={() => ({
fill: "#fff",
fontSize: 14,
textAnchor: 'end',
dy: '0.33em',
})}
/>
<LinePath
data={data}
x={(d) => xScale(d.x)}
y={(d) => yScale(d.y)}
stroke="#FFDFA3"
strokeWidth={2}
/>
{data.map((point, i) => (
<circle
key={i}
cx={xScale(point.x)}
cy={yScale(point.y)}
r={4}
fill="#fff"
stroke="#FFDFA3"
strokeWidth={2}
style={{ cursor: 'ns-resize' }}
onMouseDown={(e) => handleDragStart(i, e)}
/>
))}
</Group>
</svg>
</Box>
)
}
Element.getBoundingClientRect()
能夠取得 SVG 在畫面上的位置與大小,用於計算滑鼠相對於 SVG 的座標。若要計算滑鼠在 SVG 內的 y 座標,即滑鼠的螢幕 y 座標(event.clientY)減去 SVG 上邊界的螢幕 y 座標(rect.top)。
![[React筆記]使用 Visx 製作可互動的折線圖表 2 element box diagram](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect/element-box-diagram.png)
雙擊新增數據點
x
, y
是計算滑鼠在 SVG 內的 x、y 座標,即滑鼠的螢幕座標減去 SVG 左上角的螢幕座標。其他都跟 handleDragMove 差不多邏輯。
const handleDoubleClick = (
event: React.MouseEvent<SVGSVGElement, MouseEvent>,
) => {
const chart = chartRef.current
if (!chart) return
const svg = chart.querySelector('svg')
if (!svg) return
const rect = svg.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
let xVal = Math.round(xScale.invert(x))
let yVal = Math.round(yScale.invert(y))
xVal = Math.max(0, Math.min(100, xVal))
yVal = Math.max(0, Math.min(100, yVal))
// 避免 x 重複
if (data.some((pt) => pt.x === xVal)) return
const newData = [...data, { x: xVal, y: yVal }].sort((a, b) => a.x - b.x)
onChange?.(newData)
}
在元素上雙擊會觸發 onDoubleClick
<svg width={width} height={height} onDoubleClick={handleDoubleClick}>
<Group>
//...
右鍵刪除數據點
在元素上單擊右鍵會觸發 onContextMenu
const handlePointRightClick = (event: React.MouseEvent, index: number) => {
event.preventDefault()
if (data.length <= 1) return // 至少保留1個點
const newData = data.filter((_, i) => i !== index)
onChange?.(newData)
}
{data.map((point, i) => (
<circle
key={i}
cx={xScale(point.x)}
cy={yScale(point.y)}
r={4}
fill="#fff"
stroke={colors.secondary}
strokeWidth={2}
style={{ cursor: 'ns-resize' }}
onMouseDown={(e) => handleDragStart(i, e)}
onContextMenu={(e) => handlePointRightClick(e, i)}
/>
))}