{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
A composable, expression-based grammar of graphics over the untyped 'Expr'.
Charts are built by piping combinators and rendered to interactive Vega-Lite.

@
import qualified DataFrame.Display.Web.Chart as Plt
import DataFrame.Functions (col)

Plt.showChart
  ( Plt.chart df
      |> Plt.mark Plt.Point
      |> Plt.enc Plt.X (col \@Double "age")
      |> Plt.enc Plt.Y (col \@Double "fare")
      |> Plt.enc Plt.Color (col \@Text "class")
  )
@

The element type of each encoded expression determines the Vega-Lite field
type (numeric → quantitative, date/time → temporal, otherwise nominal). A bare
column expression encodes that column; any other expression is materialised
with the core interpreter and inlined under the channel's name.

For the schema-checked typed variant see "DataFrame.Display.Web.Chart.Typed".
-}
module DataFrame.Display.Web.Chart (
    -- * Chart
    Chart,
    chart,

    -- * Re-exported spec vocabulary
    Mark (..),
    Channel (..),
    FieldType (..),
    Agg (..),

    -- * Building blocks
    mark,
    enc,
    encAs,
    aggregateOn,
    binX,
    binY,
    facet,
    row,
    column,
    layer,
    regression,
    density,
    logScale,
    includeZero,
    title,
    size,

    -- * Rendering
    toVegaSpec,
    toHtml,
    showChart,

    -- * One-shot convenience plots
    scatter,
    bar,
    histogram,
    line,
    pie,
    box,
) where

import Data.Aeson (Value)
import Data.Function ((&))
import qualified Data.Text as T

import DataFrame.Display.Internal.Common (Agg (..))
import DataFrame.Display.Internal.VegaLite (
    Channel (..),
    ChannelEnc (..),
    FieldType (..),
    Mark (..),
    ResolvedField (..),
    ScaleSpec (..),
    ScaleType (..),
    Transform (DensityT, RegressionT),
    VLSpec (..),
    chanEnc,
    channelName,
    emptySpec,
    resolveField,
    rowCountWarning,
    specHtml,
    specToValue,
 )
import DataFrame.Display.Web.Plot (showInDefaultBrowser)
import DataFrame.Internal.Column (Columnable)
import DataFrame.Internal.DataFrame (DataFrame)
import DataFrame.Internal.Expression (Expr)

-- ---------------------------------------------------------------------------
-- Chart builder
-- ---------------------------------------------------------------------------

{- | An in-progress chart: the source frame, a mark, channel encodings, the
data fields to inline, transforms, and (for layered charts) sub-layers.
-}
data Chart = Chart
    { Chart -> DataFrame
chDf :: DataFrame
    , Chart -> Mark
chMark :: Mark
    , Chart -> [ChannelEnc]
chEncs :: [ChannelEnc]
    , Chart -> [ResolvedField]
chFields :: [ResolvedField]
    , Chart -> [Transform]
chTransforms :: [Transform]
    , Chart -> Maybe Text
chTitle :: Maybe T.Text
    , Chart -> Int
chW :: Int
    , Chart -> Int
chH :: Int
    , Chart -> [Chart]
chLayers :: [Chart]
    }

-- | Start a chart from a frame. Defaults to a point mark at 600x400.
chart :: DataFrame -> Chart
chart :: DataFrame -> Chart
chart DataFrame
df = DataFrame
-> Mark
-> [ChannelEnc]
-> [ResolvedField]
-> [Transform]
-> Maybe Text
-> Int
-> Int
-> [Chart]
-> Chart
Chart DataFrame
df Mark
Point [] [] [] Maybe Text
forall a. Maybe a
Nothing Int
600 Int
400 []

-- | Set the mark type.
mark :: Mark -> Chart -> Chart
mark :: Mark -> Chart -> Chart
mark Mark
m Chart
c = Chart
c{chMark = m}

{- | Encode an expression on a channel. The field type is inferred from the
expression's element type; use 'encAs' to override.
-}
enc :: (Columnable a) => Channel -> Expr a -> Chart -> Chart
enc :: forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
ch Expr a
e Chart
c =
    let rf :: ResolvedField
rf = DataFrame -> Text -> Expr a -> ResolvedField
forall a.
Columnable a =>
DataFrame -> Text -> Expr a -> ResolvedField
resolveField (Chart -> DataFrame
chDf Chart
c) (Channel -> Text
channelName Channel
ch) Expr a
e
        ce :: ChannelEnc
