グラフライブラリNivoを使ってみた

# はじめに

グラフライブラリの選定をしているときに、Nivoというものを見つけました。

公式サイトで設定を色々試してみることができるし、その結果のコードをコピペすることもできるで「これは楽かも」と思い、試してみることにしました。

・・・すぐに後悔しました。ドキュメントらしいドキュメントがないのです！　基本的にResponsiveLineコンポーネントに複数の属性を渡して設定を行うのですが、ResponsiveLineに何を渡すことができるのか、それぞれがどのような役割を果たしているのかの説明がどう考えても不足しています。

今後ドキュメントが書かれることを期待していますが、それまでにどうしてもNivoを使ってみたいという人向けに、試してみてわかった範囲のことを残そうと思います。

（やり方がどうにもわからなかったものについては、styled-componentを使って乗り切ってしまった部分がありますし、誤りもあるかと思いまう。ご了承ください）

# 成果物イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3527059/d5ca14ca-0082-e1e9-4d77-7d8e937fdf83.png)


# 準備

今回はNext.jsを使ってやりたいので、適当なディレクトリで`npx create-next-app@latest --typescript`を実行してプロジェクトを作成します。

その後、必要なライブラリを入れていきます。

```bash
npx create-next-app@latest --typescript

// 必要があれば適切なディレクトリに移動

// nivoのインストール
yarn add @nivo/line
```

スタート画面を構成している諸々のものはいらないので消しておいて、page.tsxは以下のようにします。

```tsx
import MyResponsiveLine from "@/features/MyResponsiveLine";

export default function Home() {
	return (
		<div>
			<div style={{ height: "500px" }}>
				{/* ↓これから作る */}
				<MyResponsiveLine />
			</div>
		</div>
	);
}

```

MyResponsiveLineコンポーネントにてNivoの設定をしていきます！

ここで注意ですが、`MyResponsiveLine`の親要素には必ず `height` を明示するようにしましょう！

# Nivoの公式からコピペする

https://nivo.rocks/line/

Nivoの公式ページからコードを丸ごとコピペします。これの設定を変更して、最終的な成果物作成を目指します。

今回dataは以下のものを使います。

```tsx
const simpleData = [
	{
		id: "cost",
		data: [
			{ x: "Sun", y: 1000 },
			{ x: "Mon", y: 800 },
			{ x: "Tue", y: 1300 },
			{ x: "Wed", y: 1250 },
			{ x: "Thur", y: 1290 },
			{ x: "Fri", y: 1080 },
			{ x: "Sat", y: 1500 },
		],
	},
];
```

# グラフの設定

### 線の種類

グラフの線の種類はcurveで変更することができます。

curveの型を見てみると

```tsx
curve?:
  | 'basis'
  | 'cardinal'
  | 'catmullRom'
  | 'linear'
  | 'monotoneX'
  | 'monotoneY'
  | 'natural'
  | 'step'
  | 'stepAfter'
  | 'stepBefore'
```

このようになっているので、これらの値を指定することができそうです。

