RechartsのBrushのラベルを余白に応じて内側/外側に自動切り替えするようにしてみた
はじめに
RechartsではBrushコンポーネントを使うことによって、チャートの範囲選択機能を手軽に追加できます。
デフォルトでは選択範囲の開始/終了ラベルがトラベラー(範囲指定するバーのつかむ部分)の外側に表示されますが、選択範囲を端に寄せるとラベルがチャート領域からはみ出てしまいます。
状況によっては、この仕様が不都合になることもあります。
この記事では、トラベラーの外側にスペースがあれば外側に、なければ内側にラベルを自動配置するカスタム方法を紹介します。
デフォルトの挙動
Brushのラベルはデフォルトではトラベラーの外側に表示されます。
選択範囲を端に寄せると、ラベルが領域外に出てしまいます。
この画像の終了ラベルのように、ラベルが見切れてしまう状況では、この仕様は不都合なので直したいです。

改善策
余白に応じて自動でラベルの表示位置がトラベラーの内側/外側に変わるようにします。
各ラベルを独立して判定し、「左は外側、右は内側」のように、左右それぞれのラベルが異なる状態を持てるようにします。
使用する技術
Rechartsでは、チャート内部の座標情報にアクセスできるhookが公開されています。
| hook | 返す値 | 用途 |
|---|---|---|
| usePlotArea() | { x, y, width, height } | チャートのデータ描画領域の位置とサイズ |
| useChartHeight() | number | チャート全体の高さ |
| useMargin() | { top, bottom, left, right } | チャートのmargin |
これらのhookはBarChart等のチャートコンポーネントの子孫コンポーネント内で使用できます。
実装例
定数の定義
const BRUSH_HEIGHT = 30 // Brushコンポーネントのheight
const TRAVELLER_WIDTH = 5 // トラベラー幅
const LABEL_WIDTH = 55 // ラベル1つ分のおおよそのピクセル幅
LABEL_WIDTHは「外側に表示して良いか」の閾値になります。今回の例では55pxにしていますが、表示する文字列の内容やフォントサイズに合わせて適宜変更します。
その他の定数も環境に応じて変えるものです。
BrushLabelsコンポーネント
デフォルトのラベルを非表示にして、オリジナルのラベルを表示するBrushLabelsコンポーネントを作成します。
const BrushLabels = ({
data,
startIndex,
endIndex,
}: {
data: { yearMonth: string }[]
startIndex: number
endIndex: number
}) => {
const plotArea = usePlotArea()
const chartHeight = useChartHeight()
const margin = useMargin()
if (!plotArea || !chartHeight || !margin) return null
// 選択範囲をデータ件数で等分し、トラベラーの左端・右端のX座標を求める
const pointWidth = data.length > 0 ? plotArea.width / data.length : 0
const startX = plotArea.x + startIndex * pointWidth
const endX = plotArea.x + (endIndex + 1) * pointWidth
// BrushのY軸の中央座標
const labelY = chartHeight - margin.bottom - BRUSH_HEIGHT / 2
// 選択範囲の外側の余白を計算
const startOuterSpace = startX - plotArea.x
const endOuterSpace = plotArea.x + plotArea.width - endX
// 余白が足りなければ内側に配置
const startLabelInside = startOuterSpace <= LABEL_WIDTH
const endLabelInside = endOuterSpace <= LABEL_WIDTH
// 外側に余白があれば外側、なければ内側にラベルを配置
const startLabelX = startLabelInside
? startX + TRAVELLER_WIDTH + 3
: startX - 3
const startAnchor = startLabelInside ? 'start' : 'end'
const endLabelX = endLabelInside
? endX - TRAVELLER_WIDTH - 3
: endX + TRAVELLER_WIDTH + 3
const endAnchor = endLabelInside ? 'end' : 'start'
return (
<g pointerEvents="none">
<text
x={startLabelX}
y={labelY}
textAnchor={startAnchor}
dominantBaseline="middle"
fontSize={10}
fill="#333"
>
{data[startIndex]?.yearMonth}
</text>
<text
x={endLabelX}
y={labelY}
textAnchor={endAnchor}
dominantBaseline="middle"
fontSize={10}
fill="#333"
>
{data[endIndex]?.yearMonth}
</text>
</g>
)
}
解説
座標計算の仕組み
ラベルを配置するには、BrushのトラベラーのX座標を知る必要があります。rechartsはバーをデータ描画領域に等間隔で並べるため、usePlotArea()で取得した幅をデータ件数で割れば、1データあたりの幅が求まります。Brushも同じ等間隔の区切りにトラベラーを配置するので、この幅に選択中のインデックスを掛ければトラベラーのX座標が計算できます。
内外判定のロジック
startOuterSpaceは開始トラベラーから選択範囲の左端までの距離です。これがLABEL_WIDTH以下なら外側に表示するとはみ出るので、内側に切り替えます。終了ラベルも同様に右端との距離で判定します。
pointerEvents="none"
カスタムラベルのSVG要素がマウスイベントを拾ってしまうと、Brushのドラッグ操作を邪魔します。pointerEvents="none"でイベントを透過させています。
textAnchorの切り替え
SVGのtextAnchorは、テキストの基準点がどこかを指定します。
- 外側(左に表示):
textAnchor="end"→ テキスト右端がx座標に揃う - 内側(右に表示):
textAnchor="start"→ テキスト左端がx座標に揃う
親コンポーネントでの使用
const Chart = ({ data }: { data: { yearMonth: string; value: number }[] }) => {
const [startIndex, setStartIndex] = useState(0)
const [endIndex, setEndIndex] = useState(data.length - 1)
return (
{/* デフォルトのBrushラベルをCSSで非表示 */}
<div className="[&_.recharts-brush-texts]:hidden">
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data}>
<XAxis dataKey="yearMonth" />
<YAxis />
<Bar dataKey="value" fill="#3B82F6" />
<Brush
dataKey="yearMonth"
height={30}
onChange={({ startIndex: si, endIndex: ei }) => {
if (si != null && ei != null) {
setStartIndex(si)
setEndIndex(ei)
}
}}
/>
{/* カスタムラベルをBarChart内に直接配置 */}
<BrushLabels
data={data}
startIndex={startIndex}
endIndex={endIndex}
/>
</BarChart>
</ResponsiveContainer>
</div>
)
}
デフォルトラベルの非表示
今回はBrushがデフォルトで描画するラベルを自前のBrushLabelsで置き換えるので、デフォルトのラベルはCSSで非表示にします。デフォルトラベルにはrecharts-brush-textsというクラスが付いているので、これを指定します。
.recharts-brush-texts {
display: none;
}
Tailwind CSSを使っている場合は、ラッパー要素に以下のクラスを追加します。
<div class="[&_.recharts-brush-texts]:hidden">
完成イメージ



まとめ
ということで、Brushのラベルのカスタム方法でした。
ぜひ使ってみてください。