ce = Channel -> Text -> FieldType -> ChannelEnc
chanEnc Channel
ch (ResolvedField -> Text
rfName ResolvedField
rf) (ResolvedField -> FieldType
rfType ResolvedField
rf)
     in Chart
c{chEncs = setChannel ce (chEncs c), chFields = chFields c ++ [rf]}

-- | Like 'enc' but force the Vega-Lite field type (e.g. 'Temporal', 'Ordinal').
encAs :: (Columnable a) => Channel -> Expr a -> FieldType -> Chart -> Chart
encAs :: forall a.
Columnable a =>
Channel -> Expr a -> FieldType -> Chart -> Chart
encAs Channel
ch Expr a
e FieldType
ft Chart
c =
    let rf :: ResolvedField
rf = DataFrame -> Text -> Expr a -> ResolvedField
forall a.
Columnable a =>
DataFrame -> Text -> Expr a -> ResolvedField
resolveField (Chart -> DataFrame
chDf Chart
c) (Channel -> Text
channelName Channel
ch) Expr a
e
        ce :: ChannelEnc
ce = Channel -> Text -> FieldType -> ChannelEnc
chanEnc Channel
ch (ResolvedField -> Text
rfName ResolvedField
rf) FieldType
ft
     in Chart
c{chEncs = setChannel ce (chEncs c), chFields = chFields c ++ [rf]}

{- | Apply a declarative aggregation to a channel (computed by Vega-Lite). For
'Count' the field is dropped, matching Vega-Lite's fieldless count.
-}
aggregateOn :: Channel -> Agg -> Chart -> Chart
aggregateOn :: Channel -> Agg -> Chart -> Chart
aggregateOn Channel
ch Agg
a =
    Channel -> (ChannelEnc -> ChannelEnc) -> Chart -> Chart
withChannel
        Channel
ch
        ( \ChannelEnc
e ->
            ChannelEnc
e
                { ceAggregate = Just (aggVegaName a)
                , ceField = if a == Count then "" else ceField e
                }
        )

-- | Bin the X (resp. Y) channel (Vega-Lite @bin@ transform).
binX, binY :: Chart -> Chart
binX :: Chart -> Chart
binX = Channel -> (ChannelEnc -> ChannelEnc) -> Chart -> Chart
withChannel Channel
X (\ChannelEnc
e -> ChannelEnc
e{ceBin = True})
binY :: Chart -> Chart
binY = Channel -> (ChannelEnc -> ChannelEnc) -> Chart -> Chart
withChannel Channel
Y (\ChannelEnc
e -> ChannelEnc
e{ceBin = True})

{- | Put a channel on a log scale. Like all channel modifiers, apply it
after 'enc' on the same channel ('enc' replaces the whole encoding).
-}
logScale :: Channel -> Chart -> Chart
logScale :: Channel -> Chart -> Chart
logScale Channel
ch = Channel -> (ScaleSpec -> ScaleSpec) -> Chart -> Chart
withScale Channel
ch (\ScaleSpec
s -> ScaleSpec
s{scaleType = Just LogS})

{- | Anchor (@True@) or release (@False@) a channel's scale at zero.
Vega-Lite includes zero on quantitative scales by default, which squashes
data far from the origin (e.g. geographic coordinates) against the chart
edge; @includeZero X False@ lets the axis fit the data instead. Apply it
after 'enc' on the same channel ('enc' replaces the whole encoding).
-}
includeZero :: Channel -> Bool -> Chart -> Chart
includeZero :: Channel -> Bool -> Chart -> Chart
includeZero Channel
ch Bool
b = Channel -> (ScaleSpec -> ScaleSpec) -> Chart -> Chart
withScale Channel
ch (\ScaleSpec
s -> ScaleSpec
s{scaleZero = Just b})

-- | Facet into small multiples by a column (alias for 'column').
facet :: (Columnable a) => Expr a -> Chart -> Chart
facet :: forall a. Columnable a => Expr a -> Chart -> Chart
facet = Expr a -> Chart -> Chart
forall a. Columnable a => Expr a -> Chart -> Chart
column

-- | Facet across columns / down rows.
column, row :: (Columnable a) => Expr a -> Chart -> Chart
column :: forall a. Columnable a => Expr a -> Chart -> Chart
column = Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
Column
row :: forall a. Columnable a => Expr a -> Chart -> Chart
row = Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
Row

