跳至主要内容

[note] ag-charts

Time Axes

參考:AG Charts - Time Axes

總覽:三種 time 相關軸型別

const options = useMemo<AgChartOptions>({
axes: {
x: {
type: "time",
},
},
});

核心差異在於「怎麼決定每個資料點在 x 軸上的位置」。用一個例子最容易看出差異:假設資料在週五有一筆、下一筆要等到週一(跳過週六日)。

1. type: 'time'(Continuous Time Axis)—— 連續數線,位置 = 真實時間距離

把時間值當成一般的連續數字軸來畫,性質上跟數值軸(number axis)一樣,都是用來畫連續值。週五跟週一之間,物理距離就是「兩天」,跟週一到週二的距離(一天)不一樣寬。如果資料中間空了兩個月沒有值,軸上就會出現一大段視覺上的空白,忠實反映「這段時間真的沒發生事」。

  • 使用時機:需要在連續時間尺度上如實呈現資料,讓資料點之間的間距忠實反映實際時間差距,是最常見、預設的 x 軸型別。
  • 對應的 Options 介面AgTimeAxisOptions

2. type: 'unit-time'(Unit Time Axis,目前使用的型別)—— 固定格子,但格子仍對應真實曆法

每個 unit(如一天、一個月)在軸的定義域裡都有專屬的一個 band,彼此均勻間隔排列——不論實際時間跨度或是否缺資料。例如以「月」為單位時,1 月 1 日跟 3 月 30 日一樣會被畫成三個等寬間隔的項目。即使某一天沒有資料,那天的格子還是存在、還是占位置——只是格子寬度統一了,不會像 time 軸那樣因為單一資料點的實際時間差而拉伸或壓縮。

  • 使用時機:適合「每個 unit 剛好對應一筆資料值」的情境,尤其是週期性資料(例如週資料可設 step: 7)。
  • 對應的 Options 介面AgUnitTimeAxisOptions

3. type: 'ordinal-time'(Ordinal Time Axis)—— 完全忽略時間間隔,只看「有資料的點」

這個最特別:它把 x 軸當成類別軸(ordinal)在處理,資料點會依照它們在時間上的先後順序排列,但完全忽略彼此之間實際的時間間隔。週五跟週一之間如果沒有週六日資料,畫出來會直接無縫接在一起,看起來就像「連續的兩天」,缺資料的那段不會產生任何視覺上的空隙。

  • 使用時機:官方文件指出這種型別「常用於金融資料的 x 軸」,尤其適合資料點本來就分佈不均勻的場景——股票交易圖偏好用它的原因正是非交易日(週末、假日)不該在圖上留下一段沒意義的空白。
  • 對應的 Options 介面AgOrdinalTimeAxisOptions

簡單對照

型別位置依據缺資料的那天/那段常見使用時機
time真實時間距離留白,寬度等比例反映時間長度需要如實呈現時間間距的一般時序資料
unit-time固定曆法格子格子還在,但寬度固定、不佔額外空間每個 unit 剛好一筆值的週期性資料
ordinal-time只看資料點的順序完全消失,前後資料點直接相鄰金融/交易資料,資料點分佈不均勻

unit-time 軸的設定細節

const options = useMemo<AgChartOptions>({
axes: {
x: {
type: "time",

// unit 可以設定字串或物件
// unit: 'day', // every day
unit: {
unit: "day",
step: 7,
},
},
},
});

unit(字串形式)

用在 type: 'unit-time' 軸上,決定時間軸切成多細的固定格子(band)。型別定義是:

unit?: AgTimeInterval | AgTimeIntervalUnit;

也就是說 unit 可以吃兩種型別:

  • AgTimeIntervalUnit:字串形式,直接指定單位:

    export type AgTimeIntervalUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year';
  • AgTimeInterval:物件形式,除了 unit 之外還能再搭配 stepepoch 做更細的控制(詳見下方 unit.stepunit.epoch 說明)。

  • 跟一般 type: 'time' 不同:time 軸的刻度間距是自動湊出來、不保證對齊有意義的邊界;unit-time 則是固定大小的格子,放大再多也不會比指定的 unit 更細,軸的終點也會停在最後一筆資料,不會被自動湊整超過去。

  • 範例:type: 'unit-time', unit: 'day' —— 每一格固定是 1 天。

