VolumeProfileChart
Production:src/components/AssetPrediction/source/VolumeProfileChart/VolumeProfileChart.tsx
Installation
import { VolumeProfileChart } from "@/components/product/volume-profile-chart/volume-profile-chart";Overview
現貨成交量依價格分布,橫向柱、價格由高到低。回答的是 「這個市場實際接受過哪些價格」。
柱身填的是半色調網點, 跟 DistributionChart 同一套語彙。 但這裡柱長保留 —— volume profile 的輪廓就是它的內容:節點在哪、value area 多寬、POC 突出多少。 DistributionChart 可以放掉柱高,是因為機率分佈是逐桶對著標籤讀的; profile 是當成一個形狀讀的,抽掉輪廓等於抽掉這張圖。
所以這裡是雙通道編碼:長度講一次成交量, 網點再講一次。刻意冗餘 —— POC 同時是最長也是最密的那一列,那正是它要主張的事。
- default —— 一般價格階(中性網點)
- va —— 落在 70% value area 內(較亮的中性網點)
- poc —— point of control,成交最集中的一階 (brand 橘網點 + 帶 tag)
版面是一個 CSS grid,不是一列一個 flex row。grid 讓每一列共用同樣的三欄 —— 價格 / 軌道 / tag —— 所以每一列的軌道寬度完全相同。 舊的做法讓帶 tag 的列軌道被擠窄,同一個成交量在 VAH / POC / VAL 那幾列會畫出比較短的柱子。 價格欄同理改成 max-content:原本固定 56px,價格字串長一點就會擠。
網點一律整顆。每一列的軌道各自量測、切成整數行,柱長再吸附到整數個格子, 所以柱子的收尾邊緣落在點與點之間、不會切穿任何一顆。 只量一次就夠 —— grid 保證每列軌道等寬,這是結構上的保證,不是假設。 代價是柱長被量化到約一個格子(軌道的 1% 上下),分桶資料負擔得起。
spot 線畫在夾住 spot 的兩階「之間」,不插值到某一階上。profile 是分桶資料,把 spot 畫進桶內等於宣稱一個桶子本身沒有的精度。
顏色契約(2026-07-28 由 user 裁決修訂): default 與 va 維持中性,POC 上 brand(--color-data-fill-focus)。 這裡的 brand 意思是「市場一直回到的那個價格」—— 是強調,不是極性。
成交量仍然沒有方向性:任何一列都不得使用 outcome 或 trend 顏色。 spot 下方有一根很重的柱子不叫「看空」,它只是說交易發生在那裡。 spot 線維持 --color-data-reference —— 它是參考記號,不是對資料的主張。
Interactive Demo
切換回看窗 —— 結構隨窗口改變,spot 相對位置跟著移動
65.6K
65.45K
VAH $65.45K
65.3K
spot $65,224
65.15K
POC
65.0K
64.9K
VAL $64.9K
64.75K
64.6K
24H:spot 就在 POC 上方一階。30D:結構明顯下重,spot 位在高位 —— 同一個 spot, 三個窗口講三種故事。
Static State Matrix
列狀態 — default / va / poc / 帶 tag
66.0K
65.5K
65.2K
POC
64.8K
VAL $64.8K
64.0K
spot 線 — 最上方 / 中間 / 最下方 / 不畫
spotAfterIndex=0(最上)
spot $65,224
65.6K
65.45K
VAH $65.45K
65.3K
65.15K
POC
65.0K
spotAfterIndex=2(中間)
65.6K
65.45K
VAH $65.45K
spot $65,224
65.3K
65.15K
POC
65.0K
spotAfterIndex=5(最下)
65.6K
65.45K
VAH $65.45K
65.3K
65.15K
POC
65.0K
省略(不畫 spot)
65.6K
65.45K
VAH $65.45K
65.3K
65.15K
POC
65.0K
細顆粒模式 — rowHeight=5 · 72 階(固定階高)
66.8K
VAH $66.1K
66.1K
65.3K
spot $65,224
POC
64.6K
VAL $63.9K
63.9K
63.1K
真實 volume profile 的價格階很窄、柱子數量很多(PM 2026-08-04 回饋)—— 輪廓要靠列數而不是列高。價格標籤每 12 階一個,錨點價格由 VAH / POC / VAL 的 tag 自己帶。上方粗顆粒形態保留給 tag 逐列可讀的小空間場景。
rowHeight="fill" — 階高由容器高度反推,跟旁欄齊底(dashboard 現行傳法)
模擬 stats 欄
h-[420px]
66.8K
VAH $66.1K
66.1K
65.3K
spot $65,224
POC
64.6K
VAL $63.9K
63.9K
63.1K
chart 在一個 h-full wrapper 裡以 absolute 定位填滿,所以 wrapper 的高度 完全由外面決定(這裡是左邊的模擬 stats 欄;dashboard 裡是 MetricReadout 欄),量到多高就把它均分給 72 階 —— 不論旁欄比較高或比較矮都齊底。 absolute 是關鍵:in-flow 的 chart 會自己撐出它要量的高度,形成回饋迴圈。 單欄版面沒有旁欄可對齊時,wrapper 的 min-height 200px 接住。
窄容器 / 無 tag / 極端寬度差
65.6K
65.45K
VAH $65.45K
65.3K
65.15K
POC
65.0K
64.9K
VAL $64.9K
64.75K
64.6K
66.0K
POC
65.5K
65.0K
Hover 任一價格階會亮起該列並顯示讀數面板(精確價格 · 成交量占比 · POC / value area / 之外)——細顆粒模式把逐列事實從軸上壓縮掉了,hover 把它們交還。無 pressed / focus, 面板 pointer-events-none 不攔截滑鼠。窗口切換屬於外層卡片(AnalysisCard 的 controls), 不是圖表自己的 state。
引擎對照 — div bars vs recharts
同一組 rows、同一套中性顏色契約,兩種畫法並排。上面各節用的都是現行 div 版。
注意:現行版柱身已改為半色調網點,recharts 版仍是實心柱, 所以左右不再只差引擎、也差了填充。下面關於 spot 線與軸標籤的發現與填充無關,仍然成立; 但要在 recharts 重現網點得改走 SVG <pattern>, 且每個實例要自己的 id,代價未列入下方清單。
24H — 注意 spot 虛線落在哪
65.6K
65.45K
VAH $65.45K
65.3K
spot $65,224
65.15K
POC
65.0K
64.9K
VAL $64.9K
64.75K
64.6K
7D — 注意 spot 虛線落在哪
67.0K
66.5K
66.0K
VAH $66.1K
65.5K
spot $65,224
65.0K
64.9K
POC
64.5K
64.0K
VAL $63.9K
63.5K
63.0K
30D — 注意 spot 虛線落在哪
67.0K
66.4K
VAH $66.8K
spot $65,224
65.6K
64.8K
64.0K
63.4K
POC
62.6K
61.9K
VAL $61.9K
61.0K
最關鍵的一點:spot 線
契約要求 spot 虛線必須畫在夾住它的兩階之間, 不可插值到某一階上 —— profile 是分桶資料,畫進桶內等於宣稱桶子沒有的精度。 recharts 的 ReferenceLine 在 category 軸上只會吸附到 band 的中心,沒有任何 API 能把它放到 band 邊界。
所以 recharts 版的 spot 線是手算位置的 overlay, 程式碼裡必須硬記 row 高度與上下 margin。這一筆記號上,庫完全沒有幫上忙, 而且從此多一個「改了幾何就會靜默跑掉」的耦合。
其餘差異
- 價格標籤與右側 tag 都變成 SVG
<text>,失去 CSS 排版能力 - 橫向柱在 recharts 要走
layout="vertical",軸的 type 對調,讀起來反直覺 - 換到的是整頁引擎統一,以及未來要加 hover 讀值時的現成掛點
Implementation Notes
版面與列結構來源:../opinion-labs-archive/prototypes/assets-forecasting/btc-dashboard-spike-v1.html 的 .vp / .vp-row / .vp-bar / .vp-price / .vp-tag / .vp-spotline。
半色調填充來源:10A 半色調 × L5 密度, 與 DistributionChart 同一組值(格距 3 × 3.4px 目標值,runtime 吸附)。 形態庫與未採用的替代方案見 docs/asset-forecasting/bar-chart-style-library.md。
缺口
- 網點格距與點徑比例(0.45–1.0)是視覺調校值、不是 token。 這裡的下限比
DistributionChart高(0.18), 因為長度已經分開各列、短列不需要再被網點淡到消失。 兩個組件已經是兩組值,應該抽成共用常數再各自 override 下限。 - 價格與 tag 走 Manrope +
tabular-nums, 不用font-number(Space Mono)。 依 T4 分級契約這是 tier-2 PASS,不是 fail。 - 列高 18px 只容得下 5 列網點。列高若再壓低,半色調會退化成雜訊 —— 縮圖情境需另外驗。
- 窄容器 + 有 tag 時,tag 欄會吃掉大量軌道寬度。上面 280px 那個例子實測軌道只剩約 50px(tag 欄佔 97px)。 這是 grid 修正的代價:所有列共用同寬軌道換來柱長可比較,但軌道總寬被最長的 tag 壓縮。 舊做法在窄容器一樣難讀,只是難讀的方式不同(各列軌道不等寬、柱長不可比)。 正解是窄寬度時讓 tag 掉到柱子下方自成一列 —— 量測基礎設施已經在了,尚未實作。不要試圖用
min()/clamp()在 grid track 裡設上限:瀏覽器會判定整條grid-template-columns無效並整個丟掉, grid 會無聲退回單欄。 - 價格欄已改為
max-content,長字串不再被擠; 但長度仍由呼叫端決定 —— production 有KMNumber做縮寫, 本 repo 沒 vendor,所以極長的價格字串會直接吃掉軌道寬度。 spotAfterIndex由呼叫端算。 正確做法是傳 spot 數值進來讓組件自己找夾住它的兩階, 但那需要 rows 帶真實數值而非顯示字串 —— 等接真資料時一起改。