How to update model without redrawing?

I have a small page that has a model that generates an SVG chart. The page has some sliders for users to change parameters that affect the chart. My issue is that I would like to update the model as the user is dragging the sliders (to display the numeric value of the updated values), but if I update the model currently that redraws the whole chart which is expensive and lags the UI.

My idea is to make the model somewhat recursive; the model contains a cached copy of itself for the chart. The main UI depends on the values in the “main” model, so that onInput can continue update numbers shown, but the chart will only update on a onMouseUp, which generates a message that modifies the model such that the internal model is copied. Here’s a general mock-up of what I’m talking about.

type alias Model = 
  { param1 : Int
  , param2 : Int
  , cache : ModelCache

-- Essentially a hard-coded maybe
type ModelCache
  = Cached Model
  | Empty

init = 
  { param1 = 0
  , param2 = 0
  , cache = Empty

type Msg
  = UpdateParam1 Int
  | UpdateParam2 Int
  | UpdateCache

update : Msg -> Model -> Model
update msg model = 
  case msg of
    UpdateParam1 p1 ->
      { model | param1 = p1 }
    UpdateParam2 p2 ->
      { model | param2 = p2 }
    UpdateCache ->
        -- Avoid references to old models forever
        -- I don't know how strictly necessary this is
        cachedModel = { model | cache = Empty } 
        { model | cache = cachedModel }

view : Model -> Html Msg
view model = 
  div []
    [ input 
      [ value <| fromInt model.param1
      , onInput UpdateParam1  -- simplified, ignores String vs. Int
      , onMouseUp UpdateCache 
    , input 
      [ value <| fromInt model.param2
      , onInput UpdateParam2 -- simplified, ignores String vs. Int
      , onMouseUp UpdateCache
    , Html.Lazy.lazy makeChart model.cache

This feels sloppy to me. In particular, I need to handle two input events for every control, and if there are controls which should update the chart immediately, updating them is a two-step process. Are there any better ideas?

Edits: included Html.Lazy.lazy for the chart, onInput and onMouseUp functions.

Such issues are usually solved with Html.lazy. The docs would describe better then I am. Just throw one to your charts view and see the magic :slight_smile:

1 Like

@akoppela Thanks for the reply :slight_smile:. Within the context of my solution with a cache, I think Html.lazy is a great improvement. However as I understand it, it wouldn’t obviate the need for a cache because the input would always be changing. It just makes the cached version faster, right?

The view that you’re going to make lazy should only depend on values (part of the model) that will NOT be changed by your frequent input updates. You should understand how lazy works and how to use it properly, it is a simple thing actually, and should not be considered as “magic”.

And using lazy is actually the only way to prevent excessive rendering, and you don’t need things like “cached” something, you just need to structure your model properly.

@romper What you’re suggesting seems impossible for my scenario without a cache.

Every field in my model is used to render the chart, it’s a simple page which only does that one thing…renders the chart, with options from a user.

If I have a slider for param1 in my model, then the frequent updates of param1 will cause frequent updates in my chart unless I somehow duplicate those variables.

Rather than complicating my model by having two of every field, I have one of every field and one field representing an old copy of my model. The chart depends on the copy which is therefore updated less frequently.

My question essentially boils down to whether there is a more elegant way to solve this rather than using a cache. I’ll update my current code to show my use of lazy.

This is just the way you structure your model, any way you model should fully reflect the actual rendered state, if you need to have a “more stable” part of the model and call it “cached”, let it be.

In your case you could probably debounce changes of parameters. E.g. on each change of parameter run a timer function which will update the parameter and chart. If changes come too frequently cancel timer and run a new one. In this case only when slider changes settles down a parameter would be updated and thus the chart will be updated. I can create Ellie if you want.

1 Like

That sounds like a more elegant solution, but I am a little lost on the details. Does this solution allow you to still update a numeric label next to the slider? An Ellie would be much appreciated! :heart:

1 Like

I don’t understand the cached things you’re doing, can’t see that doing anything.

Elm should only redraw the html that has actually changed. It won’t change things on your page that have not changed, unless your model makes them change. I.e. the simple demo example where a button is incrementing a string does not change anything else. I’ve created a simple example for you. Simply change the <h1> via your browser inspector. Then increment buttons. The <h1> is not being updated.

I hope that helps you to get some feel of what Elm does, and what’s the issue in your pogramme.

Note that lazy is not helping you to avoid redraws, it helps you to avoid expensive computations, i.e. the step before Elm is figuring out what to redraw.

1 Like

I have a chart. That chart has numeric parameters. Those parameters are controlled in my UI via some very basic <input type="range"> sliders, and there is also a <span> showing the current value of the slider.

The chart uses the numeric parameters to generate a few thousand points and plot a line with them. This is an expensive operation, so I’d like to not do it very often.

As a user drags the slider, the updated values are shown in the <span>, so I can’t just not update my model onInput. However updating that value in my model would change the values needed to draw the chart. That’s what I’d like to avoid.

My solution of caching an old model is so that my sliders update the main model, the <span> can update as needed, but the chart won’t update until I update the cache. I update the cache in a separate Msg value, which gets generated onMouseUp.

@adamyakes maybe this helps you: Debounced Validation - Walking though the Elm woods

For your use-case, I think a cache is really the only way to go; lazy alone won’t help you. However, I think that you get a cleaner solution in this case by putting all the parameters needed for the chart into a separate data type

type alias Data =
    { param1 : Int
    , param2 : Float
    , ...

Then, in your model, you have two copies of this:

type alias Model =
    { currentData : Data
    , chartData : Data
    , ... other fields

where currentData contains the current values and is updated immediately and chartData contains the values used for the currently visible chart. On mouse up (or periodically based on a timer), you copy the current values over the chart values, triggering a re-rendering of the chart.

This is similar to what you proposed but:

  • The Data is not recursive, so you don’t need to set the cache value to Empty when copying the data over.
  • You can put values that don’t affect the chart and thus don’t need to be cached into the model instead.
  • You will have nested record updates; though you can use this opportunity to factor out everything in update that touches your Data into a new function.
1 Like

Thank you! I think this most cleanly answers my question’s main goal of doing something that feels less sloppy, so that’s that!

I was hung up on the fact that this isn’t really a full website, it’s literally just the chart and some controls (couple sliders, couple checkboxes, couple open fields) but they all have to do with creating the chart. There’s nothing more to it. Avoiding the recursion is a nice touch.

That’s correct. Html.lazy won’t help here because chart depends of params you’re changing. So caching should work. Here is simple ellie with two values, one being real param value and one is debounced param value.


@akoppela Very nice solution! And a quite different and simpler approach than Debounced Validation - Walking though the Elm woods Your solution works without the need of subscriptions. Do you know of any downside compared to the subscriptions solution. Maybe some edge cases? Race conditions? Performance?

EDIT: I think I found a disadvantage. Your solution triggers an additional message for every value change. The subscriptions solution only checks the “ValueDebounced” message in certian intervals (in the subscription) hence there are less updates to the model which could lead to a better performance.

The subscription function is used to emulate throttling. Which can be used instead of debouncing. With debouncing you get updates when changes settle, with throttling you just reduce numbers of updates. So it depends, if redrawing chart is really expensive then throttling might be more expensive then debouncing.

I don’t think it’s throttling. 14: { model | debouncing = Just ( pass, 500 ) } does reset the timer when NameEntered is triggered.

EDIT: I think I found a disadvantage. Your solution triggers an additional message for every value change. The subscriptions solution only checks the “ValueDebounced” message in certian intervals (in the subscription) hence there are less updates to the model which could lead to a better performance.

But the subs version does trigger additional messages too: Time.every 100 (always msg) But at most every 100 ms, regardless of the input frequency.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.