D3 for UI: Refined Technique

47 views
Skip to first unread message

Pedram Emrouznejad

unread,
Apr 16, 2015, 2:15:07 AM4/16/15
to d3...@googlegroups.com
Hey All,

Just wanted to share a refined technique if you use D3 a lot for building applications/components. This small "once" helper function essentially amounts to a functional/modular DOM diffing solution:


In this article on idempotent components, there's a note on how you can use joins for both data-driven and structural elements. But it can be quite verbose doing the entire join dance (and often you are likely to forget to do it properly, like .exit().remove() on every join), so this function makes it cheaper to consistently use this pattern. It vastly simplifies the common use case, and returns the join so you can continue to extend it for further customisations (.classed, .on, .attr, .style, etc). There's an example here (and throughout the repo) and a longer braindump explanation here.

I'll move this utility function out into a separate repo/package soon. 

Would be interested to know what others think of this approach or if there are any different tips/tricks people are using!

Regards,
Pedram

Pedram Emrouznejad

unread,
Jul 8, 2015, 12:52:12 PM7/8/15
to d3...@googlegroups.com
I've refined this further to make D3 code more readable, into a terse lispy style syntax with more defaults and moved into it's own repo now. Would love to know if you have any comments/feedback/+1/-1's!

Function for building entirely data-driven idempotent components/UI with D3.

once(node)                        // limit to this node
  ('div', { results: [1, 2, 3] }) // creates one div (with the specified datum)
    ('li', key('results'))        // creates three li (with datum 1, 2, 3 respectively)
      ('a', inherit)              // creates anchor in each li (with parent datum)
        .text(String)             // sets the text in anchor to the datum

The first time you call once(node | string) it essentially selects that element and limits the scope of subsequent operations to that.

Subsequents calls generate a D3 join using the syntax (selector, data). The selector can be:

  • A selector string (foo.bar.baz). Classes are fine too and will be added to the final elements created.
  • A real element, which will be replicated.
  • A function, which will be given parent data, in case you wish to output different (custom) elements based on data.

The data is the same as what you would normally use to generate a join (array of items, or function), with some convenient defaults: if you pass an object, number or boolean it'll be converted to a single-valued array, meaning "create one element with this as the datum". If you pass in a falsy, it defaults to empty array "meaning removing all elements of this type".

The return value is essentially a D3 join selection (enter/update/exit), so you can continue to customise using .text.classed.attr, etc. You can also access the elements added via .enter and removed via .exit.

There are two further optional arguments you can use (selector, data[, key[, before]]). The key function has the exact same meaning as normal (how to key data), which D3 defaults to by index. The before parameter can be used to force the insertion before a specific element à la .insert(something, before) as opposed to just .append(something).

Reply all
Reply to author
Forward
0 new messages