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.).
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: maindependencies:
- chartjsInstall the JS dependency:
bun add chart.js
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 instcreateChartAuto 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.
All standard Chart.js chart types are supported:
data ChartType = Line | Bar | Pie | Doughnut | Radar | Scatter | Bubble | PolarArea| 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 } |
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 moreUse 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)
}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] barsColors can be CSS strings, canvas gradients, or canvas patterns:
data Color = CSSColor String | GradientColor CanvasGradient | PatternColor CanvasPattern
-- Convenience constructor:
css :: String -> Color
css = CSSColorColor 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 CanvasPatternScales 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 ($)") }
}
]
}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 (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 unitsimpleInput :: 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 |
This package does not ship a component for any specific framework. To use it with Halogen, React, or similar, write a thin component that:
- Creates a
<canvas>element on initialization. - Calls
createChart(orcreateChartWithCallbacks) withtoChartJsConfig yourConfig. - On input change, calls
updateChart/updateChartWithCallbacks— do not destroy and recreate. - On finalization, calls
destroyChartto 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).
bunx spago build
bunx spago test
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.
Apache-2.0