それぞれがどのようなものかは[公式ページ](https://nivo.rocks/line/#:~:text=curve,%27linear%27)で確認できます。今回は`catmullRom`にします。

 `catmullRom` が何かについては以下の記事が参考になります。

https://zenn.dev/mushe/articles/92c65e0c8023aa

### グラフの色

ついでに線の色も変えておきましょう。 `colors` で変更することができます。

 `colors` には”red”や”#FC60FF”などの文字列の他に、文字列の配列を渡すこともできます。複数の線を描画したい時は配列を渡すことで、すべての線の色を指定することができます。

```tsx
curve="catmullRom"
colors={"red"}
```

余談ですが、Nivoには複数のカラーテーマが用意されており、これらを使うこともできます。

https://nivo.rocks/guides/colors/

### 丸を消す

縦の線とグラフが交わるところに丸が表示されています。これは `point` で管理されています。今回 `point` は不要なので、`point` に関係するものは削除します。また`enablePoint={false}`を追記して、丸を使わないことを明示しておきましょう。これを設定しないと丸が残ってしまいます。

```tsx
// 削除
pointSize={5}
pointColor={{ theme: "background" }}
pointBorderWidth={2}
pointBorderColor={{ from: "serieColor" }}
pointLabel="data.yFormatted"
pointLabelYOffset={-12}

// 追加
enablePoint={false}
```

# 軸の設定を変える

### X軸の設定

設定は`xScale`で行います。

```tsx
xScale={{
  type: 'point', // カテゴリデータとしてポイントを使用
  min: 0,      // 最小値（線形スケールの場合）
  max: 10,     // 最大値（線形スケールの場合）
  stacked: false, // スタックしない
  reverse: false, // 逆順にはしない
}}
```

このように、軸の最大値や最小値、逆順で表示するかなどを設定することができます。

今回は`xScale={{ type: "point" }}`だけ指定します。この設定により、各ポイントが等間隔に配置されます。

pointの他にもlinearを指定することができます。

https://nivo.rocks/line/#:~:text=Chart%20data.-,xScale,optional,-X%20scale%20configuration

### Y軸の設定

設定は`yScale`で行います。設定の方法は`xScale`と同様にオブジェクトを渡します。

今回は以下のように設定します。

```tsx
yScale={{
	type: "linear",
	min: 0, // Y軸の最小値
	max: 2000, // Y軸の最大値
	stacked: false, // グラフを積み上げて表示するか
	reverse: false, // 逆順表示するか
}}
```

Y軸は0〜2000で固定したいので、`min`は0、`max`は2000で固定します。データの値によって`max`を変動させたい場合は、`”auto”`を指定しましょう。最大値は必ず特定の桁で切り上げたいなど細かい要望がある場合は、別途そのようなhooksを実装しましょう。

積み上げも逆順もしないので、`stacked`と`reverse`はどちらもfalseにします。

↓積み上げとは

https://nivo.rocks/storybook/?path=/story/line--stacked-lines


### axisの設定

それぞれの軸に表示する文字や説明を設定するのがaxisです。

現段階では軸の側にcountやtransportationと表示されていますが、これは不要なので消したいです。

この設定は`axisBottom`と`axisLeft`で行います。

```tsx
axisBottom={{
	tickSize: 0,
	tickPadding: 5,
	renderTick: ({ value, x, y }) => (
		<text
			x={x}
			y={y + 18}
			fill="#79858C"
			fontSize="11"
			textAnchor="middle"
		>
			{value}
		</text>
	),
}}
axisLeft={{
	tickSize: 0,
	tickPadding: 5.72,
	tickValues: [0, 500, 1000, 1500, 2000],
	renderTick: ({ value, x, y }) => (
		<text
			x={x - 35}
			y={y}
			fill="#79858C"
			fontSize="11"
			dominantBaseline="middle"
		>
			{value}
		</text>
	),
}}

```

`tick`は軸のメモリとして表示されている文字のことです。tickの色を直接変更する方法が見つからなかったので、従来のtickのSizeを０にして、renderTickで生成しています。`<tetx>`はsvgの書き方です。

axisLeftの方ではメモリとして表示するものを指定しています。この内容とyScaleで指定した内容が合うように注意しましょう。

countやtransportationは`legends`というもので管理されています。今回`legends`は不要なのでこれに関係するものは削除します。

また、グラフの説明としてグラフ外にcountと書かれたものがありますが、これも`legends`です。ついでに削除しておきましょう。

# ツールチップを変更する

デフォルトのツールチップは以下のようになっており、少しダサいです。

![スクリーンショット 2024-11-03 13.42.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3527059/aac677f2-f195-f1c2-a21b-2a4951a595e0.png)


なので独自のツールチップを設定しようと思います。以下をResponsiveLineコンポーネントに追加しましょう。

```tsx
tooltip={({ point }: { point: Point }) => (
	<StyledTooltip>
		<div>
			<TooltipGeneratedText>{point.data.yFormatted}円</TooltipGeneratedText>
		</div>
		{/* 下の吹き出し */}
		<StyledTooltipArrow />
	</StyledTooltip>
)}
```

StyledTooltipなどは以下のように定義しました。

- スタイル
    
    ```tsx
    import styled from "styled-components";
    
    const TooltipGeneratedText = styled.span`
    	font-family: Nunito;
    	font-size: 13px;
    	font-weight: 600;
    `;
    
    const StyledUnit = styled.span`
    	font-family: Nunito;
    	font-size: 11px;
    	font-weight: 600;
    	margin-left: 5px;
    `;
    
    const StyledTooltip = styled.div`
    	color: black;
    	background-color: white;
    	border: 2px solid red;
    	border-radius: 8px;
    	width: auto;
    	height: auto;
    	padding: 13px 11px 11px;
    `;
    
    const StyledTooltipArrow = styled.div`
    	position: absolute;
    	left: calc(50% - 6px);
    	transform: translateX(-50%);
    	width: 0;
    	height: 0;
    
    	// 枠線下の三角形
    	&::before {
    		content: "";
    		position: absolute;
    		bottom: -20px;
    		border-left: 6px solid transparent;
    		border-right: 6px solid transparent;
    		border-top: 9px solid red;
    	}
    
    	// 白い三角形の部分
    	&::after {
    		content: "";
    		position: absolute;
    		bottom: -16px;
    		border-left: 6px solid transparent;
    		border-right: 6px solid transparent;
    		border-top: 9px solid white;
    		z-index: 2;
    	}
    `;
    ```
    

これで以下のようなツールチップになりました。

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3527059/75a87499-7a9c-0368-eda0-13a3d005e779.png)


