← Hylograph

Self-contained examples. Each one builds from a fresh clone. Demo on the left, source on the right.

Axes And Ticks

Axes and Ticks — Scatter plot with X and Y axes using pure scales

View in repo →
src/Main.purs
-- | Axes and Ticks — Scatter plot with X and Y axes using pure scales
module Main where

import Prelude
import Effect (Effect)
import Hylograph.Scale.Pure as Scale
import Hylograph.HATS (Tree, elem, forEach, staticStr)
import Hylograph.HATS.Friendly as F
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Internal.Element.Types (ElementType(..))

type Point = { x :: Number, y :: Number, label :: String }

points :: Array Point
points =
  [ { x: 10.0, y: 28.0, label: "A" }, { x: 25.0, y: 62.0, label: "B" }
  , { x: 38.0, y: 45.0, label: "C" }, { x: 52.0, y: 80.0, label: "D" }
  , { x: 60.0, y: 35.0, label: "E" }, { x: 72.0, y: 90.0, label: "F" }
  , { x: 85.0, y: 55.0, label: "G" }, { x: 95.0, y: 72.0, label: "H" }
  , { x: 42.0, y: 18.0, label: "I" }, { x: 68.0, y: 68.0, label: "J" }
  ]

chart :: Tree
chart =
  let
    ml = 50.0
    mt = 10.0
    pw = 440.0
    ph = 300.0
    w = ml + pw + 10.0
    h = mt + ph + 40.0
    xS = Scale.linear # Scale.domain [0.0, 100.0] # Scale.range [ml, ml + pw]
    yS = Scale.linear # Scale.domain [0.0, 100.0] # Scale.range [mt + ph, mt]
    xT = Scale.ticks 5 xS
    yT = Scale.ticks 5 yS
    bY = mt + ph
  in
  elem SVG
    [ F.viewBox 0.0 0.0 w h, F.width w
    , F.style "max-width: 520px; font-family: system-ui, sans-serif;"
    ]
    [ forEach "pts" Circle points _.label \pt ->
        elem Circle
          [ F.cx (Scale.applyScale xS pt.x), F.cy (Scale.applyScale yS pt.y)
          , F.r 4.0, F.fill "#2563eb", F.fillOpacity "0.7"
          ] []

    , elem Line
        [ F.x1 ml, F.y1 bY, F.x2 (ml + pw), F.y2 bY
        , F.stroke "#333", F.strokeWidth 1.0
        ] []
    , forEach "xTicks" Group xT show \tv ->
        let px = Scale.applyScale xS tv in
        elem Group []
          [ elem Line
              [ F.x1 px, F.y1 bY, F.x2 px, F.y2 (bY + 6.0)
              , F.stroke "#333", F.strokeWidth 1.0
              ] []
          , elem Text
              [ F.x px, F.y (bY + 20.0), F.textAnchor "middle"
              , F.fontSize "11", F.fill "#555"
              , staticStr "textContent" (show tv)
              ] []
          ]

    , elem Line
        [ F.x1 ml, F.y1 mt, F.x2 ml, F.y2 (mt + ph)
        , F.stroke "#333", F.strokeWidth 1.0
        ] []
    , forEach "yTicks" Group yT show \tv ->
        let py = Scale.applyScale yS tv in
        elem Group []
          [ elem Line
              [ F.x1 (ml - 6.0), F.y1 py, F.x2 ml, F.y2 py
              , F.stroke "#333", F.strokeWidth 1.0
              ] []
          , elem Text
              [ F.x (ml - 10.0), F.y (py + 4.0), F.textAnchor "end"
              , F.fontSize "11", F.fill "#555"
              , staticStr "textContent" (show tv)
              ] []
          ]
    ]

main :: Effect Unit
main = do
  _ <- rerender "#app" chart
  pure unit

Color Scales

Color Scales — Linear scales, color interpolators, and tick marks

View in repo →
src/Main.purs
-- | Color Scales — Linear scales, color interpolators, and tick marks
-- | Demonstrates Hylograph's pure scale system without D3.
module Main where

