packages feed

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 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"))