-- | Set the chart title.
title :: T.Text -> Chart -> Chart
title :: Text -> Chart -> Chart
title Text
t Chart
c = Chart
c{chTitle = Just t}

-- | Set the chart size in pixels.
size :: Int -> Int -> Chart -> Chart
size :: Int -> Int -> Chart -> Chart
size Int
w Int
h Chart
c = Chart
c{chW = w, chH = h}

{- | Overlay several charts that share data into a single layered chart. The
title and size of the first layer are used for the container.
-}
layer :: [Chart] -> Chart
layer :: [Chart] -> Chart
layer [] = [Char] -> Chart
forall a. HasCallStack => [Char] -> a
error [Char]
"DataFrame.Display.Web.Chart.layer: empty layer list"
layer cs :: [Chart]
cs@(Chart
c0 : [Chart]
_) =
    Chart
c0
        { chLayers = map (\Chart
c -> Chart
c{chLayers = []}) cs
        , chFields = concatMap chFields cs
        }

{- | Add a regression (least-squares) line over the chart, fitting @y@ on @x@.
Produces a layered chart: the original marks plus a fitted line.
-}
regression :: (Columnable a, Columnable b) => Expr a -> Expr b -> Chart -> Chart
regression :: forall a b.
(Columnable a, Columnable b) =>
Expr a -> Expr b -> Chart -> Chart
regression Expr a
xE Expr b
yE Chart
c =
    let rfx :: ResolvedField
rfx = DataFrame -> Text -> Expr a -> ResolvedField
forall a.
Columnable a =>
DataFrame -> Text -> Expr a -> ResolvedField
resolveField (Chart -> DataFrame
chDf Chart
c) Text
"x" Expr a
xE
        rfy :: ResolvedField
rfy = DataFrame -> Text -> Expr b -> ResolvedField
forall a.
Columnable a =>
DataFrame -> Text -> Expr a -> ResolvedField
resolveField (Chart -> DataFrame
chDf Chart
c) Text
"y" Expr b
yE
        withData :: Chart
withData = Chart
c{chFields = chFields c ++ [rfx, rfy]}
        points :: Chart
points = Chart
withData{chLayers = []}
        lineLayer :: Chart
lineLayer =
            Chart
withData
                { chMark = Line
                , chEncs =
                    [ chanEnc X (rfName rfx) Quantitative
                    , chanEnc Y (rfName rfy) Quantitative
                    ]
                , chTransforms = [RegressionT (rfName rfy) (rfName rfx)]
                , chLayers = []
                }
     in Chart
withData{chLayers = [points, lineLayer]}

{- | Kernel-density estimate of an expression, drawn as an area. Replaces the
chart's mark and encodings with the density curve (Vega-Lite @density@).
-}
density :: (Columnable a) => Expr a -> Chart -> Chart
density :: forall a. Columnable a => Expr a -> Chart -> Chart
density Expr a
e Chart
c =
    let rf :: ResolvedField
rf = DataFrame -> Text -> Expr a -> ResolvedField
forall a.
Columnable a =>
DataFrame -> Text -> Expr a -> ResolvedField
resolveField (Chart -> DataFrame
chDf Chart
c) Text
"value" Expr a
e
     in Chart
c
            { chFields = chFields c ++ [rf]
            , chMark = Area
            , chTransforms = chTransforms c ++ [DensityT (rfName rf)]
            , chEncs =
                [ chanEnc X "value" Quantitative
                , chanEnc Y "density" Quantitative
                ]
            }

-- ---------------------------------------------------------------------------
-- Rendering
-- ---------------------------------------------------------------------------

-- | The Vega-Lite spec as an aeson 'Value' (escape hatch for advanced use / hvega).
toVegaSpec :: Chart -> Value
toVegaSpec :: Chart -> Value
toVegaSpec Chart
c = [ResolvedField] -> VLSpec -> Value
specToValue (Chart -> [ResolvedField]
allFields Chart
c) (Chart -> VLSpec
toVLSpec Chart
c)

-- | A self-contained HTML snippet embedding the chart.
toHtml :: Chart -> String
toHtml :: Chart -> [Char]
toHtml Chart
c = Text -> [Char]
T.unpack (Text -> [ResolvedField] -> VLSpec -> Text
specHtml Text
"vis" (Chart -> [ResolvedField]
allFields Chart
c) (Chart -> VLSpec
toVLSpec Chart
c))