import Prelude
import Data.Array (range)
import Data.Int (toNumber)
import Effect (Effect)
import Hylograph.Scale.Pure as Scale
import Hylograph.Scale.Sequential (interpolateViridis, interpolateRdYlGn)
import Hylograph.HATS (Tree, elem, forEach, staticStr)
import Hylograph.HATS.Friendly as F
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Internal.Element.Types (ElementType(..))

-- | Build an SVG with two color gradient bars and tick marks
colorScales :: Number -> Tree
colorScales w =
  let
    n      = 100 -- color samples per bar
    indices = range 0 (n - 1) <#> \i ->
                { idx: i, t: toNumber i / toNumber (n - 1) }
    xScale  = Scale.linear # Scale.domain [0.0, toNumber (n - 1)] # Scale.range [0.0, w]
    nScale  = Scale.linear # Scale.domain [0.0, 1.0] # Scale.range [0.0, w]
    ticks   = Scale.ticks 10 nScale
    barH = 32.0
    stripW = w / toNumber n + 1.0
    y1 = 10.0                  -- first bar top
    yT = y1 + barH + 8.0      -- tick area
    y2 = yT + 40.0             -- second bar top

    -- Helper: one horizontal gradient bar via thin rects
    gradientBar name yPos interp =
      forEach name Rect indices (show <<< _.idx) \s ->
        elem Rect
          [ F.x (Scale.applyScale xScale (toNumber s.idx)), F.y yPos
          , F.width stripW, F.height barH
          , F.fill (interp s.t)
          ] []
  in
  elem SVG
    [ F.viewBox 0.0 0.0 (w + 20.0) (y2 + barH + 10.0)
    , F.width (w + 20.0)
    , F.style "max-width: 640px; font-family: system-ui, sans-serif;"
    ]
    [ gradientBar "viridis" y1 interpolateViridis

    , forEach "ticks" Group ticks show \tv ->
        let px = Scale.applyScale nScale tv in
        elem Group []
          [ elem Line
              [ F.x1 px, F.y1 yT, F.x2 px, F.y2 (yT + 8.0)
              , F.stroke "#333", F.strokeWidth 1.0
              ] []
          , elem Text
              [ F.x px, F.y (yT + 20.0), F.textAnchor "middle"
              , F.fontSize "11", F.fill "#555"
              , staticStr "textContent" (show tv)
              ] []
          ]

    , gradientBar "rdylgn" y2 interpolateRdYlGn
    ]

main :: Effect Unit
main = do

  _ <- rerender "#app" (colorScales 500.0)
  pure unit

Coordinated Highlighting

Coordinated Highlighting — hover a category in the key and all

View in repo →
src/Main.purs
-- | Coordinated Highlighting — hover a category in the key and all
-- | matching circles in the scatter plot highlight.
module Main where

import Prelude

import Data.Array (mapWithIndex)
import Data.Int as Int
import Data.Maybe (Maybe(..))
import Effect (Effect)

import Hylograph.HATS (Tree, elem, forEach, staticStr, withBehaviors, onCoordinatedHighlight)
import Hylograph.HATS.Friendly as F
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Internal.Behavior.Types (HighlightClass(..))
import Hylograph.Internal.Element.Types (ElementType(..))

-- Categories for the key
type Category = { name :: String, color :: String, idx :: Int }

categories :: Array Category
categories =
  [ { name: "Igneous",     color: "#e15759", idx: 0 }
  , { name: "Sedimentary", color: "#4e79a7", idx: 1 }
  , { name: "Metamorphic", color: "#59a14f", idx: 2 }
  , { name: "Volcanic",    color: "#f28e2b", idx: 3 }
  ]

-- Scattered circles, each belonging to a category
type Dot = { x :: Number, y :: Number, r :: Number, category :: String, color :: String, id :: String }