unit.step(物件形式的 unit

當需要把 unit 變成物件時,語法是:

unit: {
unit: 'day', // 基準單位
step: 7, // 幾個基準單位算一格
}
  • step 是「基準單位的倍數」,決定每一格實際涵蓋多少範圍。例如 { unit: 'day', step: 7 } = 每格 7 天(週分組)。
  • 注意:{ unit: 'day', step: 30 } 不等於 unit: 'month'——前者是固定 30 天一格,後者是跟著日曆走、長度會變動(28~31 天)。要按月分組永遠該直接用 unit: 'month',不要用 daystep 去湊。

unit.epoch

  • 只有在 step > 1 時才有意義;step: 1(或沒設 step)時完全不影響結果,因為 1 個單位的整數倍,不管從哪個基準點開始算,都會落在同一組邊界上。
  • 它是一個「錨點(anchor)」,用來決定格線要對齊在哪個基準日,不是「資料從這天開始算」。AG Charts 會以 epoch 為基準,往前、往後各自用 step 的間距延伸出整條格線,直到覆蓋整段資料範圍——epoch 本身可以落在資料範圍內、之前、或之後,都沒差。
  • 實務用途:例如週分組時,用 epoch 指定一個「剛好是週一」的日期,就能讓所有格線都對齊到週一開始,而不是被內部預設的基準點決定成別的星期幾。
範例:資料範圍是 2026-04-01 ~ 2026-07-01,但 epoch 設成 2026-05-03

只有 step > 1 時,epoch 才真正產生效果。AG Charts 不會管 2026-05-03 是不是在資料的「開頭」,它只把 2026-05-03 當成一個基準點,然後往前、往後各自用「每 7 天」延伸出整條格線,直到蓋過整段資料範圍。

{ unit: 'day', step: 7 } 搭配 epoch: '2026-05-03' 為例,實際畫出的格線會是(往前、往後各自以 7 天為間距展開):

… 2026-03-29 → 2026-04-05 → 2026-04-12 → 2026-04-19 → 2026-04-26
→ 2026-05-03(epoch)
→ 2026-05-10 → 2026-05-17 → 2026-05-24 → 2026-05-31 → 2026-06-07 → 2026-06-14 → 2026-06-21 → 2026-06-28 → 2026-07-05 …

可以看到 2026-05-03 落在資料範圍中間,並不是資料的起點,但格線仍然是以它為錨點,往兩端展開到蓋過 2026-04-012026-07-01 為止——這就是「epoch 只是錨點,不是資料的起算點」的具體效果。

unit vs interval:設定內容雖然相似,作用的地方完全不同

unit 決定的是資料在底層怎麼「分箱」(bucketing)——每個 band 多寬、有幾個資料點落進同一格;interval 完全不碰資料分箱,只決定畫面上「挑哪些位置畫刻度線/格線/標籤」,是純粹的顯示層行為。換句話說,就算把 interval 拿掉或改變數值,底層的資料分組與繪圖區塊大小完全不會變,變的只是視覺上看起來多密或多疏。

unitunit-time 專屬)interval(所有軸類型通用)
作用層級資料層:決定每個 band/bucket 的大小顯示層:決定刻度線/格線/標籤的間距
影響什麼資料點如何被分組、band 的寬度、軸的定位方式畫面上顯示哪些刻度、多密
適用軸類型只有 type: 'unit-time'timeunit-timeordinal-time 都可以用
拿掉會怎樣整個軸的分箱邏輯消失(unit-time 甚至不能沒有它)只是回到 AG Charts 自動決定刻度密度,資料呈現不受影響

用一句話總結:unit 決定「資料怎麼被切成一格一格」,interval 決定「這些格子(或連續軸上的位置)要每隔幾格才畫一條刻度線」——兩者互不干涉,甚至可以在 unit-time 軸上同時設定 unit: 'day'(每天一個 band)加上 interval: { step: 7 }(但只在畫面上每 7 天才顯示一條刻度線標籤)。

interval 是建議值,不是強制值

如果沒有設定 interval,刻度的顯示會隨著使用者縮放而改變(AG Charts 自動依可視範圍計算出合適的密度);但如果設定了 interval,刻度的呈現則會盡可能依照 interval 的設定來畫。

