跳至主要内容

[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% 保證。

unitintervallabel.format:三層疊加的完整因果鏈

延續前面 unitinterval 的討論,再加上 label.format 之後,可以看出 AG Charts 畫時間軸其實是「由外而內」疊加的三個獨立步驟:

// 1. unit:軸本身的資料分桶大小
unit?: AgTimeIntervalUnit | {
unit: AgTimeIntervalUnit; // 'day' | 'month' | 'year' | ...
step?: number; // unit 的倍數,例如 unit: 'day', step: 7 → 每 7 天一格
epoch?: Date; // 對齊基準點
};

// 2. interval:tick 取樣頻率
interval?: {
step?: AgTimeInterval | AgTimeIntervalUnit | number; // 每隔多少個 unit 才畫一個 tick
values?: any[];
minSpacing?: PixelSize;
maxSpacing?: PixelSize;
};

// 3. label.format:tick 上文字外觀
label?: {
format?: Record<AgTimeIntervalUnit, string>; // 純粹是字串格式對照表
};

三者各自回答一個獨立的問題,彼此互不影響:

設定回答的問題影響範圍
unit(含 step資料在軸上要被切成多寬的「一格」?決定畫布上的空間佈局解析度——設 unit: 'day',代表無論怎麼縮放,底層永遠以「一天」為最小格子單位在排列資料
interval這些格子裡,隔幾格才真的畫一條刻度線+label?決定 tick 出現的疏密與位置;沒設就讓 AG Charts 依縮放範圍自動決定
label.format被選中畫出來的那個 tick,文字要寫成什麼樣子?純粹是顯示格式,跟前兩者「要不要有這個 tick」完全無關

用一句話串起因果:unit 先決定資料怎麼被切成一格一格的空間,interval 再從這些格子裡挑出哪幾格要真的畫上刻度,label.format 最後只負責幫被選中的那個刻度換上對應粒度的文字外衣。

實務上很常見的組合是:只設定第 1 層(unit)和第 3 層(label.format),第 2 層(interval)完全留給 AG Charts 自動判斷密度。label.formatRecord<AgTimeIntervalUnit, string> 讓不同粒度(daymonthyear...)各自對應一種文字格式,這樣即使 tick 的密度隨縮放改變,顯示出來的文字格式依然正確——這也是為什麼 parentLevel 底下也是用同一種 Record<AgTimeIntervalUnit, string> 格式在設定父層文字,兩者是同一套機制。

label.format 的格式字串:可用的時間/數字 directive

參考:AG Charts - Formatters(Format Strings)

前面範例中的 '%e\n%b''%b\n%Y',裡面的 %e%b%Y 都是官方定義好的「格式指令(directive)」,本質是 strftime 風格的時間格式語法。完整清單:

指令說明
%a縮寫的星期名稱*
%A完整的星期名稱*
%b縮寫的月份名稱*
%B完整的月份名稱*
%c語系慣用的日期時間,等同 %x%X*
%d補零的日期 [01,31]
%e補空白的日期 [ 1,31]
%f微秒 [000000,999999]
%H24 小時制的小時 [00,23]
%I12 小時制的小時 [01,12]
%j該年的第幾天 [001,366]
%m月份 [01,12]
%M分鐘 [00,59]
%L毫秒 [000,999]
%pAM 或 PM*
%Q自 UNIX epoch 起算的毫秒數
%s自 UNIX epoch 起算的秒數
%S秒數 [00,61]
%u以週一為起始的 ISO 星期數字 [1,7]
%U以週日為起始的當年第幾週 [00,53]
%VISO 8601 當年第幾週
%w以週日為起始的星期數字 [0,6]
%W以週一為起始的當年第幾週 [00,53]
%x語系慣用的日期格式,如 %-m/%-d/%Y*
%X語系慣用的時間格式,如 %-I:%M:%S %p*
%y兩位數年份 [00,99]
%Y完整年份
%Z時區偏移,如 -0700-07:00-07Z
%%字面上的 % 符號

(標 * 的指令會受語系設定影響)

也就是說前面 label.format 範例中的 { month: '%e\n%b', year: '%b\n%Y' },意思是:month 這個粒度顯示「補空白的日期 + 換行 + 縮寫月份名稱」(例如 3\nJul);year 這個粒度顯示「縮寫月份名稱 + 換行 + 完整年份」(例如 Jul\n2026)。

數字格式則遵循 [[fill]align][sign][#][0][width][grouping_option][.precision][type] 的語法結構:

  • align(對齊)> 靠右(預設)、< 靠左、^ 置中、= 符號靠左、數字靠右
  • sign(正負號)- 負數顯示減號(預設)、+ 正負號都顯示、( 負數用括號表示、空白 正數前補空白
  • type(型別)% 乘以 100 顯示百分比、b 二進位、d 十進位整數、e 指數記號、f 固定小數位、g 十進位或指數記號(自動選擇)、p 百分比(自動乘以 100)、r 十進位(依有效位數)、s 十進位加 SI 前綴、x / X 十六進位(小寫/大寫)

語法提醒:#{} 包裹規則只適用於數字格式,時間格式不需要。 官方文件是在 Number Formats 段落下特別註明:

Formats should be wrapped with #{} if included within a string so that it's clear where the number format begins and ends. For example: I'm #{0>2.0f} years old.

也就是說,#{} 是用來標示「數字格式指令」的起訖邊界,因為數字格式裡的 fill/width/precision 這些片段本身容易跟周圍的文字混淆;時間格式完全不受這條規則影響——因為時間指令都以 % 開頭,本身就有明確邊界,不管是不是跟其他文字(例如 \n 換行)混在一起都不需要包 #{}

{
formatter: {
x: '%b %Y', // 時間格式:不管有沒有跟其他文字混用,都不需要包 #{}
y: "I'm #{0>2.0f} years old", // 數字格式:混在文字中,格式的部分要用 #{} 包起來標出邊界
},
}

這也是為什麼前面 axis.label.formatparentLevel.label.format 範例中的 '%e\n%b' 這類時間格式字串,即使裡面有換行符號,也完全不需要 #{}

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

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

兩個 label.format 設定的是不同層級:軸本身的 label.format 設定的是主 axis 的刻度要怎麼顯示;parentLevel.label.format 設定的則是 parentLevel 呈現的樣子——兩組格式各自獨立,互不影響。

這個功能在 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 因為是照「格子」在算,最後一個有資料的格子就是終點,不會多留空白。