Design SystemPatternsAssetDistributionChart

DistributionChart

Adopted在本 repo 設計、production 已採用。站上是設計端的版本,production 實作可能已再調整。

Production:src/components/AssetPrediction/source/DistributionChart/DistributionChart.tsx

Installation

import { DistributionChart } from "@/components/product/distribution-chart/distribution-chart";

Overview

某一個結算日底下、互斥區間市場家族的機率曲線 —— 從各市場可成交中價推出來的。 回答的是「市場認為 BTC 在 8/29 會落在哪」。

形態是半色調柱狀(halftone bars):機率編碼兩次—— 一次是柱高,一次是柱身網點的大小。 與 VolumeProfileChart 同一套雙通道,理由也一樣:分佈的輪廓本身就是它要講的事。

中間有一版拿掉柱高、只留密度(等高版塊),2026-07-28 由 user 裁決把柱高放回來—— 密度是比較弱的通道,人眼比長度準、比濃淡不準,所以分佈的形狀必須留在長度裡。 網點負責的是「不要長得像每一張 dashboard 柱狀圖」,長度負責的是「讀得懂」。 另外每個區間都保留百分比標籤,貼在自己那根柱子上方:數字是值,材質與長度是形狀。

四種區間況各自帶一個主張:

  • default —— 一般區間
  • modal —— 機率最高的那一塊,網點大到幾乎相接、且上 brand 橘。 讀者最想要的那個答案
  • thin —— 掛在薄流動性上。斜線從網點裡挖掉, 不是畫在底板上:底板斜線會被高密度網點蓋住, 那樣 modal + thin 會看起來就只是 modal。數字是真的但背後的信心不是,不隱藏、不抹平。
  • selected —— 這個結算日的 featured market 落在哪, 軸上用 ▲ 標。這是使用者的當前脈絡,不是資料的性質, 所以它是唯一一處非中性的墨水。

顏色契約(2026-07-28 由 user 裁決修訂): 底板與一般網點維持中性(--color-data-fill / --color-data-ink——網點墨色的專屬 role, 不再借序列節點的 data-point);modal 上 brand(--color-data-fill-focus)。

這裡的 brand 意思是「這就是答案」,不是「這是好事」—— 是強調,不是極性。機率分佈仍然沒有極性:任何區間都不得使用 outcome 或 trend 顏色, 高於 spot 的區間不等於「好」。

已知撞色:▲ 選取記號也是 brand。 兩者落在不同記號上(底板 vs 軸上字符),但橘色在同一張圖裡現在承載兩個意思。 若實際使用覺得混淆,應該動的是 ▲(改中性或改空心字符), brand 留給 modal。

網點一律整顆。格寬 = 柱寬 ÷ 整數行、格高 = 版高 ÷ 整數列,都在 runtime 量測後吸附, 所以邊緣不會出現被切一半的點。這也是它需要 ResizeObserver、 不能只靠一組靜態 CSS background 的原因。

Interactive Demo

切換結算日 —— 曲線形狀跟著變(近的窄、遠的平)

4%
8%
19%
34%
20%
10%
5%

<$58K

58–62K

62–66K

66–70K

70–74K ▲

74–78K

>$78K

Jul 31 只剩幾天 → 曲線集中,modal 47%。Aug 29 中間隔著三個總經事件 → 攤平,modal 只有 34%, 且高價端兩根是薄書(斜線)。

Static State Matrix

柱況 — default / modal / thin / selected

20%
34%
18%
22%
30%

default

modal

thin

selected ▲

modal+thin

modal + thin 併發時斜線優先 —— 「最高機率但書很薄」比「最高機率」重要, 不能只讓它看起來最實。這正是斜線必須從網點挖掉、而不是畫在底板的理由: modal 的網點大到幾乎相接,底板上的任何紋路都會被它蓋掉。

區間數 — 4 / 7 / 11(軸標籤在窄欄會截斷)

4%
14%
33%
14%

60–62K

62–64K

64–66K

66–68K

4%
7%
17%
31%
31%
17%
7%

60–62K

62–64K

64–66K

66–68K

68–70K

70–72K

72–74K

4%
5%
9%
16%
25%
32%
32%
25%
16%
9%
5%

60–62K

62–64K

64–66K

66–68K

68–70K

70–72K

72–74K

74–76K

76–78K

78–80K

80–82K

窄容器 / 高度變化

3%
11%
47%
26%
9%
4%

<$62K

62–64K

64–66K

66–68K

68–70K

>$70K

3%
11%
47%
26%
9%
4%

<$62K

62–64K

64–66K

66–68K

68–70K

>$70K

Edge case — 單一區間 100% / 全部極低

100%

64–66K

1%
2%
1%

a

b

c

點徑是相對最大值而非絕對 100% —— 全部都很低時仍看得出彼此差異。 單一區間 100% 時整版接近實心,這是對的:沒有別的答案可以比。

唯讀顯示層,無 hover / pressed / focus。選中態由 selected prop 從外部驅動(父層知道哪個 market 是 featured),不是圖表自己的互動。

