block by vijithassar 3518ea727b10e03d02a6c3fdd97d3b69

d3.history example

Full Screen

demonstration of d3.history, a plugin for D3.js which adds support for deep-linking and URLs based on the user interface state. (To see this demonstration with the URL bar intact, you’ll need to open it without the iframe.)

index.html

<html>
  <head>
    <title>d3.history example</title>
    <link rel="stylesheet" type="text/css" href="style.css">
  </head>
  <body>
    <div class="target"></div>
    <script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
    <script src="./d3-history.min.js" charset="utf-8"></script>
    <script type="text/javascript" charset="utf-8" src="circles.js"></script>
  </body>
</html>

circles.js

(function(d3) {

    'use strict'

    var target,
        height,
        width,
        grid,
        horizontal,
        style,
        drawing,
        get_active,
        activate,
        render_value,
        items,
        item,
        circle,
        dispatcher,
        initialize,
        bound,
        reset,
        reset_button

    d3.json('data.json', function(error, data) {

        if (error) {
            return new Error('could not fetch data')
        }

        // configuration
        target = d3.select('div.target')
        height = 500
        width = 960
        grid = 80
        horizontal = Math.floor(width / grid) - 2
        style = {
            active: function(selection) {
                selection
                    .style('stroke', 'blue')
                    .style('fill', 'blue')
                    .style('fill-opacity', 0.2)
            },
            inactive: function(selection) {
                selection
                    .style('stroke', 'grey')
                    .style('fill', 'white')
                    .style('fill-opacity', 0.01)
            }
        }
        // draw grid
        drawing = target
            .append('svg')
            .attr('width', width)
            .attr('height', height)
            .append('g')
            .classed('drawing', true)

        // retrieve current active items from URL bar
        get_active = function() {
            var url,
                ids,
                active
            url = window.location.href
            ids = data.map(function(item) {return item.id})
            if (url.indexOf('?') !== -1) {
                active = url
                    .split('?active=').pop()
                    .split(',')
                    .map(function(active) {return +active})
                    .filter(function(active) {return ids.indexOf(active) !== -1})
            } else {
                active = []
            }
            return active
        }

        // various transformations to make to an item on mouseover,
        // stored in a reusable function so they can be easily reused
        // for initialization
        activate = function(selection) {
            var parent
            selection
                .classed('active', true)
                .select('circle')
                .transition()
                .attr('r', grid * 1.2)
                .call(style.active)
            if (history.state && history.state[0]) {
                render_value(history.state[0])
            }
            // move active item to the back so it's easier to select other nodes
            if (selection.node()) {
                parent = selection.node().parentNode
                parent.insertBefore(selection.node(), parent.firstChild)
            }
        }

        // render bound data as text
        render_value = function(data) {
            bound.select('.bound .value').text(JSON.stringify(data))
        }

        // wrapper group
        items = drawing.append('g')
            .attr('transform', 'translate(' + (grid * 1.5) + ',' + (grid * 1.5) + ')')
        // position each item
        item = items.selectAll('g.item')
            .data(data)
            .enter()
            .append('g')
            .classed('item', true)
            .attr('data-id', function(d) {return '_' + d.id})
            .attr('transform', function(d, i) {
                var x,
                    y
                x = i % horizontal * grid
                y = Math.floor(i / horizontal) * grid
                return 'translate(' + x + ',' + y + ')'
            })
        // render circles
        circle = item.append('circle')
        circle
            .attr('r', grid)
            .call(style.inactive)

        // create a d3.history dispatcher object
        dispatcher = d3.history('activate')

        // use d3.history to activate nodes on mouseover
        item.on('mousemove', function(d) {
            var active,
                current_fragment,
                new_fragment
            // get existing active items
            active = get_active()
            current_fragment = '?' + window.location.href.split('?').pop()
            // append new active item
            if (active.indexOf(d.id) === -1) {
                active.push(d.id)
                active.sort(function(a, b) {return a - b})
            }
            // compile active items into URL fragment
            new_fragment = '?active=' + active.join(',')
            // prevent updates with redundant urls
            if (new_fragment !== current_fragment) {
                // activate current item with new URL fragment
                dispatcher.call('activate', this, new_fragment, d)
            }
        })
        // set item state on mouseover
        dispatcher.on('activate', function(d) {
            var selector,
                current_item
            selector = '.item[data-id=_' + d.id + ']'
            current_item = d3.select(selector)
            current_item.classed('active', true)
            current_item.call(activate)
        })

        // display the datum associated with the most recent URL action
        bound = target.append('div')
            .classed('bound', true)
        bound.append('div')
            .classed('features', true)
            .html('Permalinks and forward/back buttons work thanks to <a href="http://github.com/vijithassar/d3-history">d3-history</a>.')
        bound.append('div')
            .classed('explanation', true)
            .text('The state data bound to the most recent URL update event in the HTML5 History API is:')
        bound.append('div')
            .classed('value', true)

        // reset function
        reset = function() {
            // set circles back to initial state
            target.selectAll('circle')
                .attr('r', grid)
                .call(style.inactive)
            bound.select('.value').text('')
        }
        // reset button
        reset_button = target.append('div')
        reset_button
            .classed('reset', true)
            .text('reset')
            .on('click', function() {
                reset()
                // restore url without any actives marked
                history.replaceState(null, null, window.location.href.split('?')[0])
            })

        // initialization function to activate on load
        initialize = function() {
            var active
            // select only the items specified in the URL bar
            active = get_active()
            item
                .filter(function(d, i) {
                    return active.indexOf(d.id) !== -1
                })
                // set UI state
                .call(activate)
        }
        // initialize state on page load
        initialize()

        // initialize state for manual browsing actions
        window.addEventListener('popstate', function(event) {
            reset()
            initialize()
            // the popstate event saves the data associated with the URL change
            // render the popstate value after running the initialization function,
            // since both will try to set the most recently bound data
            if (event.state && event.state[0]) {
                render_value(event.state[0])
            }
        })

    })

}).call(this, d3)