dots :: Array Dot
dots =
  [ { x: 45.0,  y: 35.0,  r: 10.0, category: "Igneous",     color: "#e15759", id: "d1" }
  , { x: 160.0, y: 55.0,  r: 14.0, category: "Sedimentary", color: "#4e79a7", id: "d2" }
  , { x: 90.0,  y: 120.0, r: 8.0,  category: "Metamorphic", color: "#59a14f", id: "d3" }
  , { x: 210.0, y: 40.0,  r: 11.0, category: "Igneous",     color: "#e15759", id: "d4" }
  , { x: 130.0, y: 160.0, r: 13.0, category: "Volcanic",    color: "#f28e2b", id: "d5" }
  , { x: 55.0,  y: 190.0, r: 9.0,  category: "Sedimentary", color: "#4e79a7", id: "d6" }
  , { x: 240.0, y: 130.0, r: 12.0, category: "Metamorphic", color: "#59a14f", id: "d7" }
  , { x: 180.0, y: 200.0, r: 10.0, category: "Igneous",     color: "#e15759", id: "d8" }
  , { x: 30.0,  y: 100.0, r: 15.0, category: "Volcanic",    color: "#f28e2b", id: "d9" }
  , { x: 270.0, y: 80.0,  r: 7.0,  category: "Sedimentary", color: "#4e79a7", id: "d10" }
  , { x: 200.0, y: 170.0, r: 11.0, category: "Volcanic",    color: "#f28e2b", id: "d11" }
  , { x: 100.0, y: 60.0,  r: 9.0,  category: "Metamorphic", color: "#59a14f", id: "d12" }
  , { x: 150.0, y: 100.0, r: 16.0, category: "Igneous",     color: "#e15759", id: "d13" }
  , { x: 260.0, y: 190.0, r: 8.0,  category: "Sedimentary", color: "#4e79a7", id: "d14" }
  , { x: 70.0,  y: 155.0, r: 12.0, category: "Volcanic",    color: "#f28e2b", id: "d15" }
  ]

-- | The key: labeled color swatches
keyView :: Tree
keyView =
  let swatchH = 28.0
      gap = 8.0
  in
    elem SVG [ F.viewBox 0.0 0.0 140.0 160.0, F.width 140.0, F.height 160.0 ]
      [ forEach "key" Group categories _.name \cat ->
          let y = 10.0 + Int.toNumber cat.idx * (swatchH + gap)
          in withBehaviors
              [ onCoordinatedHighlight
                  { identify: cat.name
                  , classify: \hovered -> if hovered == cat.name then Primary else Dimmed
                  , group: Nothing
                  }
              ] $
              elem Group [ F.transform ("translate(0," <> show y <> ")") ]
                [ elem Rect
                    [ F.x 0.0, F.y 0.0, F.width 20.0, F.height swatchH
                    , staticStr "rx" "3"
                    , F.fill cat.color
                    ] []
                , elem Text
                    [ F.x 28.0, F.y (swatchH / 2.0 + 5.0)
                    , F.fontSize "13"
                    , F.fontFamily "system-ui, sans-serif"
                    , F.fill "#333"
                    , staticStr "textContent" cat.name
                    ] []
                ]
      ]

-- | The scatter: circles colored by category
scatterView :: Tree
scatterView =
  elem SVG [ F.viewBox 0.0 0.0 300.0 230.0, F.width 300.0, F.height 230.0 ]
    [ forEach "scatter" Group dots _.id \d ->
        withBehaviors
          [ onCoordinatedHighlight
              { identify: d.category  -- identifies as category, not individual dot
              , classify: \hovered -> if hovered == d.category then Primary else Dimmed
              , group: Nothing
              }
          ] $
          elem Group []
            [ elem Circle
                [ F.cx d.x, F.cy d.y, F.r d.r
                , F.fill d.color
                , F.opacity "0.85"
                ] []
            ]
    ]

main :: Effect Unit
main = do
  _ <- rerender "#key" keyView
  _ <- rerender "#scatter" scatterView
  pure unit

Css Hover

CSS Hover — pure CSS :hover on HATS elements, no JavaScript needed

View in repo →
src/Main.purs
-- | CSS Hover — pure CSS :hover on HATS elements, no JavaScript needed
module Main where

import Prelude

import Effect (Effect)
import Hylograph.HATS (Tree, elem, forEach, staticStr)
import Hylograph.HATS.Friendly as F
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Internal.Element.Types (ElementType(..))

type Dot = { color :: String, cx :: Number }

