hgg-dataframe (empty) → 0.1.0.0
raw patch · 5 files changed
+315/−0 lines, 5 filesdep +basedep +containersdep +dataframe-core
Dependencies added: base, containers, dataframe-core, dataframe-operations, directory, filepath, hgg-core, hgg-dataframe, hgg-frame, hgg-svg, hspec, text, vector
Files
- CHANGELOG.md +8/−0
- LICENSE +30/−0
- hgg-dataframe.cabal +65/−0
- src/Graphics/Hgg/DataFrame.hs +113/−0
- test/Spec.hs +99/−0
+ CHANGELOG.md view
@@ -0,0 +1,8 @@+# Changelog for `hgg-dataframe`++## 0.1.0.0 — 2026-08-05++First public release on Hackage.++- `PlotData` instance for the Hackage `dataframe` package's `DataFrame`,+ so `df |>> spec` works directly on a `DataFrame` read by `readCsv`.
+ LICENSE view
@@ -0,0 +1,30 @@+BSD 3-Clause License++Copyright (c) 2026, Toshiaki Honda+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+ contributors may be used to endorse or promote products derived from this+ software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ hgg-dataframe.cabal view
@@ -0,0 +1,65 @@+cabal-version: 3.0+name: hgg-dataframe+version: 0.1.0.0+extra-doc-files: CHANGELOG.md+synopsis: Hackage dataframe binding for hgg (PlotData instance for DataFrame)+description:+ Bridges the Hackage @dataframe@ package to hgg, so a @DataFrame@ can be+ plotted directly by column name.+ .+ * @instance PlotData DataFrame@ — makes @df '|>>' spec@ accept a+ @DataFrame@ (e.g. one returned by @readCsv@) with no conversion step.+ * @dfResolver@ / @plotDF@ — the underlying resolver and a one-call save+ helper.+ .+ Nullable columns are supported: a @Maybe@ column can be referenced by+ name and missing entries are dropped, mirroring ggplot2's @na.rm@.+ .+ @hgg-core@ and @hgg-frame@ stay independent of any dataframe library;+ this package is the opt-in binding for users of Hackage @dataframe@.+license: BSD-3-Clause+license-file: LICENSE+homepage: https://github.com/frenzieddoll/hgg+author: Toshiaki Honda+maintainer: frenzieddoll@gmail.com+copyright: 2026 Aelysce Project (Toshiaki Honda)+category: Graphics+build-type: Simple++common warnings+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wpartial-fields+ -Wredundant-constraints++library+ import: warnings+ exposed-modules: Graphics.Hgg.DataFrame+ hs-source-dirs: src+ build-depends: base >= 4.17 && < 5+ , text >= 2.0 && < 2.2+ , vector >= 0.13 && < 0.14+ , dataframe-core ^>= 1.1+ , dataframe-operations >= 1.1.1 && < 1.2+ , hgg-core ^>= 0.1+ , hgg-frame ^>= 0.1+ , hgg-svg ^>= 0.1+ default-language: Haskell2010++test-suite hgg-dataframe-tests+ import: warnings+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test+ build-depends: base+ , text+ , vector+ , containers+ , directory >= 1.3 && < 1.4+ , filepath >= 1.4 && < 1.6+ , dataframe-core ^>= 1.1+ , hgg-core+ , hgg-frame+ , hgg-svg+ , hgg-dataframe+ , hspec >= 2.10 && < 2.12+ default-language: Haskell2010
+ src/Graphics/Hgg/DataFrame.hs view
@@ -0,0 +1,113 @@+-- |+-- Module : Graphics.Hgg.DataFrame+-- Description : DataFrame ↔ hgg Resolver bridge (Phase 26 §A 拡張)+-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License : BSD-3-Clause+--+-- @+-- import qualified DataFrame.IO.CSV as DF+-- import Graphics.Hgg.Easy+-- import Graphics.Hgg.Backend.SVG (saveSVGWith)+-- import Graphics.Hgg.DataFrame (dfResolver, plotDF)+--+-- main = do+-- df <- DF.readCsv "data.csv"+-- plotDF "out.svg" df $+-- purePlot+-- <> layer (scatter "weight" "mpg" <> colorBy "origin")+-- <> title "燃費 vs 重量"+-- @+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+-- | 'PlotData' instance for Hackage @dataframe@ は orphan (型もクラスも当 package+-- が所有しないが、 spec-2 §3.1/§6 の設計で **橋 package が所有**する正当な配置)。+{-# OPTIONS_GHC -Wno-orphans #-}+module Graphics.Hgg.DataFrame+ ( dfResolver+ , plotDF+ -- * Phase 14: PlotData instance (df |>> spec を使えるように)+ -- $plotdata+ ) where++import Graphics.Hgg.Backend.SVG (saveSVGWith)+import Graphics.Hgg.Frame (PlotData (..))+import Graphics.Hgg.Spec (ColData (..), Resolver, VisualSpec)+import Control.Applicative ((<|>))+import Data.Text (Text)+import qualified Data.Vector as V+import qualified DataFrame.Internal.Column as DFC+import qualified DataFrame.Internal.DataFrame as DFI+import qualified DataFrame.Operations.Core as DF+import qualified DataFrame.Operators as DF++-- | 'DataFrame' から hgg の 'Resolver' を作る。+-- 列名で `Double` または `Text` の列を抽出、 数値列なら 'NumData' / 文字列列+-- なら 'TxtData' を返す。 失敗 (= 列不在 / 型不一致 / 空 DataFrame) は 'Nothing'。+dfResolver :: DFI.DataFrame -> Resolver+dfResolver df name =+ -- nullable (Maybe) 列対応: 欠損 (NA) は NaN で運び **長さを保つ** (行整列を壊さない・+ -- ggplot が na.rm で列の欠損を内部処理するのと同方針)。 消費側 (range / binning /+ -- 点描画) が NaN を弾く。 ★Maybe 版を plain より先に試す: DF.fromList で作った+ -- Maybe Int 列は plain @Int が成功して NA→0 と読んでしまう (誤り) ため、 Maybe を+ -- 優先して NA→NaN を保証する。 非 NULL 列は Maybe 版でも全要素 Just ゆえ同値 (無害)。+ fmap NumData (tryMaybeDoubleCol name df)+ <|> fmap NumData (tryMaybeIntCol name df)+ <|> fmap NumData (tryDoubleCol name df)+ <|> fmap NumData (tryIntCol name df)+ <|> fmap TxtData (tryTextCol name df)++tryDoubleCol :: Text -> DFI.DataFrame -> Maybe (V.Vector Double)+tryDoubleCol n df = safeColumnAs @Double n df++tryIntCol :: Text -> DFI.DataFrame -> Maybe (V.Vector Double)+tryIntCol n df =+ fmap (V.map fromIntegral) (safeColumnAs @Int n df)++-- | @Maybe Double@ 列: NA → NaN。+tryMaybeDoubleCol :: Text -> DFI.DataFrame -> Maybe (V.Vector Double)+tryMaybeDoubleCol n df =+ fmap (V.map (maybe (0/0) id)) (safeColumnAs @(Maybe Double) n df)++-- | @Maybe Int@ 列: NA → NaN。+tryMaybeIntCol :: Text -> DFI.DataFrame -> Maybe (V.Vector Double)+tryMaybeIntCol n df =+ fmap (V.map (maybe (0/0) fromIntegral)) (safeColumnAs @(Maybe Int) n df)++tryTextCol :: Text -> DFI.DataFrame -> Maybe (V.Vector Text)+tryTextCol n df = safeColumnAs @Text n df++-- | 列を例外セーフに 'V.Vector' として取り出す (hgg PR #1 = mchav 氏提案の+-- `columnAsVector` 版。 中間 list と unsafePerformIO/try/force を撤去)。+--+-- 先頭の `DFI.null` ガードは冗長に見えるが**消してはいけない**: `columnAsVector`+-- は列欠落・型不一致を `Left` で返す一方、 **空 DataFrame だけは `Either` に+-- 載せず純粋例外を投げる** (dataframe-operations-1.1.1.1+-- `Operations/Core.hs:825` = `throw (EmptyDataSetException ...)`)。 ここで先に+-- 'Nothing' を返して従来挙動 (空データ = 空の図、 例外なし) を保つ。+safeColumnAs+ :: forall a. (DFC.Columnable a)+ => Text -> DFI.DataFrame -> Maybe (V.Vector a)+safeColumnAs name df+ | DFI.null df = Nothing+ | otherwise = either (const Nothing) Just (DF.columnAsVector (DF.col @a name) df)++-- | DF + spec を 1 行で SVG 出力 (= matplotlib `plt.savefig` 感)。+plotDF :: FilePath -> DFI.DataFrame -> VisualSpec -> IO ()+plotDF path df spec = saveSVGWith path (dfResolver df) spec++-- $plotdata+-- Phase 14: Hackage @dataframe@ の 'DFI.DataFrame' を 'PlotData' instance に+-- することで、 @df |>> layer (scatter "x" "y")@ (df-first バインド) が使える。+-- @toResolver@ は既存 'dfResolver' を再利用、 @columnNames@/@nrows@ は dataframe+-- の API (@DFI.columnNames@ / @DFI.dataframeDimensions@) で実装する。++-- | df-first バインド ('(|>>)') / 列名検証のための instance。+--+-- * @columnNames@ = @DataFrame.columnNames@ (= 全列名)+-- * @nrows@ = @dataframeDimensions@ の第 1 要素 (= 行数。 実測: @(rows, cols)@)+instance PlotData DFI.DataFrame where+ toResolver = dfResolver+ columnNames = DFI.columnNames+ nrows = fst . DFI.dataframeDimensions
+ test/Spec.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Graphics.Hgg.Backend.SVG (renderBound)+import Graphics.Hgg.DataFrame (dfResolver, plotDF)+import Graphics.Hgg.Frame (PlotData (..), (|>>))+import Graphics.Hgg.Spec (ColData (..), layer, scatter)+import Data.List (isInfixOf)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as M+import Data.Text (Text)+import qualified Data.Vector as V+import qualified DataFrame.Internal.Column as DF+import qualified DataFrame.Internal.DataFrame as DF+import System.Directory (getTemporaryDirectory, removeFile)+import System.FilePath ((</>))+import Test.Hspec++main :: IO ()+main = hspec $ do+ describe "dfResolver" $ do+ it "Double 列を resolve" $ do+ let df = DF.fromNamedColumns+ [ ("x", DF.fromList ([1.0, 2.0, 3.0] :: [Double])) ]+ r = dfResolver df+ case r "x" of+ Just (NumData v) -> V.toList v `shouldBe` [1.0, 2.0, 3.0]+ _ -> expectationFailure "expected NumData"+ it "Int 列を resolve (= Double に変換)" $ do+ let df = DF.fromNamedColumns+ [ ("n", DF.fromList ([10, 20, 30] :: [Int])) ]+ r = dfResolver df+ case r "n" of+ Just (NumData v) -> V.toList v `shouldBe` [10.0, 20.0, 30.0]+ _ -> expectationFailure "expected NumData"+ it "存在しない列は Nothing" $ do+ let df = DF.fromNamedColumns+ [ ("x", DF.fromList ([1.0, 2.0] :: [Double])) ]+ r = dfResolver df+ r "nope" `shouldBe` Nothing+ it "nullable (Maybe Int) 列を resolve (NA → NaN・長さ保持)" $ do+ let df = DF.fromNamedColumns+ [ ("m", DF.fromList ([Just 1, Nothing, Just 3] :: [Maybe Int])) ]+ r = dfResolver df+ case r "m" of+ Just (NumData v) -> do+ length (V.toList v) `shouldBe` 3+ (V.toList v !! 0, V.toList v !! 2) `shouldBe` (1.0, 3.0)+ isNaN (V.toList v !! 1) `shouldBe` True -- NA は NaN+ _ -> expectationFailure "expected NumData for Maybe Int column"+ it "nullable (Maybe Double) 列を resolve" $ do+ let df = DF.fromNamedColumns+ [ ("d", DF.fromList ([Just 1.5, Nothing] :: [Maybe Double])) ]+ r = dfResolver df+ case r "d" of+ Just (NumData v) -> do+ V.toList v !! 0 `shouldBe` 1.5+ isNaN (V.toList v !! 1) `shouldBe` True+ _ -> expectationFailure "expected NumData for Maybe Double column"++ -- Phase 61: safeColumnAs を columnAsVector 版へ置換した回帰 (§0 実測 4 ケース ++ -- end-to-end)。 とくに空 DataFrame は columnAsVector が Left でなく純粋例外を+ -- 投げる唯一の入力 (dataframe-operations Operations/Core.hs:825) なので、+ -- 「Nothing が返り例外が漏れない」 ことが本 Phase の要。+ describe "dfResolver 回帰 (Phase 61 = columnAsVector 化)" $ do+ it "型不一致: Text 列は TxtData として resolve (数値型 4 試行が例外を投げず Nothing で通過)" $ do+ let df = DF.fromNamedColumns+ [ ("s", DF.fromList (["a", "b"] :: [Text])) ]+ r = dfResolver df+ case r "s" of+ Just (TxtData v) -> V.toList v `shouldBe` ["a", "b"]+ _ -> expectationFailure "expected TxtData"+ it "空 DataFrame は Nothing (EmptyDataSetException を透過させない)" $ do+ let r = dfResolver DF.empty+ r "x" `shouldBe` Nothing+ it "空 DataFrame を plotDF に渡して例外なく SVG が書ける (end-to-end)" $ do+ tmp <- getTemporaryDirectory+ let path = tmp </> "hgg-df-empty-test.svg"+ plotDF path DF.empty (layer (scatter "x" "y"))+ svg <- readFile path+ ("<svg" `isInfixOf` svg) `shouldBe` True -- 空の図が出る = 従来挙動+ removeFile path++ describe "PlotData DataFrame instance (Phase 14 A4)" $ do+ let df = DF.fromNamedColumns+ [ ("x", DF.fromList ([1.0, 2.0, 3.0] :: [Double]))+ , ("y", DF.fromList ([4.0, 5.0, 6.0] :: [Double])) ]+ it "columnNames は全列" $+ columnNames df `shouldBe` ["x", "y"]+ it "nrows は行数 (dataframeDimensions の fst)" $+ nrows df `shouldBe` 3+ it "Map と DataFrame で同一 SVG (同データ、 spec-2 §7)" $ do+ let mapDF :: Map Text ColData+ mapDF = M.fromList+ [ ("x", NumData (V.fromList [1.0, 2.0, 3.0]))+ , ("y", NumData (V.fromList [4.0, 5.0, 6.0])) ]+ renderBound (df |>> layer (scatter "x" "y"))+ `shouldBe`+ renderBound (mapDF |>> layer (scatter "x" "y"))