Package

purescript-chartjs

Repository
philippedev101/purescript-chartjs
License
Apache-2.0
Uploaded by
pacchettibotti
Published on
2026-04-17T16:18:15Z

A PureScript wrapper for Chart.js, providing type-safe, config-driven chart rendering via FFI.

This package is framework-agnostic: it provides types, a JSON config builder, FFI bindings to chart.js, and an overlay mechanism for values (callbacks, gradients, patterns) that can't round-trip through JSON. Consumers can use it directly on a canvas element or wrap it in a component for their framework of choice (Halogen, React, etc.).

Installation

Add the package to your spago.yaml extraPackages (not yet in the registry):

extraPackages:
  chartjs:
    git: https://your-gitea-instance/you/purescript-chartjs.git
    ref: main
dependencies:
  - chartjs

Install the JS dependency:

bun add chart.js

Quick Start

module MyApp where

import Prelude
import Chartjs (createChartAuto, destroyChart)
import Chartjs.Callbacks (defaultCallbacks)
import Chartjs.Types (ChartType(..), TitleText(..), defaultConfig, defaultDataset,
  defaultOptions, defaultPluginsConfig, defaultTitleConfig, fromNumbers, siValue, css)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Web.HTML.HTMLElement (HTMLElement)

myConfig = defaultConfig
  { chartType = Bar
  , labels = ["Jan", "Feb", "Mar", "Apr"]
  , datasets =
      [ defaultDataset
          { label = "Revenue"
          , "data" = fromNumbers [100.0, 200.0, 150.0, 300.0]
          , backgroundColor = siValue (css "#4CAF50")
          }
      ]
  , options = defaultOptions
      { plugins = Just defaultPluginsConfig
          { title = Just defaultTitleConfig
              { display = Just true
              , text = Just (TitleSingle "Monthly Revenue")
              }
          }
      }
  }

render :: HTMLElement -> Effect Unit
render canvas = do
  inst <- createChartAuto canvas myConfig defaultCallbacks
  -- ...later, to clean up:
  destroyChart inst

createChartAuto is the recommended entry point. It inspects the config + callbacks and automatically routes through the overlay merge when needed — so consumers never have to choose between createChart and createChartWithCallbacks. If the config only contains JSON-compatible values (CSS colors, plain config records, no callbacks), the routing is equivalent to a direct createChart call with zero overhead. If any field contains a non-JSON value (a gradient color, a pattern color, a PSImage point style, or any configured Callbacks field), it transparently switches to createChartWithCallbacks and applies the overlay merge. The corresponding updater is updateChartAuto.

The lower-level FFI wrappers (createChart, updateChart, createChartWithCallbacks, updateChartWithCallbacks, buildOverlays) are still exported and available for consumers who want explicit control over the dispatch — for example, to avoid rebuilding the overlay object on every update in a hot loop.

Chart Types

All standard Chart.js chart types are supported:

data ChartType = Line | Bar | Pie | Doughnut | Radar | Scatter | Bubble | PolarArea

API Overview

Core Types

Type Description
ChartConfig Top-level config: chart type, labels, datasets, options
Dataset A single data series (80+ optional fields matching Chart.js)
DatasetDefaults Same shape as Dataset minus label/data — used for per-chart-type defaults
DatasetsDefaults Per-chart-type dataset defaults keyed by chart type (line, bar, etc.)
ChartOptions Nested options: plugins, scales, animation, interaction, layout, elements, chart-level defaults
Callbacks Optional event handlers: onClick, onHover, onResize, tooltip/legend/tick callbacks
ComponentInput Convenience record: { config :: ChartConfig, callbacks :: Callbacks, updateMode :: Maybe String }

Defaults

Every config type has a default* that sets all optional fields to Nothing:

defaultConfig           :: ChartConfig
defaultDataset          :: Dataset
defaultDatasetDefaults  :: DatasetDefaults
defaultDatasetsDefaults :: DatasetsDefaults
defaultOptions          :: ChartOptions
defaultPluginsConfig    :: PluginsConfig
defaultTitleConfig      :: TitleConfig
defaultLegendConfig     :: LegendConfig
defaultScaleConfig      :: ScaleConfig
-- ... and many more

Use PureScript's record update syntax to override what you need:

defaultDataset
  { label = "Sales"
  , "data" = fromNumbers [10.0, 20.0, 30.0]
  , borderWidth = siValue (BarBorderWidthUniform 2.0)
  }

Data Points

Datasets accept Array DataPoint with smart constructors:

fromNumbers     :: Array Number -> Array DataPoint                                 -- simple values
fromXY          :: Array { x :: Number, y :: Number } -> Array DataPoint            -- scatter/line
fromXYString    :: Array (Tuple String Number) -> Array DataPoint                   -- categorical x
fromBubble      :: Array { x :: Number, y :: Number, r :: Number } -> Array DataPoint
fromFloatingBar :: Array (Tuple Number Number) -> Array DataPoint                   -- [lo, hi] bars

Colors

Colors can be CSS strings, canvas gradients, or canvas patterns:

data Color = CSSColor String | GradientColor CanvasGradient | PatternColor CanvasPattern

-- Convenience constructor:
css :: String -> Color
css = CSSColor

Color fields on datasets use ScriptableIndexable Color — a single value, a per-item array, or a scriptable function:

siValue :: forall a. a -> Maybe (ScriptableIndexable a)        -- one value for all points
siArray :: forall a. Array a -> Maybe (ScriptableIndexable a)  -- one value per point
siFn    :: forall a. (ScriptableContext -> a) -> Maybe (ScriptableIndexable a)  -- dynamic

