跳轉到

座標軸註記

y 軸位於繪圖區左緣,x 軸位於下緣,兩軸外側的空間稱為邊欄。其中三種圖層配置在邊欄中,第四種則在繪圖區內畫出大括號。

標記、註記與大括號

AxisMarkLayer(
    axis,
    value,
    label,
    math=False,
    style=None,
    *,
    role="axes",
    ...
)

在軸 "x" 或 "y" 的 value 處、繪圖區外的短符號,例如 \(a_0\) 或 \(p^*\)。

AxisNoteLayer(
    axis,
    value,
    text,
    style=None,
    *,
    role="axes.note",
    ...
)

用來說明 value 的文字,可以分成多行,位於邊欄最外側的一欄。在預設主題中,axes.note 角色會讓註記比標記更小、更淡(9 pt、grey-600)。

BraceLayer(
    axis,
    start,
    end,
    label=None,
    side="inside",
    *,
    role="axes",
    math=False,
    style=None,
    stroke=None,
    ...
)

覆蓋軸上 start..end 的大括號,可加上標籤。side="outside" 會畫在邊欄中、標記之外;"inside" 則畫在繪圖區內側,標籤會避開所有線、點、區域與文字,尖端外沒有空位時便改用引線拉出。

SpanBraceLayer(
    start,
    end,
    label=None,
    side="below",
    *,
    role="axes",
    math=False,
    style=None,
    stroke=None,
    ...
)

繪圖區內兩點之間的大括號。跨距必須是水平(side 為 "above" 或 "below")或垂直("left" 或 "right");大括號會朝 side 凸出,與兩點相距 4 pt、深 8 pt。標籤位於尖端之外,並會避開其他所有內容(詳見區域標籤與點標籤)。

from mosaickit import (
    AxisMarkLayer,
    AxisNoteLayer,
    BraceLayer,
    Canvas,
    quadrant_axes,
)

canvas = Canvas().extend(quadrant_axes(10, 10))
for value, symbol, note in [
    (7, "a_1", "Upper\nvalue"),
    (5, "a_0", "Lower\nvalue"),
]:
    canvas.add(AxisMarkLayer("y", value, symbol, math=True))
    canvas.add(AxisNoteLayer("y", value, note))
canvas.add(BraceLayer("y", 5, 7, "Span", side="outside"))
canvas.add(AxisMarkLayer("x", 6, "b", math=True))

y 軸邊欄的標記、註記與大括號。

兩點之間的跨距大括號。

邊欄的欄位

各欄從軸線向外排列,第一欄與軸線相距 5 pt,各欄彼此相距 8 pt。順序依次是標記、每條外側大括號軌道各一欄,以及註記。每欄寬度取其中最寬的內容;空欄不占空間,也不增加間距(參見y 軸邊欄的標記、註記與大括號。,圖中另加了輔助線)。接著還要處理兩個問題:同一欄中相鄰的文字不能重疊,以及哪些大括號可以共用一條軌道。

沿軸分散文字

同一欄中的每段文字都對應到軸上的一個區間,並以其標示的值為中心。區間重疊時,系統會在維持順序的前提下將它們分開,並讓各項位移的平方和最小。

定義 · 保序排列

設 \(c_1, \ldots, c_n\) 為中心、\(s_1, \ldots, s_n \ge 0\) 為大小、\(g \ge 0\) 為間距,編號使 \(c_1 \le \cdots \le c_n\)(相等者依輸入順序)。排列是滿足

\[ x_{k+1} - x_k \ge (s_k + s_{k+1}) / 2 + g, \quad k = 1, \ldots, n - 1 \]

的向量 \(x \in \mathbb{R}^n\);保序排列問題是在所有排列中最小化 \(\sum_k (x_k - c_k)^2\)。

mosaickit.layout.stack1d.spread(
    centers,
    sizes,
    gap=0.0
) -> tuple[float, ...]

求解此問題:將連續且重疊的項目合併成群,每群緊密排列在所有成員目標位置的平均值周圍;只要某群碰到前一群,就再次合併。結果依輸入順序回傳。長度不一致或大小為負時會引發 ValueError。

定理 · 分散為最佳解

spread 回傳保序排列保序排列問題的唯一解。

這個程序採用保序迴歸的相鄰違反者合併演算法(pool-adjacent-violators algorithm,PAVA)Ayer (1955):扣除緊密排列的位移後,間距限制就會變成 \(y_1 \le \cdots \le y_n\)。

推論 · 分散結果的性質

設 \(x\) 為 spread 的結果,則 (i) 任兩項 \(i \ne j\) 滿足 \(|x_i - x_j| \ge (s_i + s_j) / 2 + g\); (ii) 若各中心本身已構成排列,則 \(x = c\); (iii) 每群的位移總和為零,因此群的平均位置等於其成員的平均目標。

目標(上)與分散結果(下)。

目標(上)與分散結果(下)。中的前三項因重疊而形成一群,群的位置以各項目標位置的平均值為中心;其餘兩項原本就已分開,因此不會移動。

大括號的軌道

跨距(連同標籤)彼此太近的大括號必須放在不同軌道,也就是與軸線不同的距離。

定義 · 衝突區間

對間距 \(g \ge 0\),若 \(b + g \le a'\) 與 \(b' + g \le a\) 都不成立,則區間 \([a, b]\) 與 \([a', b']\) 衝突。軌道指派為每個區間指定一個軌道編號,使衝突的區間不共用軌道。

mosaickit.layout.stack1d.assign_lanes(
    intervals,
    gap=0.0
) -> tuple[int, ...]

首次適配:依輸入順序處理區間,把每個區間放進與已有區間都不衝突的最低軌道。端點可依任一順序給出。

定理 · 首次適配使用最少軌道

assign_lanes 一定回傳軌道指派。若區間依下端遞增的順序給出,且每個都滿足 \(b - a + g > 0\),則它恰好使用 \(\omega\) 條軌道,其中 \(\omega\) 為兩兩衝突的區間數的最大值;沒有任何軌道指派能用得更少。

若輸入不是上述順序,首次適配可能使用超過必要數量的軌道。因此,可能重疊的大括號應依數值由低到高加入。

mosaickit.layout.gutter.gutter_columns(
    mark_width,
    brace_widths,
    note_width,
    *,
    start,
    gap
)

負責邊欄欄位配置的純函式:回傳 GutterColumns,其中包含 marks、braces(每條軌道一個 Band,由內而外)與 notes。每個欄位都是從軸線向外量得的 Band(near, far),另有最遠邊緣 extent。

留言