hgg-core-0.2.0.0: src/Graphics/Hgg/Spec/Theme.hs
-- |
-- Module : Graphics.Hgg.Spec.Theme
-- Description : theme presets (ThemeName), series palettes, and per-element overrides
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: 'Graphics.Hgg.Spec' の module 分割で切り出し。 描画 theme の名前
-- ('ThemeName')、 preset ごとの series palette、 named palette (Okabe-Ito 等)、
-- element 単位の上書き ('ThemeOverride'、 ggplot theme(element_*) 相当) を持つ。
-- 公開 API は従来どおり 'Graphics.Hgg.Spec' (facade) が re-export する。
-- 挙動・出力は完全に不変。
--
-- [English]: Split out from 'Graphics.Hgg.Spec' during a module split.
-- Holds the theme name ('ThemeName'), the series palette for each preset,
-- named palettes (Okabe-Ito etc.), and per-element overrides
-- ('ThemeOverride', equivalent to ggplot's theme(element_*)). The public
-- API is still re-exported by 'Graphics.Hgg.Spec' (the facade) as before.
-- Behavior and output are entirely unchanged.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
module Graphics.Hgg.Spec.Theme
( -- * theme preset + palette
ThemeName(..)
, themeSeriesPalette
, okabeIto, tolBright, brewerSet2, brewerDark2
-- * element 単位 override
, ThemeOverride(..)
, TickDir(..)
, Margin(..)
) where
import Data.Aeson (FromJSON, ToJSON)
import Data.Monoid (Last (..))
import Data.Text (Text)
import GHC.Generics (Generic, Generically (..))
import Graphics.Hgg.Spec.Decoration (FontSpec, LegendPosition)
-- | [日本語]: 描画 theme (= 名前で参照、 関数を持たない = JSON serializable)。
-- ggplot 標準 preset (ThemeGrey) + ブランドテーマを追加。
--
-- * ThemeGrey = ggplot 既定 theme_grey (灰背景 #EBEBEB・白 grid・枠なし・軸線なし)
-- * ThemeNoir = ブランド (暗・上品・寒色アクセント、 コンペ用に残置)
-- * ThemeLumen = ブランド (白基調・深い差し色・清潔、 コンペ用に残置)
-- * ThemeParchment = 羊皮紙基調・明の正式テーマ。 配色は cream/gold/ink +
-- Universal Categorical series 由来。
-- * ThemeParchmentDark = 同テーマの暗版 (焦茶インク背景・series は shade 300 で沈み防止)。
--
-- [English]: A rendering theme (referenced by name; carries no functions,
-- so it is JSON-serializable). Adds ggplot's standard preset (ThemeGrey)
-- plus the brand themes.
--
-- * ThemeGrey = ggplot's default theme_grey (grey #EBEBEB
-- background, white grid, no border, no axis line)
-- * ThemeNoir = brand theme (dark, elegant, cool accents;
-- kept around from an earlier competition entry)
-- * ThemeLumen = brand theme (white-based, deep accent
-- colors, clean; kept around from an earlier competition entry)
-- * ThemeParchment = the official parchment-based light theme.
-- Colors come from cream/gold/ink + the Universal Categorical
-- series.
-- * ThemeParchmentDark = the dark variant of the same theme (dark
-- umber ink background; series colors use shade 300 to avoid
-- getting lost against it)
data ThemeName = ThemeDefault | ThemeMinimal | ThemeDark | ThemeLight
| ThemeGrey | ThemeBW | ThemeClassic | ThemeVoid | ThemeLinedraw
| ThemeNoir | ThemeLumen
| ThemeParchment | ThemeParchmentDark
deriving (Show, Eq, Generic)
instance ToJSON ThemeName
instance FromJSON ThemeName
-- | [日本語]: preset ごとの既定 series palette (= palette 未指定時に使う色順)。
-- ggplot 系 preset は従来通り hggMain (既定配色)、 ブランド 3 種は専用 series。
-- Layout.computeLayout の catPal 既定がこれを参照する (= palette 指定で上書き可)。
-- [English]: The default series palette for each preset (the color order
-- used when no palette is specified). ggplot-family presets use
-- hggMain (the default color scheme) as before; the three brand themes
-- use their own dedicated series. Layout.computeLayout's default catPal
-- refers to this (overridable by specifying a palette).
themeSeriesPalette :: ThemeName -> [Text]
themeSeriesPalette t = case t of
ThemeNoir -> ["#7AA2F7", "#BB9AF7", "#7DCFFF", "#9ECE6A", "#E0AF68", "#F7768E"]
ThemeLumen -> ["#4C5BD4", "#D6336C", "#2F9E44", "#E8590C", "#7048E8", "#1098AD"]
-- [日本語]: Parchment 明: 案3 (#1 = White Rabbit Inner Ear Pink #F0A5A0、
-- #3 = Dormouse 系 Warm Yellow #E8D58A、 他は既定配色)。 2026-06-02 確定。
-- [English]: Parchment (light): option 3 (#1 = White Rabbit
-- Inner Ear Pink #F0A5A0, #3 = Dormouse-family Warm Yellow #E8D58A, the
-- rest is the default color scheme). Finalized 2026-06-02.
ThemeParchment -> canvasPal
-- [日本語]: 暗版 (Charcoal 背景): 案3 の暗色 (purple/teal/rose/wine) を明度調整し沈み防止。色相・順序は維持。
-- [English]: Dark variant (charcoal background): brightness-adjusted the
-- dark colors from option 3 (purple/teal/rose/wine) to avoid getting
-- lost against the background, while keeping hue and order unchanged.
ThemeParchmentDark -> [ "#F0A5A0", "#A98BD0", "#E8D58A", "#5FA0A8"
, "#E0617E", "#B8C7D9", "#D9685F" ]
-- [日本語]: default 系 (grey/default/minimal/light/dark) は ggplot2 既定 scales::hue_pal() にならう。
-- ★Phase 28 (2026-06-14): 固定 7 色版でなく __群数 n 依存の hue sentinel__ を返す。
-- ggplot は離散色スケールごとに hue_pal()(n) を再計算するため、 群数 3 なら
-- 赤/緑/青、 4 なら別配色…と変わる。 固定 7 色だと群数 3 でも index 0,1,2 =
-- 赤/金/緑 になり R4DS と食い違っていた。 sentinel は Layout.catPal /
-- Bridge.resolveGrouped が @ggplotHue@ n で展開する。
-- [English]: The default-family presets (grey/default/minimal/light/dark)
-- follow ggplot2's default scales::hue_pal(). Rather than a fixed 7-color
-- set, this returns a __hue sentinel that depends on the group count n__.
-- ggplot recomputes hue_pal()(n) for every discrete color scale, so a
-- group count of 3 gives red/green/blue, 4 gives a different set, and so
-- on. With a fixed 7-color set, a group count of 3 would still take
-- indices 0,1,2 (red/gold/green), which disagreed with R4DS. Layout.catPal
-- / Bridge.resolveGrouped expand the sentinel via @ggplotHue@ n.
_ -> ["__ggplot_hue__"]
where
canvasPal = [ "#F0A5A0", "#7A5C92", "#E8D58A", "#3E6A6F"
, "#C7445D", "#B8C7D9", "#7E1F23" ]
-- | [日本語]: 学術向け named series palette。 theme とは独立に `palette <名>` で使う (colorblind-safe 中心)。
-- Okabe-Ito (Okabe & Ito 2008、 色覚バリアフリー定番、 R palette.colors("Okabe-Ito") と同一)。
-- [English]: An academic-style named series palette, used independently of
-- the theme via `palette <name>` (mostly colorblind-safe). Okabe-Ito
-- (Okabe & Ito 2008, a colorblind-accessibility standard, identical to R's
-- palette.colors("Okabe-Ito")).
okabeIto :: [Text]
okabeIto = [ "#000000", "#E69F00", "#56B4E9", "#009E73"
, "#F0E442", "#0072B2", "#D55E00", "#CC79A7" ]
-- | [日本語]: Paul Tol bright (7 色、 色覚バリアフリー)。
-- [English]: Paul Tol bright (7 colors, colorblind-accessible).
tolBright :: [Text]
tolBright = [ "#4477AA", "#EE6677", "#228833", "#CCBB44"
, "#66CCEE", "#AA3377", "#BBBBBB" ]
-- | [日本語]: ColorBrewer Set2 (8 色、 柔らかい定性)。
-- [English]: ColorBrewer Set2 (8 colors, soft qualitative palette).
brewerSet2 :: [Text]
brewerSet2 = [ "#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3"
, "#A6D854", "#FFD92F", "#E5C494", "#B3B3B3" ]
-- | [日本語]: ColorBrewer Dark2 (8 色、 濃いめ定性、 白背景向き)。
-- [English]: ColorBrewer Dark2 (8 colors, deeper qualitative palette,
-- suited to a white background).
brewerDark2 :: [Text]
brewerDark2 = [ "#1B9E77", "#D95F02", "#7570B3", "#E7298A"
, "#66A61E", "#E6AB02", "#A6761D", "#666666" ]
-- | [日本語]: 軸目盛線 (tick mark) の向き。 'TickOut' = panel 外向き
-- (ggplot 既定)、 'TickIn' = panel 内向き (base R / 金融チャート系)、
-- 'TickBoth' = 両向き。 JSON は nullary constructor 名 (canvas Codec と同形)。
-- [English]: The direction of the axis tick marks. 'TickOut' points
-- outward from the panel (ggplot's default), 'TickIn' points inward
-- (base R / financial-chart style), and 'TickBoth' points both ways. The
-- JSON encoding uses the nullary constructor name (matching the canvas
-- codec's shape).
data TickDir = TickOut | TickIn | TickBoth
deriving (Show, Eq, Generic)
instance ToJSON TickDir
instance FromJSON TickDir
-- | [日本語]: 図の外周余白 (pt)。 フィールド順は ggplot @margin(t, r, b, l)@
-- と同じ。 'ThemeOverride' の @toPlotMargin@ に指定すると自動算出の外周分
-- (各辺 half_line = 5.5pt) を __置き換える__ (加算ではない)。
-- [English]: The outer margin of the figure (pt). Field order matches
-- ggplot's @margin(t, r, b, l)@. When specified via the @toPlotMargin@
-- field of 'ThemeOverride', it __replaces__ the automatically computed
-- outer margin
-- (each side's half_line = 5.5pt) rather than adding to it.
data Margin = Margin
{ marTop :: !Double
, marRight :: !Double
, marBottom :: !Double
, marLeft :: !Double
} deriving (Show, Eq, Generic)
instance ToJSON Margin
instance FromJSON Margin
-- ===========================================================================
-- element 単位 theme override (ggplot theme(element_*) 相当)
-- ===========================================================================
-- | [日本語]: preset (ThemeName) に要素単位で上書きを合成する override。 各 field は Last で
-- 「指定があれば優先」。 @resolveTheme@ (Render) が preset palette に合成する。
-- 全 field Monoid なので setter を `<>` で重ねられる (ggplot の theme() 加算と同様)。
-- [English]: An override that composes element-by-element adjustments onto
-- a preset (ThemeName). Each field is a Last, meaning "prefer it if
-- specified". @resolveTheme@ (Render) composes it onto the preset palette.
-- Since every field is a Monoid, setters can be stacked with `<>` (the same
-- way ggplot's theme() calls add up).
data ThemeOverride = ThemeOverride
{ toPlotBg :: !(Last Text) -- plot.background fill
-- [日本語]: ★ Phase 63 A18: plot.background を塗るか (False = 塗らない = 透過。
-- cowplot は rect fill NA = 透過なので合成 preset が False を焼き込む)。
-- [English]: Whether to fill plot.background (False = don't fill, i.e.
-- transparent; since cowplot's rect fill NA means transparent, the
-- composed preset bakes in False).
, toShowBackground :: !(Last Bool)
, toPanelBg :: !(Last Text) -- panel.background fill
, toShowPanel :: !(Last Bool) -- panel 矩形を塗るか
, toGridColor :: !(Last Text) -- panel.grid colour
, toShowGrid :: !(Last Bool) -- panel.grid on/off (= major/minor 両方の糖衣)
-- [日本語]: ★ Phase 63 A2: grid major/minor の個別 on/off (cowplot theme_minimal_grid 等)。
-- 優先順は 個別 (これ) > 一括 toShowGrid > preset (@resolveTheme@ で解決)。
-- [English]: Individual on/off for grid major/minor (cowplot's
-- theme_minimal_grid etc.). Priority is: individual (this) > the blanket
-- toShowGrid > the preset (resolved by @resolveTheme@).
, toShowGridMajor :: !(Last Bool) -- panel.grid.major on/off
, toShowGridMinor :: !(Last Bool) -- panel.grid.minor on/off
, toShowBorder :: !(Last Bool) -- panel.border on/off
, toShowAxisLine :: !(Last Bool) -- axis.line on/off
, toAxisColor :: !(Last Text) -- axis 線/目盛り色
, toTextColor :: !(Last Text) -- 文字色
-- [日本語]: ★ Phase 9 A-3: 文字 theme 統合 (ggplot theme(text/plot.title/axis.title/...) 相当)。
-- 各 slot の FontSpec を theme から差し替え可能に。 優先順位は
-- override (これ) > font setter (vsTitleFont 等) > preset 既定 (@mkFontTS@)。
-- [English]: Unified text theming (equivalent to ggplot's
-- theme(text/plot.title/axis.title/...)). Lets the FontSpec of each slot
-- be swapped out from the theme. Priority is: override (this) > font
-- setters (vsTitleFont etc.) > the preset default (@mkFontTS@).
, toTitleFont :: !(Last FontSpec) -- plot.title
, toAxisLabelFont :: !(Last FontSpec) -- axis.title
, toTickFont :: !(Last FontSpec) -- axis.text
, toLegendFont :: !(Last FontSpec) -- legend.title / legend.text
-- [日本語]: axis.text の回転角 (度・CCW)。 per-axis @axisRotate@ 未指定時の fallback。
-- 'toAxisTextAngle' = x/y 共通既定、 'toAxisTextAngleX'/'toAxisTextAngleY' = 軸別上書き
-- (Phase 50 A3・軸別 > 共通 の優先。 @axisTextAngleXOf@/@axisTextAngleYOf@ で解決)。
-- [English]: The rotation angle of axis.text (degrees, CCW). The
-- fallback used when the per-axis @axisRotate@ is not specified.
-- 'toAxisTextAngle' is the shared x/y default; 'toAxisTextAngleX' /
-- 'toAxisTextAngleY' are the per-axis overrides (per-axis takes priority
-- over shared; resolved by @axisTextAngleXOf@/@axisTextAngleYOf@).
, toAxisTextAngle :: !(Last Double)
, toAxisTextAngleX :: !(Last Double)
, toAxisTextAngleY :: !(Last Double)
-- [日本語]: ★ Phase 9 A-4: strip.background (facet strip の灰矩形)。
-- [English]: strip.background (the grey rectangle behind facet strips).
, toStripBg :: !(Last Text) -- strip.background fill
, toShowStrip :: !(Last Bool) -- strip 矩形を塗るか
-- [日本語]: ★ Phase 43 A4: プリセット専用だった 4 項目に上書き口を追加 (= 全プロパティ `<>` 上書き
-- 可能に)。対応 @ThemePalette@ field = tpTitleHjust / tpTitleColor / tpTickLineColor /
-- tpLegendKeyBg。generic 導出なので field 追加のみで instance は自動追従。
-- [English]: Added override hooks for four fields that used to be
-- preset-only (so every property can now be overridden with `<>`).
-- The corresponding @ThemePalette@ fields are tpTitleHjust /
-- tpTitleColor / tpTickLineColor / tpLegendKeyBg. Since the instance is
-- generically derived, adding a field is all that's needed and the
-- instance follows automatically.
, toTitleHjust :: !(Last Double) -- plot.title の水平揃え (0=左、 0.5=中央)
, toTitleColor :: !(Last Text) -- plot.title / axis.title の文字色
, toTickLineColor :: !(Last Text) -- 軸目盛線 (tick mark) の色
, toLegendKeyBg :: !(Last Text) -- legend.key 背景塗り色 ("" なら塗らない)
-- [日本語]: ★ Phase 63 A3: legend.position を theme に焼き込む口 (cowplot 自作 theme 用)。
-- 優先順は 図レベル vsLegend (legendPos setter) > これ > 既定 LegendRightCenter
-- (@effectiveLegendPos@ で解決。 ggplot の theme() と個別指定の関係に同じ)。
-- [English]: A hook for baking legend.position into the theme (for
-- cowplot-style custom themes). Priority is: the figure-level vsLegend
-- (the legendPos setter) > this > the default LegendRightCenter
-- (resolved by @effectiveLegendPos@; the same relationship as ggplot's
-- theme() versus per-call specification).
, toLegendPos :: !(Last LegendPosition) -- legend.position
-- [日本語]: ★ Phase 63 A4: 軸目盛線の長さ (pt)・向き (ggplot axis.ticks.length 相当)。
-- tick 長は軸ラベル/マージン位置に波及するため、 palette でなく
-- Layout の @effectiveTickLength@/@effectiveTickDir@ が解決し
-- computeLayout (予約) と Render.tickMarks (描画) の単一情報源になる。
-- 未指定時は ggTickLen (2.75pt) / TickOut (= 従来挙動と同一)。
-- [English]: The length (pt) and direction of the axis tick marks
-- (equivalent to ggplot's axis.ticks.length). Since tick length
-- affects axis-label and margin placement, this is resolved not by the
-- palette but by Layout's @effectiveTickLength@/@effectiveTickDir@,
-- making them the single source of truth for computeLayout (reserving
-- space) and Render.tickMarks (drawing). When unspecified, defaults to
-- ggTickLen (2.75pt) / TickOut (identical to the previous behavior).
, toTickLength :: !(Last Double) -- axis.ticks.length (pt)
, toTickDir :: !(Last TickDir) -- 目盛線の向き (外/内/両)
-- [日本語]: ★ Phase 63 A5: 図の外周余白 (ggplot plot.margin 相当)。 指定時は自動算出の
-- 外周分 (各辺 ggHalfLine) を置き換える。 軸ラベル・title 帯・凡例などの
-- 内側予約は従来どおり自動。 Layout の @effectivePlotMargin@ が解決する。
-- [English]: The outer margin of the figure (equivalent to ggplot's
-- plot.margin). When specified, it replaces the automatically
-- computed outer margin (each side's ggHalfLine). Inner reservations
-- such as axis labels, the title band, and the legend remain
-- automatic as before. Resolved by Layout's @effectivePlotMargin@.
, toPlotMargin :: !(Last Margin) -- plot.margin (t/r/b/l、 pt)
-- [日本語]: ★ Phase 63 A12: base font size (pt、 ggplot base_size 相当)。 各 slot の既定
-- font size はこれからの相対倍率 (title ×1.2 / axis.title ×1 / axis.text ×0.8 /
-- legend.title ×1 / legend.text ×0.8) で派生する。 優先順 = 個別 theme*Font
-- (fsSize) > これによる base 派生 > 既定 11 (theme_grey base_size)。
-- font size は layout 予約 (titleSize 等) に波及するため Layout の
-- @effectiveBaseFontSize@ が解決し、 computeLayout (予約) と
-- Render.mkFontTS (描画) の単一情報源になる。
-- [English]: The base font size (pt, equivalent to ggplot's base_size).
-- Each slot's default font size is derived from this via a relative
-- multiplier (title x1.2 / axis.title x1 / axis.text x0.8 /
-- legend.title x1 / legend.text x0.8). Priority is: the per-slot
-- theme*Font (fsSize) > the base-derived value > the default 11
-- (theme_grey's base_size). Since font size affects layout
-- reservations (titleSize etc.), Layout's @effectiveBaseFontSize@
-- resolves it, making it the single source of truth for computeLayout
-- (reserving space) and Render.mkFontTS (drawing).
, toBaseFontSize :: !(Last Double) -- base font size (pt)
-- [日本語]: ★ Phase 63 A19: axis.text (目盛ラベル文字) / axis.title (軸タイトル) の表示。
-- False = ggplot element_blank 相当 (tick 線の有無は toTickLength と独立)。
-- 表示 off は margin 予約に波及するため Layout の @effectiveShowAxisText@ /
-- @effectiveShowAxisTitle@ が解決し、 computeLayout (予約) と Render
-- (tickMarks/labels の描画) の単一情報源になる。 既定は ThemeVoid のみ False
-- (ggplot theme_void = axis.text/axis.title とも element_blank)、 他 preset True。
-- [English]: Visibility of axis.text (tick labels) / axis.title (axis
-- titles). False is equivalent to ggplot's element_blank (independent
-- of whether tick marks themselves are shown via toTickLength). Since
-- turning display off affects margin reservations, Layout's
-- @effectiveShowAxisText@ / @effectiveShowAxisTitle@ resolve it, making
-- them the single source of truth for computeLayout (reserving space)
-- and Render (drawing tickMarks/labels). Defaults to False only for
-- ThemeVoid (ggplot's theme_void makes both axis.text/axis.title
-- element_blank), True for every other preset.
, toShowAxisText :: !(Last Bool) -- axis.text on/off
, toShowAxisTitle :: !(Last Bool) -- axis.title on/off
-- [日本語]: ★ Phase 63 A19.5: 凡例キー 1 辺 (pt、 ggplot legend.key.size 相当)。 キーの
-- 行 pitch = キー辺なので凡例の行間もこれで決まる。 cowplot は全 preset で
-- 1.1 × font_size を明示上書きする (既定 = 1.2 lines = 1.2 × base × 1.3133)。
-- 凡例幅/高さの margin 予約に波及するため Layout の @effectiveLegendKeyW@ が解決。
-- [English]: The side length (pt) of a single legend key (equivalent
-- to ggplot's legend.key.size). Since a key's row pitch equals its
-- side length, this also determines the legend's line spacing.
-- cowplot explicitly overrides this to 1.1 x font_size for every
-- preset (the default is 1.2 lines = 1.2 x base x 1.3133). Since this
-- affects the legend's width/height margin reservation, Layout's
-- @effectiveLegendKeyW@ resolves it.
, toLegendKeySize :: !(Last Double) -- legend.key.size (pt)
-- [日本語]: ★ Phase 63 A20.5: 全 text slot 共通の font family fallback (ggplot
-- theme(text = element_text(family=...)) 相当)。 優先順位は slot 別 FontSpec の
-- fsFamily > これ > "sans-serif" (@mkFontTS@ が解決)。 slot 丸ごと置換
-- (Last FontSpec) と違い preset の fontSize 焼き込みを潰さない。
-- [English]: A font-family fallback shared by every text slot
-- (equivalent to ggplot's theme(text = element_text(family=...))).
-- Priority is: the per-slot FontSpec's fsFamily > this > "sans-serif"
-- (resolved by @mkFontTS@). Unlike replacing a whole slot (Last
-- FontSpec), this doesn't clobber the preset's baked-in fontSize.
, toFontFamily :: !(Last Text) -- text family (全 slot 共通 fallback)
-- [日本語]: ★ Phase 68: grid / 軸線の線幅 (ggplot @theme(panel.grid =
-- element_line(linewidth=))@ / @axis.line@ / @panel.border@ 相当)。 全て
-- @Last@ で、 未指定=各 role の現状決め打ちに fallback (既存 golden ゼロ diff)。
-- 解決は 'Graphics.Hgg.Render.Common' の @effective*Width@ 群が単一情報源で行う
-- (Phase 63 の @effectiveX@ 方式)。 線幅は panel 面積に波及しないので Layout 予約は
-- 不要 (grid 色 'toGridColor' と同じく純 Render 事項)。 規約:
-- * 'toGridWidth' 未指定 = Cartesian major 1.0 / polar・ternary grid 0.5 (座標系別の
-- 現状値を維持) / 指定時 = 全 grid role を統一値に (ggplot panel.grid は座標系非依存)。
-- * 'toGridMinorWidth' 未指定 = major × 0.5 (ggplot @panel.grid.minor = rel(0.5)@)。
-- * 'toAxisLineWidth' = axis.line / panel.border / ternary edge / 右 Y 軸線 (未指定 1.0)。
-- [English]: ★ Phase 68: line widths for grid / axis lines (ggplot
-- @theme(panel.grid = element_line(linewidth=))@ / @axis.line@ /
-- @panel.border@). All @Last@; unspecified falls back to each role's
-- current literal (zero golden diff). Resolved by the @effective*Width@
-- helpers in 'Graphics.Hgg.Render.Common' as the single source of truth
-- (Phase 63's @effectiveX@ style). Widths don't affect panel area, so no
-- Layout reservation is needed (a pure Render concern, like grid colour).
-- Rules: 'toGridWidth' unspecified keeps the per-coord literals (Cartesian
-- major 1.0 / polar & ternary grid 0.5), while setting it unifies every
-- grid role (ggplot panel.grid is coord-independent). 'toGridMinorWidth'
-- defaults to major × 0.5 (ggplot @rel(0.5)@). 'toAxisLineWidth' covers
-- axis.line / panel.border / ternary edge / right Y axis (default 1.0).
, toGridWidth :: !(Last Double) -- panel.grid (major・全 grid の base)
, toGridMinorWidth :: !(Last Double) -- panel.grid.minor (Cartesian minor 独立上書き)
, toAxisLineWidth :: !(Last Double) -- axis.line / panel.border / ternary edge
} deriving stock (Generic, Show, Eq)
-- [日本語]: ★ Phase 43 A3: 全 field が `Last` の素直な per-field 合成なので generic 導出。
-- 位置依存の手書き instance (旧 `a1..p1` を数で揃える形) を撲滅し、 以後の field
-- 追加を「field を足すだけ」で安全にする。挙動は旧手書きと完全同型。
-- [English]: Since every field is a straightforward per-field
-- composition of `Last`, the instance is generically derived. This
-- eliminates the old position-dependent hand-written instance (which
-- lined up `a1..p1` by count) and makes future field additions safe —
-- just add the field. Behavior is exactly identical to the old
-- hand-written version.
deriving (Semigroup, Monoid) via Generically ThemeOverride
instance ToJSON ThemeOverride
instance FromJSON ThemeOverride