-- Examples:
backgroundColor = siValue (css "#4CAF50")
backgroundColor = siArray [css "#FF6384", css "#36A2EB", css "#FFCE56"]
backgroundColor = siFn (\ctx -> css (if ctx.dataIndex == 0 then "#FF6384" else "#36A2EB"))

Gradients and patterns are created via effectful FFI calls:

createLinearGradient :: HTMLCanvasElement -> { x0, y0, x1, y1 :: Number } -> Array ColorStop -> Effect CanvasGradient
createRadialGradient :: HTMLCanvasElement -> { x0, y0, r0, x1, y1, r1 :: Number } -> Array ColorStop -> Effect CanvasGradient
createPattern        :: HTMLCanvasElement -> image -> String -> Effect CanvasPattern

Scales

Scales are keyed by axis ID in a Foreign.Object:

import Foreign.Object as Object
import Data.Tuple (Tuple(..))

options = defaultOptions
  { scales = Just $ Object.fromFoldable
      [ Tuple "x" $ defaultScaleConfig { stacked = Just StackedTrue }
      , Tuple "y" $ defaultScaleConfig
          { beginAtZero = Just true
          , title = Just $ defaultScaleTitleConfig
              { display = Just true, text = Just (TitleSingle "Amount ($)") }
          }
      ]
  }

Chart-level dataset defaults

ChartOptions includes a datasets field — per-chart-type default values that apply to every dataset of that type (Chart.js's options.datasets.<type>). It shares a single source of truth with Dataset via the DatasetCommonRow row synonym, so every field on Dataset is also available on DatasetDefaults:

options = defaultOptions
  { datasets = Just $ defaultDatasetsDefaults
      { line = Just $ defaultDatasetDefaults
          { tension = Just 0.4
          , showLine = Just true
          }
      , bar = Just $ defaultDatasetDefaults
          { barPercentage = Just 0.8
          , backgroundColor = siValue (css "#4CAF50")
          }
      }
  }

Callbacks

Callbacks (functions, gradients, patterns) can't round-trip through JSON, so they go through an overlay mechanism: PureScript builds a plain JS object via buildOverlays, and a JS helper patches the JSON config before handing it to Chart.js. Consumers use createChartWithCallbacks / updateChartWithCallbacks, which wire this together automatically.

import Chartjs.Callbacks (defaultCallbacks, simpleInput, buildOverlays)
import Chartjs.FFI (createChartWithCallbacks)
import Effect.Uncurried (mkEffectFn3)
import Data.Maybe (Maybe(..))

myCallbacks = defaultCallbacks
  { onClick = Just $ mkEffectFn3 \_event _elements _chart -> pure unit
  }

render canvas = do
  let json = toChartJsConfig myConfig
  let overlays = buildOverlays myCallbacks myConfig
  inst <- createChartWithCallbacks canvas json overlays
  pure unit

simpleInput :: ChartConfig -> ComponentInput is a convenience for building a ComponentInput record with no callbacks — useful if you're wrapping the library in a framework component that takes a ComponentInput as input.

Available callbacks:

Callback Signature
onClick EffectFn3 Event (Array ActiveElement) ChartInstance Unit
onHover EffectFn3 Event (Array ActiveElement) ChartInstance Unit
onResize EffectFn2 ChartInstance { width :: Number, height :: Number } Unit
legendOnClick EffectFn3 Event LegendItem Legend Unit
legendOnHover EffectFn3 Event LegendItem Legend Unit
legendOnLeave EffectFn3 Event LegendItem Legend Unit
tooltipCallbacks.label EffectFn1 TooltipItem String
tooltipCallbacks.title EffectFn1 (Array TooltipItem) String
tooltipCallbacks.footer EffectFn1 (Array TooltipItem) String
tickCallbacks Object (EffectFn3 Foreign Int (Array Foreign) String) — keyed by scale ID
animationOnProgress / animationOnComplete EffectFn1 Foreign Unit

Framework integration

This package does not ship a component for any specific framework. To use it with Halogen, React, or similar, write a thin component that:

  1. Creates a <canvas> element on initialization.
  2. Calls createChart (or createChartWithCallbacks) with toChartJsConfig yourConfig.
  3. On input change, calls updateChart / updateChartWithCallbacks — do not destroy and recreate.
  4. On finalization, calls destroyChart to release Chart.js internal state.

ComponentInput exists as a convenience type for framework components whose input is a config plus callbacks plus an optional update mode ("none" suppresses animation on updates, which is usually what you want when state flows in from outside).

Building

bunx spago build
bunx spago test

FFI integration tests

The PureScript test suite verifies config generation, ADT serialization, and FFI wrapper type signatures. It does not exercise the JavaScript FFI implementations at runtime. A separate integration suite in test/integration/ does: it spins up a JSDOM environment, installs a minimal Canvas 2D mock, and instantiates a real Chart.js chart to exercise each *Impl function end-to-end.

bun run test:ffi

This catches runtime FFI bugs that the PureScript compile-time checks can't see (method typos, wrong argument order, missing return statements, etc.). Run it whenever you touch src/Chartjs/FFI.js or add a new FFI wrapper.

License

Apache-2.0

Modules
Chartjs
Chartjs.Callbacks
Chartjs.Colors
Chartjs.Config
Chartjs.FFI
Chartjs.Scriptable
Chartjs.Types
Dependencies