GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — dataframe/dataframe-viz at main · DataHaskell/dataframe

Expand file tree

/

dataframe-viz

/
Copy path

Directory actions

More options

More options

Directory actions

More options

More options

Latest commit

 

History

History
History
/

dataframe-viz

/
Copy path

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
docs/base_scripts
docs/base_scripts
 
 
src/DataFrame
src/DataFrame
 
 
LICENSE
LICENSE
 
 
README.md
README.md
 
 
dataframe-viz.cabal
dataframe-viz.cabal
 
 

README.md

Outline

dataframe-viz

Plotting for the dataframe ecosystem. Two backends share one API shape:

  • Terminal (DataFrame.Display.Terminal.Plot) draws straight to the console (built on granite).
  • Web emits an interactive Vega-Lite v5 spec rendered in the browser via vega-embed — a composable grammar of graphics (facet, layer, regression, density, colour/size encodings) driven by expressions, untyped or typed.

This README is a runnable scripths notebook. Every Haskell block runs top-to-bottom in one shared session. Reproduce every output below with scripths docs/base_scripts/base_readme.md -o README.md run from dataframe-viz/.

Setup

Charts emit a Vega-Lite spec; in the REPL showInDefaultBrowser / showChart write it to a temp file and open it. To keep the output here small we print the spec without its inlined data via a tiny grammar helper, against this in-memory frame. The packages: directive builds against the local dataframe-core / dataframe-operations / dataframe-viz working trees:

-- cabal: build-depends: text, aeson
-- cabal: packages: ../dataframe-core, ../dataframe-parsing
-- cabal: packages: ../dataframe-operations, ../dataframe-viz
-- cabal: default-extensions: OverloadedStrings, TypeApplications, OverloadedLabels
-- cabal: default-extensions: DataKinds, TypeOperators, FlexibleContexts
import DataFrame.Internal.DataFrame (DataFrame, fromColumns)
import DataFrame.Internal.Column (fromList)
import DataFrame.Expression.Operators ((|>))
import qualified DataFrame.Functions as F
import DataFrame.Typed.Types (TypedDataFrame)
import DataFrame.Typed.Freeze (freeze)
import qualified DataFrame.Typed.Expr as TE
import Data.Text (Text)

import qualified DataFrame.Display.Web.Plot as Plot
import qualified DataFrame.Display.Web.Chart as Chart
import qualified DataFrame.Display.Web.Chart.Typed as TPlot

import Data.Aeson (Value (Object))
import Data.Aeson.Text (encodeToLazyText)
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Text.Lazy as TL

df = fromColumns
    [ ("income", fromList [1.5, 2.0, 3.1, 4.2, 5.0, 2.2, 3.3, 1.1 :: Double])
    , ("value",  fromList [100, 150, 200, 250, 300, 180, 220, 90 :: Double])
    , ("region", fromList (["INLAND","NEAR BAY","INLAND","NEAR OCEAN","ISLAND","INLAND","NEAR BAY","INLAND"] :: [Text]))
    ]

income = F.double "income"
value  = F.double "value"
region = F.text   "region"

-- show a Vega-Lite spec without its (verbose) inlined data.
-- Returns String so scripths prints it raw rather than show-escaped.
grammar v = case v of
    Object o -> TL.unpack (encodeToLazyText (Object (KM.delete "data" o)))
    _        -> TL.unpack (encodeToLazyText v)

Terminal plots

Terminal plots render to stdout, so they're shown here rather than run:

import qualified DataFrame.Display.Terminal.Plot as T
T.scatter (T.mkScatter "income" "value") df
T.histogram (T.mkHistogram "income") df

Web plots

Three tiers, all compiling to the same Vega-Lite spec:

Module Keyed by Use
DataFrame.Display.Web.Plot string column names quick one-liners; returns an HTML String
DataFrame.Display.Web.Chart untyped Expr composable grammar
DataFrame.Display.Web.Chart.Typed typed TExpr / TypedDataFrame same grammar, #column checked against the schema at compile time