dots :: Array Dot
dots =
  [ { color: "#e15759", cx: 40.0 }
  , { color: "#f28e2b", cx: 104.0 }
  , { color: "#edc948", cx: 168.0 }
  , { color: "#59a14f", cx: 232.0 }
  , { color: "#4e79a7", cx: 296.0 }
  , { color: "#b07aa1", cx: 360.0 }
  ]

circles :: Tree
circles =
  elem SVG
    [ F.viewBox 0.0 0.0 400.0 100.0
    , F.style "max-width: 500px;"
    ]
    [ forEach "dots" Circle dots _.color \d ->
        elem Circle
          [ F.cx d.cx
          , F.cy 50.0
          , F.r 24.0
          , F.fill d.color
          , staticStr "class" "hoverable"
          ] []
    ]

main :: Effect Unit
main = do
  _ <- rerender "#app" circles
  pure unit

General Update Pattern

general-update-pattern

View in repo →
src/Main.purs
module Main where

import Prelude
import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import App as App

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI App.component unit body

Halogen Integration

halogen-integration

View in repo →
src/Main.purs
module Main where

import Prelude
import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import App as App

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI App.component unit body

Simple Webpage

Simple Webpage — minimum viable HATS visualization

View in repo →
src/Main.purs
-- | Simple Webpage — minimum viable HATS visualization
module Main where

import Prelude
import Effect (Effect)
import Hylograph.HATS (Tree, elem, forEach, staticStr)
import Hylograph.HATS.Friendly as F
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Internal.Element.Types (ElementType(..))

type Bar = { label :: String, h :: Number, x :: Number, color :: String }

bars :: Array Bar
bars =
  [ { label: "A", h: 90.0, x:  20.0, color: "#4e79a7" }
  , { label: "B", h: 60.0, x:  75.0, color: "#f28e2b" }
  , { label: "C", h: 75.0, x: 130.0, color: "#e15759" }
  , { label: "D", h: 40.0, x: 185.0, color: "#76b7b2" }
  , { label: "E", h: 55.0, x: 240.0, color: "#59a14f" }
  ]

chart :: Tree
chart =
  elem SVG [ F.viewBox 0.0 0.0 300.0 130.0, F.style "max-width: 400px;" ]
    [ forEach "bars" Group bars _.label \d ->
        elem Group []
          [ elem Rect
              [ F.x d.x, F.y (100.0 - d.h), F.width 40.0, F.height d.h
              , F.fill d.color
              ] []
          , elem Text
              [ F.x (d.x + 20.0), F.y 120.0
              , F.textAnchor "middle", F.fontSize "12px", F.fill "#333"
              , staticStr "textContent" d.label
              ] []
          ]
    ]

main :: Effect Unit
main = do
  _ <- rerender "#app" chart
  pure unit

Tooltips

Tooltips — hover any circle to see a tooltip with its details.

View in repo →
src/Main.purs
-- | Tooltips — hover any circle to see a tooltip with its details.
-- |
-- | NOTE: tooltip content is plain text here because the HATS coordinated
-- | tooltip path in hylograph-selection@0.5.0 uses textContent instead of
-- | innerHTML, so HTML tags render as literal text. Fix tracked upstream.
module Main where

import Prelude

import Data.Maybe (Maybe(..))
import Effect (Effect)

import Hylograph.HATS (Tree, elem, forEach, staticStr, withBehaviors, onCoordinatedHighlightWithTooltip)
import Hylograph.HATS.Friendly as F
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Internal.Behavior.Types (HighlightClass(..), TooltipTrigger(..))
import Hylograph.Internal.Element.Types (ElementType(..))
import Hylograph.Tooltip (configureTooltip, lightTheme)

type Planet =
  { name :: String
  , radius :: Number
  , color :: String
  , x :: Number
  , y :: Number
  , detail :: String
  }

planets :: Array Planet
planets =
  [ { name: "Mercury", radius: 8.0,  color: "#b0b0b0", x: 50.0,  y: 120.0, detail: "Closest to the Sun" }
  , { name: "Venus",   radius: 12.0, color: "#e8c96a", x: 130.0, y: 90.0,  detail: "Hottest surface temperature" }
  , { name: "Earth",   radius: 13.0, color: "#4e9fd5", x: 220.0, y: 100.0, detail: "The only known living world" }
  , { name: "Mars",    radius: 10.0, color: "#d4644a", x: 310.0, y: 130.0, detail: "The Red Planet" }
  , { name: "Jupiter", radius: 28.0, color: "#d4a65a", x: 430.0, y: 100.0, detail: "Largest planet in the system" }
  , { name: "Saturn",  radius: 24.0, color: "#e8d88c", x: 550.0, y: 110.0, detail: "Famous for its rings" }
  ]

