packages feed

hgg-svg 0.1.0.0 → 0.2.0.0

raw patch · 8 files changed

+332/−77 lines, 8 filesdep ~aesondep ~bytestringdep ~containersPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, bytestring, containers, directory, hgg-core, hgg-frame

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,11 @@ # Changelog for `hgg-svg` +## 0.2.0.0 — 2026-08-13++- Follows the hgg-core 0.2 coordinate generalization (ternary / polar+  rendering paths).+- Bilingual (English / Japanese) haddock.+ ## 0.1.0.0 — 2026-07-18  First public release on Hackage.
examples/DocFig/Decoration.hs view
@@ -104,6 +104,29 @@       <> coordFlip       <> title "3g. coordFlip (横棒)" <> xLabel "群" <> yLabel "値" +    -- 3g-2 coord: ternary (Phase 69・ternaryScatter + coord 推論)。 a=上・b=左下・c=右下。+    --   4 点は各頂点付近 + 中心 = 頂点↔成分の対応が読める配置。 coordTernary は encZ から推論。+  , fig "s3g2-ternary.svg" $+         purePlot+      <> layer (ternaryScatter (inline [0.7, 0.2, 0.2, 0.34 :: Double])+                               (inline [0.2, 0.7, 0.1, 0.33])+                               (inline [0.1, 0.1, 0.7, 0.33])+                 <> size 6)+      <> xLabel "a" <> yLabel "b" <> zLabel "c"+      <> title "3g-2. ternaryScatter (3 成分の組成)"++    -- 3g-2 向き: coordTernaryWith (Phase 69 A4)。 既定 / clockwise / rotate120 の 3 面比較。+  , figW "s3g2-ternary-orient.svg" 1280 420 $+         subplots+           [ layer (ternaryScatter tA tB tC <> size 6) <> title "既定 (a=上)"+           , layer (ternaryScatter tA tB tC <> size 6) <> coordTernaryWith True 0+               <> title "coordTernaryWith True 0 (clockwise)"+           , layer (ternaryScatter tA tB tC <> size 6) <> coordTernaryWith False 120+               <> title "coordTernaryWith False 120 (rotate)" ]+      <> subplotCols 3+      <> xLabel "a" <> yLabel "b" <> zLabel "c"+      <> title "三角座標の向き (coordTernaryWith)"+     -- 3h 補助: 参照線 + 凡例   , fig "s3h-guides.svg" $          purePlot@@ -127,6 +150,34 @@       <> panelBorder True <> themeGrid False <> themeAxisTextAngle 45       <> title "theme 要素の部分上書き" +    -- 03 theme: grid / 軸線の線幅 (Phase 68。 element_line(linewidth=) 相当・左=既定 / 右=太らせ)+  , figW "s3e-theme-linewidth.svg" 960 400 $+         subplots+           [ layer (scatter xs ys <> size 5)+               <> theme ThemeMinimal <> gridColor "#9ca3af" <> panelBorder True+               <> title "既定 (grid 1.0 / 軸線 1.0)"+           , layer (scatter xs ys <> size 5)+               <> theme ThemeMinimal <> gridColor "#9ca3af" <> panelBorder True+               <> themeGridWidth 2.5 <> themeAxisLineWidth 2.0+               <> title "themeGridWidth 2.5 <> themeAxisLineWidth 2.0" ]+      <> subplotCols 2+      <> title "grid / 軸線の線幅 (element_line(linewidth=))"++    -- 03 theme: cowplot 風 preset 3 種 (Phase 63 A8。 panel 別 theme として並置)+  , figW "s3e-theme-cowplot.svg" 1280 420 $+         subplots [ layer (scatter xs ys <> size 5) <> themeCowplot     <> title "themeCowplot"+                  , layer (scatter xs ys <> size 5) <> themeMinimalGrid <> title "themeMinimalGrid"+                  , layer (scatter xs ys <> size 5) <> themeMap         <> title "themeMap" ]+      <> subplotCols 3+      <> title "cowplot 風 preset (themeCowplot / themeMinimalGrid / themeMap)"++    -- 3f-2 subplot: 相対幅 + panel 自動タグ (Phase 63 A6/A7。 cowplot plot_grid 相当)+  , figW "s3f2-subplot-tags.svg" 960 400 $+         subplots [ layer (scatter xs ys <> size 5) <> title "散布"+                  , layer (bar cats vals)           <> title "棒" ]+      <> subplotCols 2 <> subplotWidths [1.3, 1] <> subplotTags TagUpper+      <> title "subplotWidths [1.3, 1] <> subplotTags TagUpper"+     -- 03 theme × facet strip   , figR "s3e-theme-strip.svg" rFacet $          purePlot <> layer (scatter "x" "y" <> colorBy "g") <> facet "g"@@ -181,6 +232,10 @@   where     xs = inline    [1,2,3,4, 1,2,3,4]     ys = inline    [2,3,1,4, 3,1,4,2]+    -- Phase 69 ternary 向き図の共通 3 成分 (a 上・b 左下・c 右下)。+    tA = inline [0.7, 0.2, 0.2, 0.34 :: Double]+    tB = inline [0.2, 0.7, 0.1, 0.33]+    tC = inline [0.1, 0.1, 0.7, 0.33]     gs = inlineCat (concatMap (replicate 4) (["alpha","beta"] :: [Text]))     cats = inlineCat (["A","B","C"] :: [Text])     vals = inline [3.0, 7.0, 5.0]
examples/DocFig/Layers.hs view
@@ -192,11 +192,13 @@       <> subplotCols 2 <> legend       <> title "trace / traceLines" -    -- MCMC: ess+    -- MCMC: ess (encX = chain/パラメータ名 (categorical)、 encY = 計算済み ESS 値。+    -- ★ Phase 70 A3: 旧 demo は nameCol に数値 iter を渡す API 誤用で bar ゼロの空図+    -- だった。 閾値 3 色 (赤 <100 / 橙 <400 / 緑 ≥400) が全部見える値にする)   , fig "ess.svg" $-         purePlot <> layer (ess (inline [100,200,300,400,500,600])-                                (inline [80,150,210,260,300,330]))-      <> title "ess (有効サンプルサイズ)" <> xLabel "iter" <> yLabel "ESS"+         purePlot <> layer (ess (inlineCat (["mu", "tau", "theta1", "theta2"] :: [Text]))+                                (inline [85, 340, 620, 1450]))+      <> title "ess (有効サンプルサイズ)" <> xLabel "parameter" <> yLabel "ESS"      -- MCMC: autocorr (AR(1) 風の減衰系列・自己完結式)   , fig "autocorr.svg" $
examples/GalleryDemo.hs view
@@ -423,6 +423,65 @@     <> title "swarm + coord_flip (横向き)"     <> xLabel "genotype" <> yLabel "weight gain (g/d)" +  -- Phase 64 §1 (A5): coord_polar + distribution 族。 §1 の座標集約前は box/violin/+  --   errorbar が polar でも Cartesian の軸平行図形のまま落ちていた (= polar 負債。+  --   before 図は design/phase64-coord-ternary/before-polar-{box,violin,pointrange}.png)。+  --   集約後は cat 軸が角度・値軸が半径に写り、 box は扇形帯・violin は弧に沿った+  --   輪郭・errorbar は半径方向の線分として描かれる。 この 3 枚が解消の証拠。+  emit "coord/polar-box.svg" $+       purePlot+    <> layer (boxplot grpNum <> groupBy grpCat)+    <> coordPolar+    <> title "coord_polar + box (群が角度・値が半径)"+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"++  emit "coord/polar-violin.svg" $+       purePlot+    <> layer (violin grpNum <> groupBy grpCat)+    <> coordPolar+    <> title "coord_polar + violin (群が角度・値が半径)"+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"++  emit "coord/polar-errorbar.svg" $+       purePlot+    <> layer (pointRange (inline ([1,2,3,4,5,6] :: [Double]))+                         (inline ([4.0,5.5,4.8,6.2,5.0,5.8] :: [Double]))+                         (inline ([0.6,0.5,0.8,0.4,0.7,0.5] :: [Double])))+    <> coordPolar+    <> title "coord_polar + pointRange (誤差棒が半径方向)"+    <> xLabel "condition" <> yLabel "mean ± CI"++  -- Phase 64 §3 (A14): coord_ternary (三角座標)。 3 成分 (encX=a 上・encY=b 左下・+  --   encZ=c 右下) の組成データを正三角形の重心座標へ写す。 各行は成分和 1 に正規化+  --   (退化行は除外)。 土壌テクスチャ (sand/silt/clay) を色群付き散布で。+  emit "coord/ternary-scatter.svg" $+       purePlot+    <> layer (scatter (inline ([0.6,0.2,0.4,0.1,0.5,0.34] :: [Double]))   -- sand=a+                      (inline ([0.3,0.6,0.2,0.5,0.2,0.33] :: [Double]))   -- silt=b+              <> encZ (inline ([0.1,0.2,0.4,0.4,0.3,0.33] :: [Double]))   -- clay=c+              <> colorBy (inlineCat (["loam","silt","clay","silt","sand","loam"] :: [Text]))+              <> size 5)+    <> coordTernary+    <> xLabel "Sand" <> yLabel "Silt" <> zLabel "Clay"+    <> title "coord_ternary + scatter (soil texture)"++  -- Phase 64 §3 (A14): coord_ternary + line = 組成の推移を三角形内の path で。+  --   line/scatter とも encZ を持つ (レイヤーごとに第 3 成分列が要る)。 三角形 clip+  --   (§2) が接続済 = 数値スピルの安全網 (正規化後は全点内部ゆえ通常は不可視)。+  emit "coord/ternary-line.svg" $+       purePlot+    <> layer (line (inline ([0.8,0.6,0.4,0.2,0.1] :: [Double]))+                   (inline ([0.1,0.2,0.3,0.5,0.6] :: [Double]))+              <> encZ (inline ([0.1,0.2,0.3,0.3,0.3] :: [Double]))+              <> stroke 2)+    <> layer (scatter (inline ([0.8,0.6,0.4,0.2,0.1] :: [Double]))+                      (inline ([0.1,0.2,0.3,0.5,0.6] :: [Double]))+              <> encZ (inline ([0.1,0.2,0.3,0.3,0.3] :: [Double]))+              <> size 4)+    <> coordTernary+    <> xLabel "a" <> yLabel "b" <> zLabel "c"+    <> title "coord_ternary + line (組成の推移)"+   -- Phase 10 A5 ②: waterfall の flip (standalone・自前 baseline/y tick/cat ラベル)。   emit "coord/waterfall-flip.svg" $        purePlot@@ -576,7 +635,8 @@   -- DECORATION   -- =========================================================================== -  -- facet (= Resolver 経由で column 解決が必要、 inline では不可なので別 demo)+  -- facet (この demo は名前参照 + Resolver。 Phase 62 以降は inline encoding でも+  -- panel 分割される — facet 列と同じ長さの inline なら名前参照と等価)   let facetResolver facetN = case facetN of         "x" -> Just (NumData (V.fromList [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]))         "y" -> Just (NumData (V.fromList [1, 4, 9, 16, 2, 5, 8, 12, 3, 6, 9, 15]))
hgg-svg.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               hgg-svg-version:            0.1.0.0+version:            0.2.0.0 extra-doc-files:    CHANGELOG.md synopsis:           SVG backend for hgg (pure Haskell) description:@@ -28,8 +28,8 @@   build-depends:    base               >= 4.17 && < 5                   , text               >= 2.0  && < 2.2                   , vector             >= 0.13 && < 0.14-                  , hgg-core  ^>= 0.1-                  , hgg-frame ^>= 0.1+                  , hgg-core  ^>= 0.2+                  , hgg-frame ^>= 0.2   default-language: Haskell2010  executable scatter-demo@@ -39,7 +39,7 @@   build-depends:    base               >= 4.17 && < 5                   , text                   , vector-                  , hgg-core+                  , hgg-core  ^>= 0.2                   , hgg-svg   default-language: Haskell2010 @@ -49,7 +49,7 @@   hs-source-dirs:   examples   build-depends:    base               >= 4.17 && < 5                   , vector-                  , hgg-core+                  , hgg-core  ^>= 0.2                   , hgg-svg   default-language: Haskell2010 @@ -59,8 +59,8 @@   hs-source-dirs:   examples   build-depends:    base               >= 4.17 && < 5                   , vector-                  , directory-                  , hgg-core+                  , directory          >= 1.3  && < 1.4+                  , hgg-core  ^>= 0.2                   , hgg-svg   default-language: Haskell2010 @@ -69,11 +69,11 @@   main-is:          DfPlotDemo.hs   hs-source-dirs:   examples   build-depends:    base               >= 4.17 && < 5-                  , containers-                  , directory+                  , containers         >= 0.6  && < 0.8+                  , directory          >= 1.3  && < 1.4                   , text                   , vector-                  , hgg-core+                  , hgg-core  ^>= 0.2                   , hgg-frame                   , hgg-svg   default-language: Haskell2010@@ -85,10 +85,10 @@   build-depends:    base               >= 4.17 && < 5                   , text                   , vector-                  , directory-                  , aeson-                  , bytestring-                  , hgg-core+                  , directory          >= 1.3  && < 1.4+                  , aeson              >= 2.0  && < 2.3+                  , bytestring         >= 0.11 && < 0.13+                  , hgg-core  ^>= 0.2                   , hgg-svg   default-language: Haskell2010 @@ -98,7 +98,7 @@   hs-source-dirs:   examples   build-depends:    base               >= 4.17 && < 5                   , text-                  , hgg-core+                  , hgg-core  ^>= 0.2                   , hgg-svg   default-language: Haskell2010 @@ -111,8 +111,8 @@   ghc-options:      -rtsopts "-with-rtsopts=-M2500m"   build-depends:    base               >= 4.17 && < 5                   , text-                  , directory-                  , hgg-core+                  , directory          >= 1.3  && < 1.4+                  , hgg-core  ^>= 0.2                   , hgg-svg   default-language: Haskell2010 @@ -121,9 +121,9 @@   main-is:          JsonDump.hs   hs-source-dirs:   examples   build-depends:    base               >= 4.17 && < 5-                  , bytestring-                  , aeson-                  , hgg-core+                  , bytestring         >= 0.11 && < 0.13+                  , aeson              >= 2.0  && < 2.3+                  , hgg-core  ^>= 0.2   default-language: Haskell2010  -- Runnable examples for the docs/ tutorials (referenced from docs/getting-started.md / api-guide.md)@@ -172,7 +172,7 @@                   , DocFig.Layers                   , DocFig.EncodingScale                   , DocFig.Decoration-  build-depends:    base >= 4.17 && < 5, text, vector, directory, hgg-core, hgg-svg+  build-depends:    base >= 4.17 && < 5, text, vector, directory >= 1.3 && < 1.4, hgg-core, hgg-svg   default-language: Haskell2010  -- Smoke test for BoundPlot (df |>> spec) → SVG. Lives here rather than in@@ -185,8 +185,8 @@   build-depends:    base               >= 4.17 && < 5                   , text                   , vector-                  , containers-                  , hgg-core+                  , containers         >= 0.6  && < 0.8+                  , hgg-core  ^>= 0.2                   , hgg-frame                   , hgg-svg                   , hspec      >= 2.10 && < 2.12
src/Graphics/Hgg/Backend/SVG.hs view
@@ -1,6 +1,6 @@ -- | -- Module      : Graphics.Hgg.Backend.SVG--- Description : SVG backend (Phase 26 §B-1 Resolver 対応版)+-- Description : SVG backend (with Resolver support) -- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda) -- License     : BSD-3-Clause {-# LANGUAGE OverloadedStrings #-}@@ -8,7 +8,7 @@   ( -- * 通常 (Resolver 不要 = inline 列のみの図)     renderSVG   , saveSVG-    -- * Resolver 同伴 (= 'ColByName' を含む図)+    -- * Resolver 同伴 (= @ColByName@ を含む図)   , renderSVGWith   , saveSVGWith   , renderSVGInteractive@@ -34,13 +34,17 @@                                         emptyResolver, lyKind, vsDpi) import           Data.Monoid           (getFirst, getLast) import           Graphics.Hgg.Validate (PlotDiagnostic, Severity (..),-                                        diagnosticSeverity, renderDiagnostic)+                                        diagnosticSeverity, renderDiagnostic,+                                        reportFacetInlineWarnings,+                                        reportTernaryMarkWarnings) import           Data.Text             (Text) import qualified Data.Text             as T import qualified Data.Text.IO          as TIO import           System.IO             (hPutStrLn, stderr) --- | 'Resolver' を渡して 'VisualSpec' を SVG text に。 'ColByName' を含む図用。+-- | [日本語]: 'Resolver' を渡して 'VisualSpec' を SVG text に。 @ColByName@ を含む図用。+--   [English]: Renders a 'VisualSpec' to SVG text given a 'Resolver'. For+--   figures that include @ColByName@. renderSVGWith :: Resolver -> VisualSpec -> Text renderSVGWith r spec =   let layout     = computeLayout r spec@@ -59,48 +63,80 @@         ]   in T.concat (header : primsToSvg primitives : ["</svg>"]) --- | render 'VisualSpec' to SVG text。 Resolver 不要 (= 全 ColRef が inline、--- 'ColByName' が無い図で使う、 = 通常)。 列名参照を含む図は 'renderSVGWith'。+-- | [日本語]: render 'VisualSpec' to SVG text。 Resolver 不要 (= 全 ColRef が inline、+--   @ColByName@ が無い図で使う、 = 通常)。 列名参照を含む図は 'renderSVGWith'。+--   [English]: Renders a 'VisualSpec' to SVG text. No 'Resolver' needed (used+--   for figures where every @ColRef@ is inline and there is no @ColByName@ —+--   the usual case). For figures with column-name references, use+--   'renderSVGWith'. renderSVG :: VisualSpec -> Text renderSVG = renderSVGWith emptyResolver --- | 'Resolver' を渡して SVG ファイルに保存。 'ColByName' を含む図用。+-- | [日本語]: 'Resolver' を渡して SVG ファイルに保存。 @ColByName@ を含む図用。+--   [English]: Saves to an SVG file given a 'Resolver'. For figures that+--   include @ColByName@. saveSVGWith :: FilePath -> Resolver -> VisualSpec -> IO ()-saveSVGWith path r spec = TIO.writeFile path (renderSVGWith r spec)+saveSVGWith path r spec = do+  reportFacetInlineWarnings r spec   -- ★ Phase 62 A4 (§3): 描画は継続+  reportTernaryMarkWarnings r spec   -- Phase 64 A13: coordTernary unsupported mark+  TIO.writeFile path (renderSVGWith r spec) --- | SVG ファイルに保存。 Resolver 不要 (= inline 列のみの図、 = 通常)。--- 列名参照を含む図は 'saveSVGWith'、 DataFrame は 'saveSVGBound' (@df |>> spec@)。+-- | [日本語]: SVG ファイルに保存。 Resolver 不要 (= inline 列のみの図、 = 通常)。+--   列名参照を含む図は 'saveSVGWith'、 DataFrame は 'saveSVGBound' (@df |>> spec@)。+--   [English]: Saves to an SVG file. No 'Resolver' needed (figures with+--   inline columns only — the usual case). For figures with column-name+--   references use 'saveSVGWith'; for a DataFrame use 'saveSVGBound'+--   (@df |>> spec@). saveSVG :: FilePath -> VisualSpec -> IO () saveSVG path = saveSVGWith path emptyResolver --- | 'purePlot' (= 純粋値) と対をなす副作用関数。 中身は 'saveSVG' の alias、--- SVG backend が default。 他 backend (PDF / PNG) を使う場合はそれぞれの--- module の `plot` を import する。+-- | [日本語]: @purePlot@ (= 純粋値) と対をなす副作用関数。 中身は 'saveSVG' の alias、+--   SVG backend が default。 他 backend (PDF / PNG) を使う場合はそれぞれの+--   module の `plot` を import する。+--   [English]: The effectful counterpart of @purePlot@ (the pure value).+--   Internally just an alias of 'saveSVG', with the SVG backend as default.+--   To use another backend (PDF / PNG), import `plot` from the corresponding+--   module instead. -- -- > main = plot "out.svg" $ purePlot <> layer (scatter ...) <> title "..." plot :: FilePath -> VisualSpec -> IO () plot = saveSVG --- | Phase 14: 'BoundPlot' (= @df |>> spec@ の結果) を SVG text に。--- 'renderSVGWith' を 'bpResolver' / 'bpSpec' で呼ぶ薄いラッパ。--- 検証診断 ('bpDiagnostics') は副作用が無いここでは無視する--- (報告は 'saveSVGBound' / 利用者が 'bpDiagnostics' を直接見る)。+-- | [日本語]: 'BoundPlot' (= @df |>> spec@ の結果) を SVG text に。+--   'renderSVGWith' を 'bpResolver' / 'bpSpec' で呼ぶ薄いラッパ。+--   検証診断 ('bpDiagnostics') は副作用が無いここでは無視する+--   (報告は 'saveSVGBound' / 利用者が 'bpDiagnostics' を直接見る)。+--   [English]: Renders a 'BoundPlot' (the result of @df |>> spec@) to SVG+--   text. A thin wrapper that calls 'renderSVGWith' with 'bpResolver' /+--   'bpSpec'. The validation diagnostics ('bpDiagnostics') are ignored here+--   since this function has no side effects (reporting is left to+--   'saveSVGBound' or to the caller inspecting 'bpDiagnostics' directly). renderBound :: BoundPlot -> Text renderBound (BoundPlot r spec _) = renderSVGWith r spec --- | Phase 14: 'BoundPlot' を SVG ファイルに保存。--- 'bpDiagnostics' に Error severity があれば **stderr に報告**してから書き出す--- (描画自体は止めない = 純値 '(|>>)' の lenient 既定。 無検証で通したい場合は--- 'unBound' → 'saveSVGWith' を直接使う)。+-- | [日本語]: 'BoundPlot' を SVG ファイルに保存。+--   'bpDiagnostics' に Error severity があれば __stderr に報告__してから書き出す+--   (描画自体は止めない = 純値 @(|>>)@ の lenient 既定。 無検証で通したい場合は+--   @unBound@ → 'saveSVGWith' を直接使う)。+--   [English]: Saves a 'BoundPlot' to an SVG file. If 'bpDiagnostics'+--   contains an error-severity entry, __reports it to stderr__ before+--   writing the file (rendering itself is not stopped — the lenient default+--   of the pure @(|>>)@. To skip validation entirely, use @unBound@ followed+--   by 'saveSVGWith' directly). saveSVGBound :: FilePath -> BoundPlot -> IO () saveSVGBound path bp@(BoundPlot _ spec diags) = do   reportErrors diags   warnUnresolvedStats spec   TIO.writeFile path (renderBound bp) --- | Phase 16 footgun 緩和: spec に未解決 stat layer (@MStatLM@/@MStatSmooth@) が残っていたら--- stderr に警告 (描画では skip され回帰線が出ない)。 回帰を描くには analyze-bridge の--- @saveSVGBoundStats@ / @resolveStats@ を使う。 stat を使わない通常図では何もしない。+-- | [日本語]: footgun 緩和: spec に未解決 stat layer (@MStatLM@/@MStatSmooth@) が残っていたら+--   stderr に警告 (描画では skip され回帰線が出ない)。 回帰を描くには analyze-bridge の+--   @saveSVGBoundStats@ / @resolveStats@ を使う。 stat を使わない通常図では何もしない。+--   [English]: A footgun guard: if the spec still has an unresolved stat+--   layer (@MStatLM@/@MStatSmooth@), warns on stderr (rendering skips it, so+--   no regression line is drawn). To draw the regression, use+--   analyze-bridge's @saveSVGBoundStats@ / @resolveStats@. Does nothing for+--   ordinary figures that do not use a stat layer. warnUnresolvedStats :: VisualSpec -> IO () warnUnresolvedStats spec   | any isStat (vsLayers spec) =@@ -114,15 +150,20 @@       Just MStatSmooth -> True       _                -> False --- | Error severity の診断のみ stderr に出す。+-- | [日本語]: Error severity の診断のみ stderr に出す。+--   [English]: Emits only the error-severity diagnostics to stderr. reportErrors :: [PlotDiagnostic] -> IO () reportErrors diags =   mapM_ (hPutStrLn stderr . T.unpack . renderDiagnostic)         (filter ((== SevError) . diagnosticSeverity) diags) --- | Interactive 版: hover tooltip (= 標準 native) に加えて、--- ドラッグで pan / wheel で zoom できる inline JS を末尾に embed。--- ブラウザで開いた時だけ動作、 raw SVG viewer では普通に静止画。+-- | [日本語]: Interactive 版: hover tooltip (= 標準 native) に加えて、+--   ドラッグで pan / wheel で zoom できる inline JS を末尾に embed。+--   ブラウザで開いた時だけ動作、 raw SVG viewer では普通に静止画。+--   [English]: The interactive variant: in addition to the standard native+--   hover tooltip, embeds inline JS at the end that enables drag-to-pan and+--   wheel-to-zoom. This only works when opened in a browser; in a raw SVG+--   viewer it is just a static image. renderSVGInteractive :: Resolver -> VisualSpec -> Text renderSVGInteractive r spec =   let base = renderSVGWith r spec@@ -131,12 +172,21 @@   in T.concat [pre, panZoomScript, post]  saveSVGInteractive :: FilePath -> Resolver -> VisualSpec -> IO ()-saveSVGInteractive path r spec = TIO.writeFile path (renderSVGInteractive r spec)+saveSVGInteractive path r spec = do+  reportFacetInlineWarnings r spec   -- ★ Phase 62 A4 (§3)+  reportTernaryMarkWarnings r spec   -- Phase 64 A13: coordTernary unsupported mark+  TIO.writeFile path (renderSVGInteractive r spec) --- | Phase 3 A8: '[Primitive]' を直接 SVG にする helper。--- 'renderSVG' は VisualSpec 経由だが、 hgg-3d のように外部で--- Primitive 列を生成済の場合に使う。 既存の 'primToSvg' converter をそのまま流用、--- 出力 SVG 構造 (= header + light bg + title + body) は 'renderSVG' と同形式。+-- | [日本語]: '[Primitive]' を直接 SVG にする helper。+--   'renderSVG' は VisualSpec 経由だが、 hgg-3d のように外部で+--   Primitive 列を生成済の場合に使う。 既存の 'primToSvg' converter をそのまま流用、+--   出力 SVG 構造 (= header + light bg + title + body) は 'renderSVG' と同形式。+--   [English]: A helper that turns a '[Primitive]' directly into SVG. While+--   'renderSVG' goes through a VisualSpec, this is for cases where the+--   primitives have already been generated externally, as in+--   hgg-3d. Reuses the existing 'primToSvg' converter as-is, so the+--   output SVG structure (header + light bg + title + body) matches+--   'renderSVG'. renderPrimitivesSVG :: Int -> Int -> Text -> [Primitive] -> Text renderPrimitivesSVG w h titleTxt prims =   let header = T.concat@@ -158,12 +208,15 @@         , " font-size=\"16\" fill=\"#333\">", titleTxt, "</text>" ]   in T.concat (header : bg : title_ : primsToSvg prims : ["</svg>"]) --- | 'renderPrimitivesSVG' をファイル書出し版。+-- | [日本語]: 'renderPrimitivesSVG' をファイル書出し版。+--   [English]: The file-writing variant of 'renderPrimitivesSVG'. savePrimitivesSVG :: FilePath -> Int -> Int -> Text -> [Primitive] -> IO () savePrimitivesSVG path w h t prims =   TIO.writeFile path (renderPrimitivesSVG w h t prims) --- | pan / zoom inline JS。 SVG の viewBox を操作するだけの最小実装。+-- | [日本語]: pan / zoom inline JS。 SVG の viewBox を操作するだけの最小実装。+--   [English]: The pan / zoom inline JS. A minimal implementation that only+--   manipulates the SVG's viewBox. panZoomScript :: Text panZoomScript = T.concat   [ "<script type=\"application/ecmascript\"><![CDATA[\n"@@ -203,9 +256,14 @@ -- Primitive → SVG element -- --------------------------------------------------------------------------- --- | Phase 11 A7-a: clip stack を解決して SVG body に変換。 'PClipPush' で+-- | [日本語]: clip stack を解決して SVG body に変換。 'PClipPush' で --   @\<clipPath\>@ + @\<g clip-path\>@ を開き、 'PClipPop' で @\</g\>@ を閉じる。 --   clip プリミティブが無い列では @map primToSvg@ と完全同一出力なので既存 SVG ゼロ diff。+--   [English]: Resolves the clip stack and converts it to SVG body. Opens+--   @\<clipPath\>@ + @\<g clip-path\>@ at 'PClipPush', and closes @\</g\>@ at+--   'PClipPop'. For a primitive list with no clip primitives, the output is+--   byte-identical to @map primToSvg@, so it produces zero diff against+--   existing SVG output. primsToSvg :: [Primitive] -> Text primsToSvg = T.concat . go (0 :: Int)   where@@ -217,6 +275,18 @@             , "\" width=\"", numD w, "\" height=\"", numD h, "\"/></clipPath>"             , "<g clip-path=\"url(#", cid, ")\">" ]       in open : go (n + 1) rest+    -- Phase 64 §2: 多角形 clip。 <polygon> は暗黙に閉じるので points をそのまま出す。+    --   3 点未満は clip 無し (= <g> だけ開く) — Primitive の fail-open 規約。+    go n (PClipPath pts : rest)+      | length pts < 3 = "<g>" : go n rest+      | otherwise =+          let cid  = T.concat ["clip", num n]+              open = T.concat+                [ "<clipPath id=\"", cid, "\"><polygon points=\""+                , T.intercalate " " [T.concat [numD x, ",", numD y] | Point x y <- pts]+                , "\"/></clipPath>"+                , "<g clip-path=\"url(#", cid, ")\">" ]+          in open : go (n + 1) rest     go n (PClipPop : rest) = "</g>" : go n rest     go n (p : rest)        = primToSvg p : go n rest @@ -291,6 +361,7 @@       , "/>"       ]   PClipPush{}      -> ""+  PClipPath{}      -> ""   PClipPop         -> ""   PTransformPush{} -> ""   PTransformPop    -> ""@@ -309,8 +380,11 @@ strokeAttr (Just (StrokeStyle c w)) =   T.concat [" stroke=\"", c, "\" stroke-width=\"", numD w, "\""] --- | Phase 11 A4-b: stroke-dasharray 属性。 空配列 (= 実線) は attr を出さない+-- | [日本語]: stroke-dasharray 属性。 空配列 (= 実線) は attr を出さない --   (= 既存 SVG ゼロ diff の要)。 非空のみ \" stroke-dasharray=\\\"a,b,..\\\"\" を付す。+--   [English]: The stroke-dasharray attribute. An empty array (a solid line)+--   emits no attribute (needed for zero diff against existing SVG). Only for+--   a non-empty list does it emit \" stroke-dasharray=\\\"a,b,..\\\"\". dashAttr :: [Double] -> Text dashAttr [] = "" dashAttr ds = T.concat [" stroke-dasharray=\"", T.intercalate "," (map numD ds), "\""]
src/Graphics/Hgg/Quick.hs view
@@ -1,12 +1,16 @@ -- | -- Module      : Graphics.Hgg.Quick--- Description : Easy 層の IO ワンショット保存 (= 1 行で SVG 出力、 Phase 11 A3)+-- Description : IO one-shot save helpers for the Easy layer (SVG output in a single line) -- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda) -- License     : BSD-3-Clause ----- core は backend 非依存 (architecture §3.3) のため IO 保存ヘルパは backend 側--- (本 module) に置く。 値を渡すだけで SVG が 1 ファイル出る入門用 API。+-- [日本語]: core は backend 非依存 (architecture §3.3) のため IO 保存ヘルパは+-- backend 側 (本 module) に置く。 値を渡すだけで SVG が 1 ファイル出る入門用 API。 --+-- [English]: Since core is backend-agnostic (architecture §3.3), the IO save+-- helpers live on the backend side (this module). A beginner-friendly API+-- that produces a single SVG file just by passing in values.+-- -- @ -- import Graphics.Hgg.Quick --@@ -17,8 +21,12 @@ --                              , lineXY [1,2,3] [1,4,9] ] -- @ ----- `Graphics.Hgg.Easy` を再 export するので、 本 module 1 つの import で--- `points` / `lineXY` / `overlay` 等の Easy ヘルパも揃う。+-- [日本語]: `Graphics.Hgg.Easy` を再 export するので、 本 module 1 つの+-- import で `points` / `lineXY` / `overlay` 等の Easy ヘルパも揃う。+--+-- [English]: Re-exports `Graphics.Hgg.Easy`, so a single import of this+-- module also brings in the Easy helpers such as `points` / `lineXY` /+-- `overlay`. {-# LANGUAGE OverloadedStrings #-} module Graphics.Hgg.Quick   ( -- * Easy 層 (再 export)@@ -35,22 +43,28 @@ import           Graphics.Hgg.Backend.SVG (saveSVG) import           Graphics.Hgg.Easy --- | 散布図を 1 行で SVG 保存。 @quickScatter path xs ys@。+-- | [日本語]: 散布図を 1 行で SVG 保存。 @quickScatter path xs ys@。+--   [English]: Saves a scatter plot to SVG in one line. @quickScatter path xs ys@. quickScatter :: FilePath -> [Double] -> [Double] -> IO () quickScatter path xs ys = quickPlot path [points xs ys] --- | 折れ線を 1 行で SVG 保存。+-- | [日本語]: 折れ線を 1 行で SVG 保存。+--   [English]: Saves a line plot to SVG in one line. quickLine :: FilePath -> [Double] -> [Double] -> IO () quickLine path xs ys = quickPlot path [lineXY xs ys] --- | 棒グラフを 1 行で SVG 保存。+-- | [日本語]: 棒グラフを 1 行で SVG 保存。+--   [English]: Saves a bar chart to SVG in one line. quickBar :: FilePath -> [Double] -> [Double] -> IO () quickBar path xs ys = quickPlot path [bars xs ys] --- | ヒストグラムを 1 行で SVG 保存。+-- | [日本語]: ヒストグラムを 1 行で SVG 保存。+--   [English]: Saves a histogram to SVG in one line. quickHist :: FilePath -> [Double] -> IO () quickHist path xs = quickPlot path [hist xs] --- | layer 群を 'overlay' で重畳して SVG 保存 (= 最も汎用な Easy 保存)。+-- | [日本語]: layer 群を 'overlay' で重畳して SVG 保存 (= 最も汎用な Easy 保存)。+--   [English]: Overlays a list of layers with 'overlay' and saves the result+--   to SVG (the most general Easy-layer save). quickPlot :: FilePath -> [Layer] -> IO () quickPlot path = saveSVG path . overlay
test/Spec.hs view
@@ -1,9 +1,13 @@ -- | hgg-svg テスト。 Phase 14 A3 = BoundPlot (df |>> spec) → SVG smoke。+-- Phase 64 A7 = PClipPath (多角形 clip) の SVG 出力。 {-# LANGUAGE OverloadedStrings #-} module Main (main) where -import           Graphics.Hgg.Backend.SVG (renderBound)+import           Graphics.Hgg.Backend.SVG (renderBound, renderPrimitivesSVG) import           Graphics.Hgg.Frame       ((|>>))+import           Graphics.Hgg.Render      (FillStyle (..), Point (..),+                                           Primitive (..))+import           Graphics.Hgg.Layout      (Rect (..)) import           Graphics.Hgg.Spec        (ColData (..), layer, scatter) import           Data.Map.Strict          (Map) import qualified Data.Map.Strict          as M@@ -22,8 +26,19 @@ xyMap :: Map Text ColData xyMap = M.fromList xyAssoc +-- Phase 64 A7: 三角形 clip の中に全面 rect を 1 枚置いた最小列。+triangle :: [Point]+triangle = [Point 100 20, Point 180 180, Point 20 180]++clipped :: [Point] -> [Primitive]+clipped pts =+  [ PClipPath pts+  , PRect (Rect 0 0 200 200) (FillStyle "#ff0000" 1) Nothing+  , PClipPop+  ]+ main :: IO ()-main = hspec $+main = hspec $ do   describe "renderBound (df |>> spec)" $ do     it "df |>> spec が SVG を出す" $ do       let svg = renderBound (xyMap |>> layer (scatter "x" "y"))@@ -33,3 +48,32 @@       renderBound (xyMap   |>> layer (scatter "x" "y"))         `shouldBe`       renderBound (xyAssoc |>> layer (scatter "x" "y"))++  describe "Phase 64 A7: PClipPath (多角形 clip)" $ do+    it "clipPath + polygon 要素を出し、 <g clip-path> で囲う" $ do+      let svg = renderPrimitivesSVG 200 200 "" (clipped triangle)+      svg `shouldSatisfy` T.isInfixOf "<clipPath id=\"clip0\"><polygon points=\""+      svg `shouldSatisfy` T.isInfixOf "100.0,20.0 180.0,180.0 20.0,180.0"+      svg `shouldSatisfy` T.isInfixOf "<g clip-path=\"url(#clip0)\">"+      svg `shouldSatisfy` T.isInfixOf "</g>"++    it "頂点 3 点未満は clip 無し (polygon を出さず素通し)" $ do+      let svg = renderPrimitivesSVG 200 200 ""+                  (clipped [Point 10 10, Point 20 20])+      svg `shouldSatisfy` (not . T.isInfixOf "<polygon")+      svg `shouldSatisfy` (not . T.isInfixOf "clipPath")+      -- 中身は clip されずそのまま描かれる+      svg `shouldSatisfy` T.isInfixOf "#ff0000"++    it "<g> の開閉が釣り合う (退化列でも)" $ do+      let count needle = length . T.breakOnAll needle+          balanced ps  = let svg = renderPrimitivesSVG 200 200 "" (clipped ps)+                         in count "<g" svg `shouldBe` count "</g>" svg+      balanced triangle+      balanced [Point 10 10, Point 20 20]++    it "clip primitive の無い列には clipPath も <g> も出ない (既存図ゼロ diff)" $ do+      let plain = [PRect (Rect 0 0 200 200) (FillStyle "#ff0000" 1) Nothing]+          svg   = renderPrimitivesSVG 200 200 "" plain+      svg `shouldSatisfy` (not . T.isInfixOf "clipPath")+      svg `shouldSatisfy` (not . T.isInfixOf "<g")