不過這個「盡可能」是有保留的,型別定義上寫得很清楚:

/** The axis interval. Expressed in the units of the axis.
* If the configured interval results in too many items
* given the chart size, it will be ignored. */
step?: T;

也就是說,interval.step 是一個建議值,不是強制值。如果畫布真的很小、塞不下那麼多天的刻度標籤,AG Charts 還是會自動放棄設定的 step: 'day'、改成自己算出來的疏一點的間距,避免標籤全部疊在一起看不清楚——所以嚴格來說「絕不自動變疏」這個目標,光靠 interval: { step: 'day' } 還不能 100% 保證。

parentLevel:顯示更粗一層的時間分組

  1. parentLevel 顯示的是比目前單位更粗一層的時間分組(day → 顯示 monthmonth → 顯示 year)。
  2. 父層標籤預設會用粗體呈現,跟主刻度做視覺上的區隔。
  3. 縮放時父層會跟著動態切換(例如原本是 day/month 這組,縮小後可能自動換成 month/year)。

這個功能在 type: 'unit-time' 軸上是預設啟用的;在 type: 'time'type: 'ordinal-time' 軸上則需要自行選擇加入(opt-in)。基本設定方式:

{
parentLevel: {
enabled: true,
},
}

以預設設定呈現時:資料以 day 為單位顯示,父層的 month 會以粗體呈現;當縮小(zoom out)時,主刻度標籤會換成 month,父層則跟著換成 year

父層的標籤與刻度樣式預設會繼承主軸的設定,但也可以透過 parentLevel 個別覆寫,例如針對不同時間單位分別指定顯示格式:

{
parentLevel: {
enabled: true,
tick: { width: 1 },
label: {
format: {
month: '%e\n%b',
year: '%b\n%Y',
},
},
},
}

套用這組客製化設定後:

  • 軸本身(非 parentLevel)的 label.format 若另外設定,可以讓 day 這個層級顯示「日 + 月」,month 這個層級只顯示「月」——這是主刻度自己的格式設定,跟下面 parentLevel 裡的格式是分開的兩組設定。
  • parentLevel.tick: { width: 1 } 把父層的刻度線寬度設為 1,讓父層的刻度線可以被顯示出來。
  • parentLevel.label.format 裡的 monthyear 這兩個 key,代表的是父層自己顯示的單位:父層顯示 month 時,用 %e\n%b 換行分兩行顯示「日、月」;父層顯示 year 時,用 %b\n%Y 換行分兩行顯示「月、年」。

補充:timeunit-time 的更多差異

差異 1:同一天內有兩個不同時間點,兩種軸的處理方式不同

假設同一個 series 裡有兩筆資料:2024-01-01T08:002024-01-01T20:00

  • type: 'time'(連續軸):這兩個時間點會被畫在不同的位置上,因為它是照精確的時間值定位,8 點跟 20 點之間有 12 小時的差距,位置就會反映這 12 小時。
  • type: 'unit-time' + unit: 'day':這兩筆資料會被歸進同一個「天」的格子裡——因為 unit-time 是先把時間切成一格一格的桶(bucket),資料點是被「分類」進某一格,而不是照精確時間定位。

差異 2(更重要):unit-time 不會幫你聚合資料,所以同一個 series 在同一個 unit 裡只能有一筆值

官方文件明確提到這點:

The Unit Time Axis does not aggregate data, so you should ensure that each series only has one value per unit.

意思是:如果某個 series 在同一天(同一個 unit)裡真的丟了兩筆資料進去,AG Charts 不會幫你算平均或加總,行為是未定義/不保證正確的——這個限制在 type: 'time' 上完全不存在,因為 time 軸每個資料點都有自己精確的位置,本來就不需要「同一格只能一筆」這種限制。

這也解釋了為什麼前面會提到「軸終點停在最後一筆資料,不會被自動湊整超過去」的現象:time 軸的自動刻度計算,有時候會為了「湊出好看的整數刻度」而把可視範圍延伸到超過最後一筆資料的時間點,中間就會留下一段沒有意義的空白;而 unit-time 因為是照「格子」在算,最後一個有資料的格子就是終點,不會多留空白。