-- | Render the chart to a temp file and open it in the default browser.
showChart :: Chart -> IO ()
showChart :: Chart -> IO ()
showChart Chart
c = do
    [ResolvedField] -> IO ()
rowCountWarning (Chart -> [ResolvedField]
allFields Chart
c)
    [Char] -> IO ()
showInDefaultBrowser (Chart -> [Char]
toHtml Chart
c)

-- ---------------------------------------------------------------------------
-- One-shot convenience plots (the simple single-line path)
-- ---------------------------------------------------------------------------

-- | Scatter plot of two expressions. Axes fit the data (no zero anchor).
scatter ::
    (Columnable a, Columnable b) => Expr a -> Expr b -> DataFrame -> IO ()
scatter :: forall a b.
(Columnable a, Columnable b) =>
Expr a -> Expr b -> DataFrame -> IO ()
scatter Expr a
xE Expr b
yE DataFrame
df =
    Chart -> IO ()
showChart
        ( DataFrame -> Chart
chart DataFrame
df
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Mark -> Chart -> Chart
mark Mark
Point
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
X Expr a
xE
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr b -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
Y Expr b
yE
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Bool -> Chart -> Chart
includeZero Channel
X Bool
False
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Bool -> Chart -> Chart
includeZero Channel
Y Bool
False
        )

-- | Count of rows per category, as bars.
bar :: (Columnable a) => Expr a -> DataFrame -> IO ()
bar :: forall a. Columnable a => Expr a -> DataFrame -> IO ()
bar Expr a
xE DataFrame
df = Chart -> IO ()
showChart (DataFrame -> Chart
chart DataFrame
df Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Mark -> Chart -> Chart
mark Mark
Bar Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
X Expr a
xE Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Agg -> Chart -> Chart
aggregateOn Channel
Y Agg
Count)

-- | Histogram of a numeric expression (Vega-Lite binning + count).
histogram :: (Columnable a) => Expr a -> DataFrame -> IO ()
histogram :: forall a. Columnable a => Expr a -> DataFrame -> IO ()
histogram Expr a
xE DataFrame
df =
    Chart -> IO ()
showChart (DataFrame -> Chart
chart DataFrame
df Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Mark -> Chart -> Chart
mark Mark
Bar Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
X Expr a
xE Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Chart -> Chart
binX Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Agg -> Chart -> Chart
aggregateOn Channel
Y Agg
Count)

-- | Line chart of @y@ over @x@. Axes fit the data (no zero anchor).
line :: (Columnable a, Columnable b) => Expr a -> Expr b -> DataFrame -> IO ()
line :: forall a b.
(Columnable a, Columnable b) =>
Expr a -> Expr b -> DataFrame -> IO ()
line Expr a
xE Expr b
yE DataFrame
df =
    Chart -> IO ()
showChart
        ( DataFrame -> Chart
chart DataFrame
df
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Mark -> Chart -> Chart
mark Mark
Line
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
X Expr a
xE
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr b -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
Y Expr b
yE
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Bool -> Chart -> Chart
includeZero Channel
X Bool
False
            Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Bool -> Chart -> Chart
includeZero Channel
Y Bool
False
        )

-- | Pie chart counting rows per category.
pie :: (Columnable a) => Expr a -> DataFrame -> IO ()
pie :: forall a. Columnable a => Expr a -> DataFrame -> IO ()
pie Expr a
cE DataFrame
df = Chart -> IO ()
showChart (DataFrame -> Chart
chart DataFrame
df Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Mark -> Chart -> Chart
mark Mark
Arc Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
Color Expr a
cE Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Agg -> Chart -> Chart
aggregateOn Channel
Theta Agg
Count)

-- | Box-and-whisker plot of a numeric expression.
box :: (Columnable a) => Expr a -> DataFrame -> IO ()
box :: forall a. Columnable a => Expr a -> DataFrame -> IO ()
box Expr a
yE DataFrame
df = Chart -> IO ()
showChart (DataFrame -> Chart
chart DataFrame
df Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Mark -> Chart -> Chart
mark Mark
Boxplot Chart -> (Chart -> Chart) -> Chart
forall a b. a -> (a -> b) -> b
& Channel -> Expr a -> Chart -> Chart
forall a. Columnable a => Channel -> Expr a -> Chart -> Chart
enc Channel
Y Expr a
yE)

-- ---------------------------------------------------------------------------
-- Internals
-- ---------------------------------------------------------------------------