# グラフの背景を変える

### 格子をなくす

デザイン上邪魔な場合は消すこともできます。

```tsx
enableGridX={false}
enableGridY={false}
```

どちらか一方の線だけ消すこともできます。

### 背景に何かを表示する（応用？）

グラフの背景に文字を表示したりすることができます。今回はグラフの背景だけ色を変更するようにしてみます。

```tsx
layers={[
	"markers",
	"axes",
	"areas",
	"lines",
	"slices",
	"mesh",
	CustomLayer,
]}
```

`layers`はどのような順番でlineなどを表示するか決めることができます。ここにカスタムコンポーネントを渡すことで、独自のコンポーネントを表示させることができます。

`CustomLayer`は以下のように定義しました。

```tsx
// グラフ部分の背景色
const CustomLayer = (props: CustomLayerProps) => {
	const { innerHeight, innerWidth } = props;
	return (
		<g transform={`translate(0, 0)`} style={{ zIndex: -10 }}>
			<rect
				x={0}
				y={0}
				width={innerWidth}
				height={innerHeight}
				fill="rgba(255, 153, 0, 0.05)"
				style={{ pointerEvents: "none" }}
			/>
		</g>
	);
};
```

これでグラフの背景に色がついたかと思います。

### クリックしたところだけPointをつけたい

「すべてのPointを無効化したものの、クリックした部分にはPointがついてほしい」という場合も、layerにカスタムコンポーネントを渡すことで実現できます。

```tsx
// グラフをクリックしたときに表示するpoint
const CustomPointLayer = (clickedPoint: Point | null) =>
	function CustomPointLayerContext() {
		if (!clickedPoint) return null;
		return (
			<g style={{ pointerEvents: "none" }}>
				<circle
					cx={clickedPoint.x}
					cy={clickedPoint.y}
					r={4}
					stroke="red"
					strokeWidth={2}
					fill="white"
				/>
			</g>
		);
	};

const MyResponsiveLine = () => {	
	const [clickedPoint, setClickedPoint] = useState<Point | null>(null);

	const handleClick = (point: Point) => {
		setClickedPoint(point);
	};
	const handleMouseLeave = () => {
		setClickedPoint(null);
	};

	return (
		
		// ... 省略
		
		layers={[
			"markers",
			"axes",
			"areas",
			"lines",
			"slices",
			"mesh",
			CustomLayer,
			CustomPointLayer(clickedPoint),
		]}
		onClick={handleClick}
		onMouseLeave={handleMouseLeave} // グラフ外の場所をクリックした時Pointを消す

```

# 終わりに

最終的なコードはこちらです。

```tsx
"use client";

import styled from "styled-components";
import { ResponsiveLine, Point, CustomLayerProps } from "@nivo/line";
import { useState } from "react";

const StyledTooltip = styled.div`
	color: black;
	background-color: white;
	border: 2px solid red;
	border-radius: 8px;
	width: auto;
	height: auto;
	padding: 13px 11px 11px;
`;

const TooltipGeneratedText = styled.span`
	font-size: 13px;
	font-weight: 600;