引擎對照 — CSS 半色調 vs recharts 柱狀(已非等價)

這一節已經不是等價比較。現行版本改成半色調之後,左右兩邊差的不只是引擎,還差了形態 —— recharts 那版仍然是原本的柱狀圖。並排留著是為了看得到兩種形態的差別, 以及下面那條軸標籤截斷的發現(那條與形態無關,仍然成立)。

真正的引擎裁決還沒做:半色調的網點是 CSS mask, recharts 這邊要重做等價效果得改走 SVG <pattern>, 成本與現在這份 recharts 柱狀版不同,不能沿用下面那份代價清單直接下結論。

Aug 29 — 含 modal / selected / thin 三種柱況

div bars(現行)
4%
8%
19%
34%
20%
10%
5%

<$58K

58–62K

62–66K

66–70K

70–74K ▲

74–78K

>$78K

recharts

窄容器 280px — 軸標籤截斷行為的差異最明顯

div bars
4%
8%
19%
34%
20%
10%
5%

<$58K

58–62K

62–66K

66–70K

70–74K ▲

74–78K

>$78K

recharts

div 版走 CSS truncate,會在真正放不下的字上加省略號; recharts 版的軸標籤是 SVG <text>,沒有 truncation, 只能用手算的字數上限硬切。實測(1280 視窗、238px 繪圖區):recharts 版七個標籤共需約 299px, 相鄰標籤 6 對全部互相重疊,因為字數上限(9 字)沒被觸發、但像素放不下 —— 字數不是寬度。 要真的修好得在 tick renderer 裡量文字寬度,或改用 <foreignObject> 把 CSS 借回來。

11 個區間 — 柱寬與標籤密度

div bars
4%
5%
9%
16%
25%
32%
32%
25%
16%
9%
5%

60–62K

62–64K

64–66K

66–68K

68–70K

70–72K

72–74K

74–76K

76–78K

78–80K

80–82K

recharts

改成 recharts 換到什麼

  • 整頁同一個引擎(AssetSparkline、N1 timeline 都是 recharts)
  • 之後若要加 tooltip / hover 讀值,有現成的掛點
  • 真正的 y 尺度(現在 div 版是「相對最高柱的百分比」,此密度下視覺結果相同)

代價

  • 斜線紋從一個 Tailwind class 變成 <defs> 裡的 SVG pattern,且每個實例要自己的 id(useId),否則同頁兩張圖會撞
  • 軸標籤失去 CSS truncation(見上方 280px 對照)
  • ▲ 選中記號得寫進 custom tick renderer
  • 顏色從 class name 移進 JS 字串的 fill= 值
  • 程式碼行數約為 div 版的 1.7 倍,做的是同一件事

production 這邊沒有可對齊的對象 —— Ethan repo 裡一根柱狀圖都沒有(只有 line 與 area),所以這不是「回到 production 做法」,兩版都是我們自己的發明。

Implementation Notes

原始柱狀版來源:../opinion-labs-archive/prototypes/assets-forecasting/btc-dashboard-spike-v1.html 的 .dist / .bucket / .bar / .pct / .dist-x。

現行半色調形態來源:../opinion-labs-archive/prototypes/assets-forecasting/ 底下的三份探索頁 —— bar-chart-exploration.html(十種形態)、bar-chart-dot-halftone-variants.html(點陣與半色調各六個變體)、bar-chart-dot-density-ladder.html(密度五階)。 現行值=10A 半色調 × L5 密度(格距 3 × 3.4px 目標值,runtime 吸附)。 未採用的形態整理在 docs/asset-forecasting/bar-chart-style-library.md。

為什麼用 div 不用 recharts

本 repo 已經有 recharts 依賴,但這裡的記號就是「軸對齊的矩形,高度是百分比」—— CSS 原生就做這件事。recharts 會自己畫 SVG chrome、自己管顏色 prop, 我們得再一層層覆蓋回 token;換來的是多一層依賴與少一層 token 控制權。

缺口

  • 斜線紋是 repeating-linear-gradient(角度與間距 3px/6px 為原型值), 現在以 mask 層的形式寫在組件常數裡。若之後有第二個地方要用「薄流動性」紋路, 應抽成 utility 或 token,不要各處各寫一份。
  • 網點格距(3 × 3.4px)與點徑比例(0.18–1.0)是視覺調校出來的數字,不是 token。VolumeProfileChart 已是第二個消費者但用不同的點徑下限, 這組值應該抽出去成為共用常數。
  • 圖上的百分比走 Manrope + tabular-nums, 不用 font-number(Space Mono)—— 圖表自己的數字讀起來是卡片行文的一部分,不是 order book 那種數字欄。 依 T4 分級契約這是 tier-2 PASS,不是 fail。
  • 密度編碼在極小尺寸下未驗證 —— trending 卡內嵌那種高度,網點可能碎成雜訊。真要放進小卡前需另外量一次。
  • ▲ 標記目前是文字字元。production 若有標準的「當前選中」記號應改用它。