-- | Replace any existing encoding on the same channel (last write wins).
setChannel :: ChannelEnc -> [ChannelEnc] -> [ChannelEnc]
setChannel :: ChannelEnc -> [ChannelEnc] -> [ChannelEnc]
setChannel ChannelEnc
ce [ChannelEnc]
encs = (ChannelEnc -> Bool) -> [ChannelEnc] -> [ChannelEnc]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Channel -> Channel -> Bool
forall a. Eq a => a -> a -> Bool
/= ChannelEnc -> Channel
ceChannel ChannelEnc
ce) (Channel -> Bool) -> (ChannelEnc -> Channel) -> ChannelEnc -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ChannelEnc -> Channel
ceChannel) [ChannelEnc]
encs [ChannelEnc] -> [ChannelEnc] -> [ChannelEnc]
forall a. [a] -> [a] -> [a]
++ [ChannelEnc
ce]

-- | Modify the encoding on a channel, creating a fieldless one if absent.
withChannel :: Channel -> (ChannelEnc -> ChannelEnc) -> Chart -> Chart
withChannel :: Channel -> (ChannelEnc -> ChannelEnc) -> Chart -> Chart
withChannel Channel
ch ChannelEnc -> ChannelEnc
f Chart
c =
    let encs :: [ChannelEnc]
encs = Chart -> [ChannelEnc]
chEncs Chart
c
     in if (ChannelEnc -> Bool) -> [ChannelEnc] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ((Channel -> Channel -> Bool
forall a. Eq a => a -> a -> Bool
== Channel
ch) (Channel -> Bool) -> (ChannelEnc -> Channel) -> ChannelEnc -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ChannelEnc -> Channel
ceChannel) [ChannelEnc]
encs
            then Chart
c{chEncs = map (\ChannelEnc
e -> if ChannelEnc -> Channel
ceChannel ChannelEnc
e Channel -> Channel -> Bool
forall a. Eq a => a -> a -> Bool
== Channel
ch then ChannelEnc -> ChannelEnc
f ChannelEnc
e else ChannelEnc
e) encs}
            else Chart
c{chEncs = encs ++ [f (chanEnc ch "" Quantitative)]}

-- | Modify a channel's scale spec (creating the encoding if absent).
withScale :: Channel -> (ScaleSpec -> ScaleSpec) -> Chart -> Chart
withScale :: Channel -> (ScaleSpec -> ScaleSpec) -> Chart -> Chart
withScale Channel
ch ScaleSpec -> ScaleSpec
f = Channel -> (ChannelEnc -> ChannelEnc) -> Chart -> Chart
withChannel Channel
ch (\ChannelEnc
e -> ChannelEnc
e{ceScale = f (ceScale e)})

aggVegaName :: Agg -> T.Text
aggVegaName :: Agg -> Text
aggVegaName Agg
a = case Agg
a of
    Agg
Count -> Text
"count"
    Agg
Sum -> Text
"sum"
    Agg
Mean -> Text
"mean"
    Agg
Median -> Text
"median"
    Agg
Min -> Text
"min"
    Agg
Max -> Text
"max"

allFields :: Chart -> [ResolvedField]
allFields :: Chart -> [ResolvedField]
allFields Chart
c = Chart -> [ResolvedField]
chFields Chart
c [ResolvedField] -> [ResolvedField] -> [ResolvedField]
forall a. [a] -> [a] -> [a]
++ (Chart -> [ResolvedField]) -> [Chart] -> [ResolvedField]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Chart -> [ResolvedField]
allFields (Chart -> [Chart]
chLayers Chart
c)

toVLSpec :: Chart -> VLSpec
toVLSpec :: Chart -> VLSpec
toVLSpec Chart
c
    | [Chart] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null (Chart -> [Chart]
chLayers Chart
c) =
        (Mark -> VLSpec
emptySpec (Chart -> Mark
chMark Chart
c))
            { vlEncodings = chEncs c
            , vlTransforms = chTransforms c
            , vlTitle = chTitle c
            , vlWidth = chW c
            , vlHeight = chH c
            }
    | Bool
otherwise =
        (Mark -> VLSpec
emptySpec (Chart -> Mark
chMark Chart
c))
            { vlLayers = map toVLSpec (chLayers c)
            , vlTitle = chTitle c
            , vlWidth = chW c
            , vlHeight = chH c
            }