d3-history.min.js

!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports,require("d3-dispatch")):"function"==typeof define&&define.amd?define(["exports","d3-dispatch"],o):o(t.d3=t.d3||{},t.d3)}(this,function(t,o){"use strict";var e;e=function(){var t,e,n,r;return t=Object.create(null),n=Array.prototype.slice.call(arguments),e=o.dispatch.apply(this,n),r=function(t,o,e){window&&window.history&&window.history.pushState(t,o,e)},t.url=function(o){return o&&"function"!=typeof o&&console.error("optional argument to the .url() method of d3.history object must be a function"),"function"==typeof o?(r=o,t):r},t.on=function(o,n){return"string"!=typeof o&&console.error("first argument to .on() method of d3.history object must be an event name"),"function"!=typeof n&&console.error("second argument to .on() method of d3.history object must be a function"),e.on(o,n),t},t.call=function(){var o,e,n,r;return o=arguments[0],e=arguments[1]||null,n=arguments[2],r=Array.prototype.slice.call(arguments,3)||null,t.apply(o,e,n,r),t},t.apply=function(o,n,u,i){var s;return s=null,i=i||null,n=n||null,"string"!=typeof u&&console.error("third argument to history dispatcher must be a string with which to update the url bar"),r(i,s,u),e.apply(o,n,i),t},t};var n=e;t.history=n});

data.json

[{
    "id": 0,
    "value": 26
}, {
    "id": 1,
    "value": 81
}, {
    "id": 2,
    "value": 30
}, {
    "id": 3,
    "value": 88
}, {
    "id": 4,
    "value": 33
}, {
    "id": 5,
    "value": 6
}, {
    "id": 6,
    "value": 66
}, {
    "id": 7,
    "value": 21
}, {
    "id": 8,
    "value": 88
}, {
    "id": 9,
    "value": 48
}, {
    "id": 10,
    "value": 88
}, {
    "id": 11,
    "value": 81
}, {
    "id": 12,
    "value": 36
}, {
    "id": 13,
    "value": 19
}, {
    "id": 14,
    "value": 12
}, {
    "id": 15,
    "value": 47
}, {
    "id": 16,
    "value": 80
}, {
    "id": 17,
    "value": 87
}, {
    "id": 18,
    "value": 9
}, {
    "id": 19,
    "value": 85
}, {
    "id": 20,
    "value": 83
}, {
    "id": 21,
    "value": 39
}, {
    "id": 22,
    "value": 25
}, {
    "id": 23,
    "value": 56
}, {
    "id": 24,
    "value": 77
}, {
    "id": 25,
    "value": 42
}, {
    "id": 26,
    "value": 50
}, {
    "id": 27,
    "value": 6
}, {
    "id": 28,
    "value": 52
}, {
    "id": 29,
    "value": 42
}, {
    "id": 30,
    "value": 6
}, {
    "id": 31,
    "value": 5
}, {
    "id": 32,
    "value": 93
}, {
    "id": 33,
    "value": 72
}, {
    "id": 34,
    "value": 90
}, {
    "id": 35,
    "value": 13
}, {
    "id": 36,
    "value": 71
}, {
    "id": 37,
    "value": 10
}, {
    "id": 38,
    "value": 78
}, {
    "id": 39,
    "value": 92
}]

style.css

svg {
  display: block;
  margin: auto;
}

.item circle {
  stroke-width: 1;
}

.bound {
  width: 15em;
  display: block;
  margin: 1em auto 1em auto;
}

.bound .explanation,
.bound .features {
  color: grey;
  font-style: italic;
  line-height: 1.5em;
  margin-bottom: 1em;
}

.bound .value {
  color: blue;
  text-align: center;
  height: 2em;
  margin: 2em auto 2em auto;
}

.reset {
  text-align: center;
  padding: 0.5em;
  width: 5em;
  border-radius: 0.2em;
  border: 1px solid black;
  display: block;
  margin: auto;
  cursor: pointer;
}

.reset:hover {
  background-color: grey;
}