はじめに
グラフライブラリの選定をしているときに、Nivoというものを見つけました。
公式サイトで設定を色々試してみることができるし、その結果のコードをコピペすることもできるで「これは楽かも」と思い、試してみることにしました。
・・・すぐに後悔しました。ドキュメントらしいドキュメントがないのです! 基本的にResponsiveLineコンポーネントに複数の属性を渡して設定を行うのですが、ResponsiveLineに何を渡すことができるのか、それぞれがどのような役割を果たしているのかの説明がどう考えても不足しています。
今後ドキュメントが書かれることを期待していますが、それまでにどうしてもNivoを使ってみたいという人向けに、試してみてわかった範囲のことを残そうと思います。
(やり方がどうにもわからなかったものについては、styled-componentを使って乗り切ってしまった部分がありますし、誤りもあるかと思いまう。ご了承ください)
成果物イメージ
準備
今回はNext.jsを使ってやりたいので、適当なディレクトリでnpx create-next-app@latest --typescript
を実行してプロジェクトを作成します。
その後、必要なライブラリを入れていきます。
npx create-next-app@latest --typescript
// 必要があれば適切なディレクトリに移動
// nivoのインストール
yarn add @nivo/line
スタート画面を構成している諸々のものはいらないので消しておいて、page.tsxは以下のようにします。
import MyResponsiveLine from "@/features/MyResponsiveLine";
export default function Home() {
return (
<div>
<div style={{ height: "500px" }}>
{/* ↓これから作る */}
<MyResponsiveLine />
</div>
</div>
);
}
MyResponsiveLineコンポーネントにてNivoの設定をしていきます!
ここで注意ですが、MyResponsiveLine
の親要素には必ず height
を明示するようにしましょう!
Nivoの公式からコピペする
Nivoの公式ページからコードを丸ごとコピペします。これの設定を変更して、最終的な成果物作成を目指します。
今回dataは以下のものを使います。
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の型を見てみると
curve?:
| 'basis'
| 'cardinal'
| 'catmullRom'
| 'linear'
| 'monotoneX'
| 'monotoneY'
| 'natural'
| 'step'
| 'stepAfter'
| 'stepBefore'
このようになっているので、これらの値を指定することができそうです。
それぞれがどのようなものかは公式ページで確認できます。今回はcatmullRom
にします。
catmullRom
が何かについては以下の記事が参考になります。
グラフの色
ついでに線の色も変えておきましょう。 colors
で変更することができます。
colors
には”red”や”#FC60FF”などの文字列の他に、文字列の配列を渡すこともできます。複数の線を描画したい時は配列を渡すことで、すべての線の色を指定することができます。
curve="catmullRom"
colors={"red"}
余談ですが、Nivoには複数のカラーテーマが用意されており、これらを使うこともできます。
丸を消す
縦の線とグラフが交わるところに丸が表示されています。これは point
で管理されています。今回 point
は不要なので、point
に関係するものは削除します。またenablePoint={false}
を追記して、丸を使わないことを明示しておきましょう。これを設定しないと丸が残ってしまいます。
// 削除
pointSize={5}
pointColor={{ theme: "background" }}
pointBorderWidth={2}
pointBorderColor={{ from: "serieColor" }}
pointLabel="data.yFormatted"
pointLabelYOffset={-12}
// 追加
enablePoint={false}
軸の設定を変える
X軸の設定
設定はxScale
で行います。
xScale={{
type: 'point', // カテゴリデータとしてポイントを使用
min: 0, // 最小値(線形スケールの場合)
max: 10, // 最大値(線形スケールの場合)
stacked: false, // スタックしない
reverse: false, // 逆順にはしない
}}
このように、軸の最大値や最小値、逆順で表示するかなどを設定することができます。
今回はxScale={{ type: "point" }}
だけ指定します。この設定により、各ポイントが等間隔に配置されます。
pointの他にもlinearを指定することができます。
Y軸の設定
設定はyScale
で行います。設定の方法はxScale
と同様にオブジェクトを渡します。
今回は以下のように設定します。
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にします。
↓積み上げとは
axisの設定
それぞれの軸に表示する文字や説明を設定するのがaxisです。
現段階では軸の側にcountやtransportationと表示されていますが、これは不要なので消したいです。
この設定はaxisBottom
とaxisLeft
で行います。
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を0にして、renderTickで生成しています。<tetx>
はsvgの書き方です。
axisLeftの方ではメモリとして表示するものを指定しています。この内容とyScaleで指定した内容が合うように注意しましょう。
countやtransportationはlegends
というもので管理されています。今回legends
は不要なのでこれに関係するものは削除します。
また、グラフの説明としてグラフ外にcountと書かれたものがありますが、これもlegends
です。ついでに削除しておきましょう。
ツールチップを変更する
デフォルトのツールチップは以下のようになっており、少しダサいです。
なので独自のツールチップを設定しようと思います。以下をResponsiveLineコンポーネントに追加しましょう。
tooltip={({ point }: { point: Point }) => (
<StyledTooltip>
<div>
<TooltipGeneratedText>{point.data.yFormatted}円</TooltipGeneratedText>
</div>
{/* 下の吹き出し */}
<StyledTooltipArrow />
</StyledTooltip>
)}
StyledTooltipなどは以下のように定義しました。
-
スタイル
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; } `;
これで以下のようなツールチップになりました。
グラフの背景を変える
格子をなくす
デザイン上邪魔な場合は消すこともできます。
enableGridX={false}
enableGridY={false}
どちらか一方の線だけ消すこともできます。
背景に何かを表示する(応用?)
グラフの背景に文字を表示したりすることができます。今回はグラフの背景だけ色を変更するようにしてみます。
layers={[
"markers",
"axes",
"areas",
"lines",
"slices",
"mesh",
CustomLayer,
]}
layers
はどのような順番でlineなどを表示するか決めることができます。ここにカスタムコンポーネントを渡すことで、独自のコンポーネントを表示させることができます。
CustomLayer
は以下のように定義しました。
// グラフ部分の背景色
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にカスタムコンポーネントを渡すことで実現できます。
// グラフをクリックしたときに表示する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を消す
終わりに
最終的なコードはこちらです。
"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を検討してもいいかもしれませんね。