2022-10-21

react-native-skiaで丸みのついた折れ線グラフを実現する

react-native-graph等で見られる、カーブのついた丸みのある折れ線グラフを実現してみたいと思います。描画はReact Native Skiaで行います。

角張った折れ線グラフ

まず通常の角張った折れ線グラフを作ってみます。

Loading 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>
Loading Skia..

curve() に渡す引数でカーブの種類を指定することができます。

カーブのテンションを変化させてみる

カーブでcurveCardinalを設定している際、tensionの値を0から1まで指定できます。これはカーブの張力を指定するものです。これをアニメーションで変化させてみます。以下、タップするとテンションが0 - 1で変化します。

Loading Skia..
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>
  )
}

コード

今回のすべてのコードはこちらから確認できます。

最終更新: 2022-10-21 00:40
筆者: @gaishimo 主にReact Nativeでのアプリ開発を行っています。
© 2021 Omoidasu, Inc. All rights reserved.