react-native-graph等で見られる、カーブのついた丸みのある折れ線グラフを実現してみたいと思います。描画はReact Native Skiaで行います。
まず通常の角張った折れ線グラフを作ってみます。
これは各点から点へ直接を引くだけです。
const hPadding = 20
const yScale = 2
const data = [35, 75, 45, 60, 40]
const xPointsDistance = (canvasSize.width - hPadding * 2) / (data.length - 1)
const path = Skia.Path.Make()
for (let i = 0; i < data.length; i++) {
if (i === 0) {
path.moveTo(hPadding, canvasSize.height - data[0])
} else {
path.lineTo(
hPadding + i * xPointsDistance,
canvasSize.height - data[i] * yScale,
)
}
}
<Canvas>
<Path path={path} color="lightblue" strokeWidth={2} style="stroke" />
</Canvas>
ではカーブのついた折れ線グラフを実装してみます。今回はd3-shapeのLine Generatorを使います。自身でベジェ曲線を使って計算することもできますが難易度が上がるためそれはまたの機会にします。
D3 ShapeのLine Generatorを使うと座標のデータからPathデータを生成することができます。ジェネレータを作成する際にはカーブを設定できるため(line().curve()
)、これを利用します。
import * as shape from "d3-shape"
const hPadding = 8
const yScale = 2
const data = [5, 75, 25, 60, 35]
const xPointsDistance = (canvasSize.width - hPadding * 2) / (data.length - 1)
const points: [number, number][] = data.map((d, i) => [
hPadding + i * xPointsDistance,
canvasSize.height - d * yScale,
])
const lineGenerator = shape.line().curve(shape.curveCardinal.tension(0.1))
const pathData = lineGenerator(points)
<Canvas>
<Path path={pathData} style="stroke" color="lightblue" strokeWidth={2} />
</Canvas>
curve()
に渡す引数でカーブの種類を指定することができます。
カーブでcurveCardinal
を設定している際、tension
の値を0から1まで指定できます。これはカーブの張力を指定するものです。これをアニメーションで変化させてみます。以下、タップするとテンションが0 - 1で変化します。
const canvasSize = { width: 300, height: 200 }
const hPadding = 8
const yScale = 2
const data = [5, 75, 25, 60, 35]
const xPointsDistance = (canvasSize.width - hPadding * 2) / (data.length - 1)
const points: [number, number][] = data.map((d, i) => [
hPadding + i * xPointsDistance,
canvasSize.height - d * yScale,
])
const lineGenerator = shape.line().curve(shape.curveCardinal.tension(0.1))
const pathData = lineGenerator(points)
export default function AnimatedCurvedLineGraph() {
const tension = useValue(1)
const pathData = useComputedValue(() => {
const lineGenerator = shape
.line()
.curve(shape.curveCardinal.tension(tension.current))
const data = lineGenerator(points)
return data
}, [tension])
const touchHandler = useTouchHandler({
onStart: () => {
if (tension.current === 0) {
runTiming(tension, 1, {
easing: Easing.inOut(Easing.ease),
duration: 1000,
})
}
if (tension.current === 1) {
runTiming(tension, 0, {
easing: Easing.inOut(Easing.ease),
duration: 1000,
})
}
},
})
return (
<Canvas style={[styles.canvas, canvasSize]} onTouch={touchHandler}>
<Path path={pathData} style="stroke" color="lightblue" strokeWidth={2} />
</Canvas>
)
}
今回のすべてのコードはこちらから確認できます。