Halogen ECharts Simple

On Sat, 10 Dec 2022, by @lucasdicioccio, 529 words, 8 code snippets, 6 links, 0images.

This page is a demonstration of a ECharts-for-Halogen library I published. The library spawned out of this very-blog (or rather, from Kitchen-Sink) to display the page graph on the home page.

Installation

Assuming you use npm and spago.

npm i echarts
spago install halogen-echarts-simple

Usage

The simple in the title means that we provide a very simple layer. That is, you have to fill in the blanks.

What this library offers is a function to return Halogen components provided that you teach it:

  • (a) what is the type of the options object passed to EChart
  • (b) what sort of click-callback objects you care to capture

The type Options o = {|o} definition embodies the (a) above. The type Output i = {|i} definition embodies the (b) above.

You thus need some boilerplate work to type and translate the various ECharts datatypes (with its flurry of possible branches). That is, rather than trying to build a all-you-can-eat-buffet type that would match every ‘options’, we prefer to restrict ourselves to simple situations where a given graph has exactly one type. Besides the boilerplate, there probably are limitations (I have not tried the most advanced charts options involving JavaScript functions yet). For situations where the ECharts options merely are uniform data, the boilerplate work should be pretty straightforward.

example: simple line chart

Here we start translating the line-simple example.

Purescript definitions

We need two things to make the example work: a Slot for your Halogen component, and a type to teach PureScript what is the shape of the options object for ECharts.

type Slots =
  ( echarts :: forall query output. H.Slot query output Unit 
  )
_echarts = Proxy :: Proxy "echarts"

type SimpleExampleOptions =
  { xAxis :: { type :: String, data :: Array String }
  , yAxis :: { type :: String }
  , series :: Array { data :: Array Int, type :: String }
  }

single line

Then we can call ECharts.component to turn options into an Halogen Component, which you include in a Slot thanks to the HH.slot_ function.

Thus, the rendering function for your enclosing component needs to call ECharts.components.

  render0 =
      let
        obj :: SimpleExampleOptions
        obj =
          { xAxis: {type: "category", data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
          , yAxis: {type: "value"}
          , series: [{type: "line", data: [150, 230, 224, 218, 135, 147, 260]}]
          }
      in
      HH.div_
      [ HH.text "echarts simple-line example"
      , HH.slot_ _echarts unit ECharts.component {options: obj, modified:false}
      ]

two lines

    render1 =
      let
        obj :: SimpleExampleOptions
        obj =
          { xAxis: {type: "category", data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
          , yAxis: {type: "value"}
          , series:
            [{type: "line", data: [150, 230, 224, 218, 135, 147, 260]}
            ,{type: "line", data: [250, 130, 254, 318, 137, 247, 160]}
            ]
          }
      in
      HH.div_
      [ HH.text "echarts simple-line example with two data-lines"
      , HH.slot_ _echarts unit ECharts.component {options: obj, modified:false}
      ]

interactive

We need to define some action for our enclosing component. The component has an event for a numeric HTML input form.

data DemoAction
  = SetChart2Offset String

Now we can update the ECharts options at each re-render.

    render2 offset =
      let
        ys = [150, 230, 224, 218, 135, 147, 260]
        obj :: SimpleExampleOptions
        obj =
          { xAxis: {type: "category", data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
          , yAxis: {type: "value"}
          , series:
            [{type: "line", data: ys }
            ,{type: "line", data: map (\v -> v + offset) ys}
            ]
          }
      in
      HH.div_
      [ HH.p_ [ HH.text "echarts simple-line example with an offset" ]
      , HH.p_ [ HH.text "we update the offset with the value in the input below:" ]
      , HH.input
        [ HP.type_ HP.InputNumber
        , HP.value $ show offset
        , HE.onValueInput SetChart2Offset
        ]
      , HH.slot_ _echarts unit ECharts.component {options: obj, modified:true}
      ]

    handleAction = case _ of
     SetChart2Offset str -> do
       case Int.fromString str of
         Nothing -> pure unit
         Just n -> H.modify_ _ { chart2Offset = n }

catching clicks

Say we want to add handlers to catch clicks on the chart. ECharts has some provisions for such events and with a bit of boilerplate we can recover the event data.

Note that, much like for Options types we pass to ECharts, we need to type the sort event data we expect to receive from the JavaScript callback, and prepare an action for the enclosing component.

type SimpleExampleEvent =
  ( name :: String
  , seriesIndex :: Int
  , dataIndex :: Int
  )

data DemoAction
  = SetChart2Offset String
  | Chart3Event (Record SimpleExampleEvent)

Now our component rendering and handling actions can be as follows:

    render3 item =
      let
        obj :: SimpleExampleOptions
        obj =
          { xAxis: {type: "category", data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
          , yAxis: {type: "value"}
          , series:
            [{type: "line", data: [150, 230, 224, 218, 135, 147, 260]}
            ,{type: "line", data: [250, 130, 254, 318, 137, 247, 160]}
            ]
          }
        renderItem = case item of
          Nothing -> HH.p_ [ HH.text "nothing clicked" ]
          Just r  -> HH.div_
                     [ HH.h6_ [ HH.text "some click!" ]
                     , HH.p_  [ HH.text r.name ]
                     , HH.p_ 
                       [ HH.text 
                         $ fold
                           [ "seriesIndex/dataIndex: "
                           , show r.seriesIndex
                           , " / "
                           , show r.dataIndex
                           ]
                       ]
                     ]
      in
      HH.div_
      [ HH.p_ [ HH.text "echarts simple-line example with clickable callback" ]
      , renderItem
      , HH.slot _echarts unit ECharts.component {options: obj, modified:false} Chart3Event
      ]

    handleAction = case _ of
     Chart3Event ev -> do
      H.modify_ _ { chart3ClickEvent = Just ev }

the modified :: Bool Input parameter

From the above example you may have noticed that the Input object requires extra information. In particular, the modified boolean allows you to tune whether you want to re-render the graph each time the component is re-rendered or only using the explicit Query-ing mechanism that Halogen offers. Typically you will hardcode the modified value depending on how often component re-renders are in your application.

full source-code

You’ll find the full source-code of the generated JavaScript in a separate file.

The source-code uses HumDrum to be able to insert a same JS file one time per different example.

want more examples? contact me