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 +6/−0
- examples/DocFig/Decoration.hs +55/−0
- examples/DocFig/Layers.hs +6/−4
- examples/GalleryDemo.hs +61/−1
- hgg-svg.cabal +23/−23
- src/Graphics/Hgg/Backend/SVG.hs +111/−37
- src/Graphics/Hgg/Quick.hs +24/−10
- test/Spec.hs +46/−2
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")