view :: Tree
view =
  elem SVG [ F.viewBox 0.0 0.0 640.0 240.0, F.width 640.0, F.height 240.0 ]
    [ forEach "planets" Group planets _.name \d ->
        withBehaviors
          [ onCoordinatedHighlightWithTooltip
              { identify: d.name
              , classify: \hovered ->
                  if hovered == d.name then Primary else Dimmed
              , group: Nothing
              , tooltip: Just
                  { content: d.name <> " — " <> d.detail
                  , showWhen: OnHover
                  , borderColor: Just d.color
                  }
              }
          ] $
          elem Group []
            [ elem Circle
                [ F.cx d.x, F.cy d.y, F.r d.radius
                , F.fill d.color
                , F.opacity "0.9"
                ] []
            , elem Text
                [ F.x d.x, F.y (d.y + d.radius + 16.0)
                , F.fontSize "11"
                , F.fontFamily "system-ui, sans-serif"
                , F.fill "#555"
                , F.textAnchor "middle"
                , staticStr "textContent" d.name
                ] []
            ]
    ]

main :: Effect Unit
main = do
  configureTooltip lightTheme
  _ <- rerender "#chart" view
  pure unit

Zoom And Pan

Zoom and Pan — Minimal HATS demo

View in repo →
src/Main.purs
-- | Zoom and Pan — Minimal HATS demo
-- |
-- | Renders a 10x10 grid of circles, then attaches native zoom/pan behavior.
-- | The zoom applies to a <g class="zoom-group"> wrapping all content.
module Main where

import Prelude

import Data.Array ((..))
import Data.Int (toNumber)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Hylograph.HATS (Tree, elem, staticStr, staticNum, forEach)
import Hylograph.HATS.InterpreterTick (rerender)
import Hylograph.Interaction.Zoom (attachZoomNative)
import Hylograph.Internal.Element.Types (ElementType(..))
import Web.DOM.ParentNode (querySelector, QuerySelector(..))
import Web.HTML (window)
import Web.HTML.Window (document)
import Web.HTML.HTMLDocument (toParentNode)

-- | Grid datum: row and column index
type Dot = { row :: Int, col :: Int }

-- | Generate a 10x10 grid of dots
dots :: Array Dot
dots = do
  row <- 0 .. 9
  col <- 0 .. 9
  pure { row, col }

-- | Build the HATS tree: an SVG containing a zoom-group with circles
circleGrid :: Tree
circleGrid =
  elem SVG
    [ staticStr "viewBox" "0 0 500 500"
    , staticStr "width" "500"
    , staticStr "height" "500"
    , staticStr "style" "background: #fff;"
    ]
    -- Wrap all content in a <g class="zoom-group"> so zoom can target it
    [ elem Group [ staticStr "class" "zoom-group" ]
        [ forEach "dots" Circle dots
            (\d -> show d.row <> "," <> show d.col)
            \d ->
              elem Circle
                [ staticNum "cx" (toNumber d.col * 45.0 + 50.0)
                , staticNum "cy" (toNumber d.row * 45.0 + 50.0)
                , staticNum "r" 12.0
                , staticStr "fill" "#4e79a7"
                , staticStr "opacity" "0.7"
                ] []
        ]
    ]

main :: Effect Unit
main = do
  -- 1. Render the circle grid into #app
  _ <- rerender "#app" circleGrid

  -- 2. Find the SVG element we just created
  doc <- window >>= document
  mSvg <- querySelector (QuerySelector "#app svg") (toParentNode doc)

  -- 3. Attach zoom: scale 0.1x to 10x, targeting the .zoom-group <g>
  case mSvg of
    Just svg -> do
      _ <- attachZoomNative svg 0.1 10.0 ".zoom-group"
      pure unit
    Nothing ->
      pure unit