BSD-3-Clause licensed by Tony Day
Maintained by [email protected]
This version can be pinned in stack with:chart-svg-0.5.1.0@sha256:493b2ded3ee98aec8cdd8b9e0d2b16266b4ad98af625884ea04209b02c146288,4845
#+TITLE: chart-svg

[[https://hackage.haskell.org/package/chart-svg][file:https://img.shields.io/hackage/v/chart-svg.svg]] [[https://github.com/tonyday567/chart-svg/actions?query=workflow%3Ahaskell-ci][file:https://github.com/tonyday567/chart-svg/workflows/haskell-ci/badge.svg]]

[[file:other/banner.svg]]

A charting library targetting SVG.

* Usage

#+begin_src haskell :file other/usage.svg :results output graphics file :exports both
:set -XOverloadedLabels
:set -XOverloadedStrings
import Chart
import Optics.Core
lines = [[Point 0.0 1.0, Point 1.0 1.0, Point 2.0 5.0],[Point 0.0 0.0, Point 2.8 3.0],[Point 0.5 4.0, Point 0.5 0]]
styles = (\c -> defaultLineStyle & #color .~ palette1 c & #size .~ 0.015) <$> [0..2]
cs = zipWith (\s x -> LineChart s [x]) styles lines
lineExample = mempty & #charts .~ named "line" cs & #hudOptions .~ defaultHudOptions :: ChartOptions
writeChartOptions "other/usage.svg" lineExample
#+end_src

#+RESULTS:
[[file:other/usage.svg]]

See the haddock documentation for a detailed overview.

* Related projects

Downstream projects, where usage of chart-svg in various states of development can be found, include:

[[https://github.com/tonyday567/color-adjust][color-adjust]] - experimenting with the [[https://bottosson.github.io/posts/oklab/][oklab]] colour space. Some of this has been incorporated in to chart-svg.

[[https://github.com/tonyday567/dotparse][dotparse]] - a chart-svg <-> graphviz bridge

[[https://github.com/tonyday567/prettychart][prettychart]] - a chart-svg <-> ghci bridge

* ChangeLog
:PROPERTIES:
:EXPORT_FILE_NAME: chart-svg-changelog
:END:

** 0.4

0.4 is a major breaking change to the API, whilst being mostly concerned with plumbing.

The most important change has been the introduction of the Markup type, which represents an abstract markup DSL that could be described as simplified but non-compliant XML.

This will enable diffing of the resultant Markup tree so that ChartOption diffs can be sent over a web socket rather than entire charts.

As a result of his change:
- Chart.Markup replaces Chart.Svg functionality
- ChartOptions replaces ChartSvg
- writeChartOptions replaces writeChartSvg

There now exists an extra step in the chart rendering pipeline, which now goes:
- from ChartOptions to Markup using markupChartOptions
- from Markup to ByteString via encodeMarkup, or
- from Markup to Text via renderMarkup

Changes to dependencies include:
- lucid removed.
- tree-diff introduced in the test routines.
- flatparse replaces attoparsec
- string-interpolate replaces neat-interpolation

** 0.3

[[https://hackage.haskell.org/package/chart-svg][chart-svg-0.3]] is a major rewrite of a library I've had in the toolkit for a while. This has been a major refactoring and I'd like to share a few highlights.

*** Monomorphic primitives

Chart primitives boil down to a very short list. Charts consist of:

- Rectangles
- Lines
- Glyphs (Geometric Shapes such as circles and arrows)
- Text (specifically positioned on a page) &
- Paths (curves)

The core ~Chart~ type now reflects this and looks like:

#+begin_src haskell
data Chart where
RectChart :: RectStyle -> [Rect Double] -> Chart
LineChart :: LineStyle -> [[Point Double]] -> Chart
GlyphChart :: GlyphStyle -> [Point Double] -> Chart
TextChart :: TextStyle -> [(Text, Point Double)] -> Chart
PathChart :: PathStyle -> [PathData Double] -> Chart
BlankChart :: [Rect Double] -> Chart
deriving (Eq, Show)

newtype ChartTree = ChartTree {tree :: Tree (Maybe Text, [Chart])} deriving (Eq, Show, Generic)
#+end_src

You can find examples of all of these in Chart.Examples.

Compared to 0.2.3 ...

#+begin_src haskell
data Chart a = Chart
{ -- | annotation style for the data
annotation :: Annotation,
-- | list of data elements, either points or rectangles.
xys :: [XY a]
}

data Annotation
= RectA RectStyle
| TextA TextStyle [Text]
| GlyphA GlyphStyle
| LineA LineStyle
| PathA PathStyle [PathInfo Double]
| BlankA

data XY a
= PointXY (Point a)
| RectXY (Rect a)
#+end_src

... the unification of style via Annotation and data via XY has been ditched, and there is now a simple and tight coupling between style, data type and primitive.

I originally tried for user extensibility of what a Chart was but, in the course of refactoring, the complexity cost started to weigh pretty heavily on the code base. In this particular case, working with a concrete, serializable representation, amenable to optics and pattern matching trumped higher-kinded flexibility.

The new Chart sum type may not cover a useful primitive, or there may be ideas that fall between the GADT definition, but allowing for this just wasn't worth it versus accepting future refactoring costs.

~ChartTree~ is in constrast to the prior usage of a ~[Chart]~ as the basic chart type, and fits in well with the notion of chart as svg, and thus xml tree. The rose-tree bundling and naming of chart components enables easy downstream manipulation with tools like reanimate and CSS.

*** Browser-centric

#+attr_html: :width 400
#+caption: A LineChart
[[file:other/line.svg]]

Existing chart ecosystems, such as excel, [[https://d3js.org/][d3js]] or [[https://github.com/plotly/plotly.js][plotly]], were built in earlier times and don't tend to have regard for modern browser conventions. One addition to the library is to try and fit in with user color scheme preferences. ~Chart-svg~ charts can respect [[https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme][prefers-color-scheme]] and once [[https://github.com/haskell-infra/www.haskell.org/issues/8][this Hackage ticket]] gets done, should look superb in a haddock.


The design flexibility you get from thinking of a chart as primitive shapes to be rendered in a browser also helps expand any definition of what a chart is. A recent example of this can be found in the [[https://hackage.haskell.org/package/dotparse][dotparse]] library which includes production of a [[https://hackage.haskell.org/package/numhask-0.10.1.0][chart]] I added to the numhask docs. Given the constraints of Haddock, the chart is not (yet) clickable, but is clickable in the [[https://hackage.haskell.org/package/numhask-0.10.1.0/docs/other/nh.svg][docs]] ...
This is very difficult to do in other chart libraries outside of direct javascript hacking. Imagine a future where visualisations of class hierarchies help us to tooltip, backlink and navigate complex code bases such as lens.

* Bugz
** styleBox' imprecision

- SVG is, in general, an additive model eg a border adds a constant amount no matter the scale or aspect. Text charts, in particular, can have small data boxes but large style additions to the box.
- rescaling of style here is, in juxtaposition, a multiplicative model.

In practice, this can lead to weird corner cases and unrequited distortion.

The example below starts with the unit chart, and a simple axis bar, with a dynamic overhang, so that the axis bar represents the x-axis extremity.

#+begin_src haskell :results output
exHud h = defaultHudOptions & set #chartAspect ChartAspect & set #axes [(1,defaultAxisOptions & over #bar (fmap (set #overhang h)) & set (#ticks % #ttick) Nothing & set (#ticks % #gtick) Nothing & set (#ticks % #ltick) Nothing)]
:t exHud
x1 h = addHud (exHud h) t1
:t x1
#+end_src

#+begin_src haskell
view styleBox' $ set styleBox' (Just one) (x1 0.1)
#+end_src

#+RESULTS:
: Just Rect -0.5 0.5 -0.5 0.5001171875000001

#+begin_src haskell
view styleBox' $ set styleBox' (Just one) (x1 0)
#+end_src

#+RESULTS:
: Just Rect -0.500049504950495 0.5000495049504949 -0.5 0.5001171875000001

** style elements and the axes

Hud elements (and especially axes) do not take into account the increase in the data area due to style elements.

The structure and interaction of addHud and runHudWith makes implementation problematic.

* Development

This readme is also a nice self-documenting R&D environment.

This import list reflects the current state of library development; flatparse and tree-diff experimentation.

#+begin_src haskell :results output
:reload
:set prompt "> "
:set -XOverloadedLabels
:set -XOverloadedStrings
import Chart
import Chart.Examples
import Optics.Core
#+end_src

#+RESULTS:
#+begin_example
Loaded GHCi configuration from /Users/tonyday/haskell/chart-svg/.ghci
[ 1 of 12] Compiling Chart.Data ( src/Chart/Data.hs, interpreted )
[ 2 of 12] Compiling Data.Colour ( src/Data/Colour.hs, interpreted )
[ 3 of 12] Compiling Data.Path ( src/Data/Path.hs, interpreted )
[ 4 of 12] Compiling Data.Path.Parser ( src/Data/Path/Parser.hs, interpreted )
[ 5 of 12] Compiling Chart.Style ( src/Chart/Style.hs, interpreted )
[ 6 of 12] Compiling Chart.Primitive ( src/Chart/Primitive.hs, interpreted )
[ 7 of 12] Compiling Chart.Hud ( src/Chart/Hud.hs, interpreted )
[ 8 of 12] Compiling Chart.Surface ( src/Chart/Surface.hs, interpreted )
[ 9 of 12] Compiling Chart.Markup ( src/Chart/Markup.hs, interpreted )
[10 of 12] Compiling Chart.Bar ( src/Chart/Bar.hs, interpreted )
[11 of 12] Compiling Chart ( src/Chart.hs, interpreted )
[12 of 12] Compiling Chart.Examples ( src/Chart/Examples.hs, interpreted )
Ok, 12 modules loaded.
>>Ok, 12 modules loaded.
>>
#+end_example

* Test
** ChartOptions ==> Markup ==> ByteString rendering pipeline

#+begin_src haskell :exports both
let c0 = ChartOptions (defaultMarkupOptions & #cssOptions % #preferColorScheme .~ PreferNormal) mempty mempty
c0
#+end_src

#+RESULTS:
: ChartOptions {markupOptions = MarkupOptions {markupHeight = 300.0, cssOptions = CssOptions {shapeRendering = NoShapeRendering, preferColorScheme = PreferNormal, cssExtra = ""}}, hudOptions = HudOptions {chartAspect = FixedAspect 1.5, axes = [], frames = [], legends = [], titles = []}, charts = ChartTree {tree = Node {rootLabel = (Nothing,[]), subForest = []}}}

ChartOptions to Markup

#+begin_src haskell :exports both
markupChartOptions c0
#+end_src

#+RESULTS:
: Markup {standard = Xml, markupTree = [Node {rootLabel = StartTag "svg" [Attr "xmlns" "http://www.w3.org/2000/svg",Attr "xmlns:xlink" "http://www.w3.org/1999/xlink",Attr "width" "450",Attr "height" "300",Attr "viewBox" "-0.75 -0.5 1.5 1.0"], subForest = [Node {rootLabel = StartTag "style" [], subForest = [Node {rootLabel = Content "", subForest = []}]},Node {rootLabel = StartTag "g" [Attr "class" "chart"], subForest = []},Node {rootLabel = StartTag "g" [Attr "class" "hud"], subForest = []}]}]}

Markup to ByteString

#+begin_src haskell :exports both
import MarkupParse
import Data.ByteString qualified as B
B.putStr $ markdown (Indented 4) $ markupChartOptions c0
#+end_src

#+RESULTS:
: <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="450" height="300" viewBox="-0.75 -0.5 1.5 1.0">
: <style>
: </style>
: <g class="chart">
: </g>
: <g class="hud">
: </g>
: </svg>

*** round trip iso for encodeMarkup . parseMarkup

#+begin_src haskell :exports both
fileList fp = fmap (filter (/= ".DS_Store")) (listDirectory fp)
fps <- fileList "other"
fps
#+end_src

#+RESULTS:
| rect.svg | sbar.svg | debug.svg | unit.svg | path.svg | arrow.svg | arcflags.svg | wheel.svg | hudoptions.svg | ellipse.svg | surface.svg | cubic.svg | gradient.svg | text.svg | bar.svg | line.svg | glyphs.svg | venn.svg | quad.svg | ellipse2.svg | usage.svg | wave.svg | date.svg |

#+begin_src haskell
:{
isoMarkupParse :: BS.ByteString -> Bool
isoMarkupParse x = case runParser markupP x of
OK l "" -> encodeMarkup l == x
_ -> False

isoFile :: FilePath -> IO Bool
isoFile fp = do
bs <- BS.readFile fp
pure $ isoMarkupParse bs
:}

#+end_src


#+begin_src haskell :exports both
fok <- mapM isoFile (("other/"<>) <$> fps)
zip fps fok
#+end_src

#+RESULTS:
| rect.svg | True |
| sbar.svg | True |
| debug.svg | True |
| unit.svg | True |
| path.svg | True |
| arrow.svg | True |
| arcflags.svg | True |
| wheel.svg | True |
| hudoptions.svg | True |
| ellipse.svg | True |
| surface.svg | True |
| cubic.svg | True |
| gradient.svg | True |
| text.svg | True |
| bar.svg | True |
| line.svg | True |
| glyphs.svg | True |
| venn.svg | True |
| quad.svg | True |
| ellipse2.svg | True |
| usage.svg | True |
| wave.svg | True |
| date.svg | True |

** Markup testing

#+begin_src haskell :results output
bs <- B.readFile "other/unit.svg"
#+end_src

#+RESULTS:
: <interactive>:48:1: warning: [GHC-63397] [-Wname-shadowing]
: This binding for ‘bs’ shadows the existing binding
: defined at <interactive>:44:1

#+begin_src haskell :results output
import MarkupParse.Patch
patch (normalize $ markup_ Xml bs) (normalize $ markupChartOptions unitExample)
#+end_src

#+RESULTS:
: Nothing

* chart-svg Hud Refactor

Surface Chart example

#+begin_src haskell :results output
:reload
:set prompt "> "
:set -XOverloadedLabels
:set -XOverloadedStrings
import Chart
import Chart.Examples
import Optics.Core
#+end_src

#+RESULTS:
#+begin_example
Loaded GHCi configuration from /Users/tonyday/haskell/chart-svg/.ghci
[ 1 of 14] Compiling Chart.Data ( src/Chart/Data.hs, interpreted )
[ 2 of 14] Compiling Chart.FlatParse ( src/Chart/FlatParse.hs, interpreted )
[ 3 of 14] Compiling Data.Colour ( src/Data/Colour.hs, interpreted )
[ 4 of 14] Compiling Data.Path ( src/Data/Path.hs, interpreted )
[ 5 of 14] Compiling Data.Path.Parser ( src/Data/Path/Parser.hs, interpreted )
[ 6 of 14] Compiling Chart.Style ( src/Chart/Style.hs, interpreted )
[ 7 of 14] Compiling Chart.Primitive ( src/Chart/Primitive.hs, interpreted )
[ 8 of 14] Compiling Chart.Hud ( src/Chart/Hud.hs, interpreted )
[ 9 of 14] Compiling Chart.Surface ( src/Chart/Surface.hs, interpreted )
[10 of 14] Compiling Chart.Markup ( src/Chart/Markup.hs, interpreted )
[11 of 14] Compiling Chart.Markup.Parser ( src/Chart/Markup/Parser.hs, interpreted )
[12 of 14] Compiling Chart.Bar ( src/Chart/Bar.hs, interpreted )
[13 of 14] Compiling Chart ( src/Chart.hs, interpreted )
[14 of 14] Compiling Chart.Examples ( src/Chart/Examples.hs, interpreted )
Ok, 14 modules loaded.
>>Ok, 14 modules loaded.
>>
#+end_example

#+begin_src haskell :results output
:t surfaceExample
#+end_src

#+RESULTS:
: surfaceExample :: ChartOptions

Changes

0.5.0

  • Library split into markup-parse and chart-svg.

0.4.1

  • Changes due to numhask-0.11 upgrade
  • remove broken surface legend

0.4

  • Markup type introduced, representing an abstract markup DSL that could be described as simplified but non-compliant XML
    • Chart.Svg replaced by Chart.Markup & Chart.Markup.Parser
    • ChartSvg replaced by ChartOptions
    • functionality includes both printing and parsing.
    • the rendering pipeline is now ChartOptions => Markup => ByteString
  • lucid removed as a dependency.
  • tree-diff introduced in the test routines.
  • flatparse replaces attoparsec
  • string-interpolate replaces neat-interpolation

0.3

  • Chart type rewritten
    • Chart data is no longer a separate element
    • charts are monomorphic (underlying data is Double)
  • Aligned with prefer-color-scheme usage
  • oklab usage as per emerging CSS standards
  • chart-reanimate is a separate library
  • formatn is a seprate library
  • introduced a ChartTree type as a tree of named charts to facilitate downstream usage of classes.

0.2.2

  • Changed api for palette

0.2.1

  • Changed api for reanimate hooks.
  • Rationalised default options.

0.2.0

  • Reanimate support. See app/reanimate-example.hs
  • Data.Path added: support for Path style charts.
  • Chart.Examples expanded
  • Improvements to documentation.
  • web-rep support removed.

0.1.2

  • basic charts