`;

const StyledTooltipArrow = styled.div`
	position: absolute;
	left: calc(50% - 6px);
	transform: translateX(-50%);
	width: 0;
	height: 0;

	// 枠線下の三角形
	&::before {
		content: "";
		position: absolute;
		bottom: -20px;
		border-left: 6px solid transparent;
		border-right: 6px solid transparent;
		border-top: 9px solid red;
	}

	// 白い三角形の部分
	&::after {
		content: "";
		position: absolute;
		bottom: -16px;
		border-left: 6px solid transparent;
		border-right: 6px solid transparent;
		border-top: 9px solid white;
		z-index: 2;
	}
`;

// グラフ部分の背景色
const CustomLayer = (props: CustomLayerProps) => {
	const { innerHeight, innerWidth } = props;
	return (
		<g transform={`translate(0, 0)`} style={{ zIndex: -10 }}>
			<rect
				x={0}
				y={0}
				width={innerWidth}
				height={innerHeight}
				fill="rgba(255, 153, 0, 0.05)"
				style={{ pointerEvents: "none" }}
			/>
		</g>
	);
};

// グラフをクリックしたときに表示するpoint
const CustomPointLayer = (clickedPoint: Point | null) =>
	function CustomPointLayerContext() {
		if (!clickedPoint) return null;
		return (
			<g style={{ pointerEvents: "none" }}>
				<circle
					cx={clickedPoint.x}
					cy={clickedPoint.y}
					r={4}
					stroke="red"
					strokeWidth={2}
					fill="white"
				/>
			</g>
		);
	};

const simpleData = [
	{
		id: "cost",
		data: [
			{ x: "Sun", y: 1000 },
			{ x: "Mon", y: 800 },
			{ x: "Tue", y: 1300 },
			{ x: "Wed", y: 1250 },
			{ x: "Thur", y: 1290 },
			{ x: "Fri", y: 1080 },
			{ x: "Sat", y: 1500 },
		],
	},
];

const MyResponsiveLine = () => {
	const [clickedPoint, setClickedPoint] = useState<Point | null>(null);

	const handleClick = (point: Point) => {
		setClickedPoint(point);
	};
	const handleMouseLeave = () => {
		setClickedPoint(null);
	};

	return (
		<ResponsiveLine
			data={simpleData} // グラフ表示したいデータを渡す
			margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
			useMesh={true} // これをtrueにしないとクリックできなくなり、ツールチップも無効になる
			curve="catmullRom"
			colors={"red"}
			enablePoints={false}
			xScale={{ type: "point" }}
			yScale={{
				type: "linear",
				min: 0,
				max: 2000,
				stacked: false,
				reverse: false,
			}}
			axisBottom={{
				tickSize: 0,
				tickPadding: 5,
				renderTick: ({ value, x, y }) => (
					<text
						x={x}
						y={y + 18}
						fill="#79858C"
						fontSize="11"
						textAnchor="middle"
					>
						{value}
					</text>
				),
			}}
			axisLeft={{
				tickSize: 0,
				tickPadding: 5.72,
				tickValues: [0, 500, 1000, 1500, 2000],
				renderTick: ({ value, x, y }) => (
					<text
						x={x - 35}
						y={y}
						fill="#79858C"
						fontSize="11"
						dominantBaseline="middle"
					>
						{value}
					</text>
				),
			}}
			tooltip={({ point }: { point: Point }) => (
				<StyledTooltip>
					<div>
						<TooltipGeneratedText>
							{point.data.yFormatted}円
						</TooltipGeneratedText>
					</div>
					{/* 下の吹き出し */}
					<StyledTooltipArrow />
				</StyledTooltip>
			)}
			enableGridX={false}
			enableGridY={false}
			layers={[
				"markers",
				"axes",
				"areas",
				"lines",
				"slices",
				"mesh",
				CustomLayer,
				CustomPointLayer(clickedPoint),
			]}
			onClick={handleClick}
			onMouseLeave={handleMouseLeave}
		/>
	);
};

export default MyResponsiveLine;

```

ここまで書きましたが、ドキュメントが整備されるまでNivoを使わない方がいい気がします。設定方法を探すのがとても大変でした。

公式サイトのコードをコピペして事足りるようなものであれば、Nivoを検討してもいいかもしれませんね。