Vocabulary (re-exported from all three):

  • Marks — Bar Line Point Area Boxplot Arc Rule Tick.
  • Channels — X Y Color Size Shape Opacity Theta Column Row Tooltip Order.
  • Field types — Quantitative Nominal Ordinal Temporal, inferred from the expression's element type:
Haskell type field type
Int, Double, Float, Word, … Quantitative
Text, String, Bool, Char Nominal
Day, UTCTime, LocalTime, … Temporal
Maybe a as a

Override the inferred type with encAs. Aggregations (aggregateOn): Count Sum Mean Median Min Max.

In the REPL or a notebook you render straight to the browser (these aren't run here):

Plot.scatter (Plot.mkScatter "income" "value") df >>= Plot.showInDefaultBrowser
Chart.showChart (Chart.chart df |> Chart.mark Chart.Point
                                |> Chart.enc Chart.X income
                                |> Chart.enc Chart.Y value)

Untyped grammar (Expr)

Build a chart by piping combinators onto chart df. A scatter with a categorical colour encoding — income :: Expr Double becomes quantitative, region :: Expr Text becomes nominal:

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Point
        |> Chart.enc Chart.X income
        |> Chart.enc Chart.Y value
        |> Chart.enc Chart.Color region))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"color":{"field":"region","type":"nominal"},"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"point"},"width":600}

Map more columns onto Size / Opacity / Shape / Tooltip:

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Point
        |> Chart.enc Chart.X income
        |> Chart.enc Chart.Y value
        |> Chart.enc Chart.Size value
        |> Chart.enc Chart.Opacity income))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"opacity":{"field":"income","type":"quantitative"},"size":{"field":"value","type":"quantitative"},"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"point"},"width":600}

aggregateOn applies a Vega-Lite aggregate to a channel. Sum value by region, coloured by region:

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Bar
        |> Chart.enc Chart.X region
        |> Chart.enc Chart.Y value
        |> Chart.aggregateOn Chart.Y Chart.Sum
        |> Chart.enc Chart.Color region))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"color":{"field":"region","type":"nominal"},"x":{"field":"region","type":"nominal"},"y":{"aggregate":"sum","field":"value","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"bar"},"width":600}

A histogram is a binned X with a counted Y — binning and counting are Vega-Lite transforms:

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Bar
        |> Chart.enc Chart.X income
        |> Chart.binX
        |> Chart.aggregateOn Chart.Y Chart.Count))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"x":{"bin":true,"field":"income","type":"quantitative"},"y":{"aggregate":"count","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"bar"},"width":600}

A line:

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Line
        |> Chart.enc Chart.X income
        |> Chart.enc Chart.Y value))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"line"},"width":600}

encAs forces a field type; logScale puts a channel on a log scale:

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Point
        |> Chart.encAs Chart.X income Chart.Ordinal
        |> Chart.enc Chart.Y value
        |> Chart.logScale Chart.Y))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"x":{"field":"income","type":"ordinal"},"y":{"field":"value","scale":{"type":"log"},"type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"point"},"width":600}

The medium is expressions, not just column names. A non-column expression is evaluated and inlined under the channel's name (here y):

grammar (Chart.toVegaSpec
    (Chart.chart df
        |> Chart.mark Chart.Point
        |> Chart.enc Chart.X income
        |> Chart.enc Chart.Y (value + income)))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"x":{"field":"income","type":"quantitative"},"y":{"field":"y","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"point"},"width":600}

regression overlays a least-squares line (a second layer) and facet splits into small multiples:

grammar (Chart.toVegaSpec
    (Chart.regression income value
        (Chart.chart df
            |> Chart.mark Chart.Point
            |> Chart.enc Chart.X income
            |> Chart.enc Chart.Y value
            |> Chart.facet region)))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","height":400,"layer":[{"encoding":{"column":{"field":"region","type":"nominal"},"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"mark":{"tooltip":true,"type":"point"}},{"encoding":{"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"mark":{"tooltip":true,"type":"line"},"transform":[{"on":"income","regression":"value"}]}],"width":600}

density draws a kernel-density estimate as an area:

grammar (Chart.toVegaSpec
    (Chart.density income (Chart.chart df |> Chart.mark Chart.Area)))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"x":{"field":"value","type":"quantitative"},"y":{"field":"density","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"area"},"transform":[{"density":"income"}],"width":600}

layer overlays charts that share data:

grammar (Chart.toVegaSpec
    (Chart.layer
        [ Chart.chart df |> Chart.mark Chart.Point |> Chart.enc Chart.X income |> Chart.enc Chart.Y value
        , Chart.chart df |> Chart.mark Chart.Line  |> Chart.enc Chart.X income |> Chart.enc Chart.Y value
        ]))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","height":400,"layer":[{"encoding":{"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"mark":{"tooltip":true,"type":"point"}},{"encoding":{"x":{"field":"income","type":"quantitative"},"y":{"field":"value","type":"quantitative"}},"mark":{"tooltip":true,"type":"line"}}],"width":600}

title and size set the chart title and pixel dimensions.

Typed grammar (TExpr)

DataFrame.Display.Web.Chart.Typed mirrors every combinator above, over a TypedDataFrame, so #region / #value are checked against the schema at compile time. box draws a box-and-whisker (quartiles, 1.5×IQR whiskers, outliers):

type Cols = '[ '("income", Double), '("value", Double), '("region", Text)]

case freeze @Cols df of
    Nothing  -> "schema mismatch"
    Just tdf -> grammar (TPlot.toVegaSpec
        (TPlot.chart tdf
            |> TPlot.mark TPlot.Boxplot
            |> TPlot.enc TPlot.X #region
            |> TPlot.enc TPlot.Y #value))

{"$schema":"https://vega.github.io/schema/vega-lite/v5.json","encoding":{"x":{"field":"region","type":"nominal"},"y":{"field":"value","type":"quantitative"}},"height":400,"mark":{"tooltip":true,"type":"boxplot"},"width":600}

A typed one-liner mirrors the string tier, but the labels must exist in the schema (not run here):

TPlot.scatter #income #value tdf

Rendering

Every tier produces the same outputs:

  • toVegaSpec :: Chart -> Value — the Vega-Lite spec as an aeson Value. Escape hatch for advanced use, or hand-off to hvega, which speaks the same spec.
  • toHtml :: Chart -> String — a self-contained HTML snippet (CDN vega-embed, data inlined, so it renders from a file:// URL).
  • showChart :: Chart -> IO () — write the HTML to a temp file and open the browser.
  • showInDefaultBrowser :: String -> IO () — open an HTML String (the string tier returns these).

Frames over ~5,000 rows print a stderr warning, since the data is inlined into the spec.

String tier (one-shots)

DataFrame.Display.Web.Plot is the quick path; each call returns an HTML String (not run here):

Plot.bar       (Plot.mkBar "region")              df   -- count rows per region
Plot.histogram (Plot.mkHistogram "income")        df
Plot.scatter   (Plot.mkScatter "income" "value")  df
Plot.line      (Plot.mkLine "income" ["value"])   df
Plot.pie       (Plot.mkPie "region")              df
Plot.box       (Plot.mkBox ["income", "value"])   df

Override defaults with record syntax on the spec: Bar has y, agg, topN, title, size; Histogram has bins; Scatter has color; Pie has names, agg, topN; Box / Line take a list of columns. E.g. bar (mkBar "region") { y = Just "value", agg = Sum, topN = Just 5 } df.

Install

build-depends: dataframe-viz

The plotting modules are also re-exported from the umbrella dataframe package (DataFrame.Display.Web.Plot, DataFrame.Display.Web.Chart, DataFrame.Display.Web.Chart.Typed).

You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.