0

I am building a reusable chart following this tutorial: https://bost.ocks.org/mike/chart/. The full code is at the end of the question. I have the following problem:

As you can see the 'click' event on a specific component triggers a query that updates the whole chart retrieving new data. I am referring to this line:

selection.datum(relatedConcepts).call(chart);       // Update this vis

Now this update works great, but of course given that in the function "chart" I also have

color.domain(data.map(function(d){ return d[0]}));

the domain of the color scale will be also updated and I don't want that. So the question is: how do I set the color scale domain ONLY the first time the chart gets created?

d3.custom = d3.custom || {};
d3.custom.conceptsVis = function () {

    var color = d3.scale.category20();

    // To get events out of the module we use d3.dispatch, declaring a "conceptClicked" event
    var dispatch = d3.dispatch('conceptClicked');

    function chart(selection) {

        selection.each(function (data) {

            //TODO: This should be executed only the first time
            color.domain(data.map(function(d){ return d[0]}));

            // Data binding
            var concepts = selection.selectAll(".progress").data(data, function (d) {return d[0]});

            // Enter
            concepts.enter()
                .append("div")
                .classed("progress", true)
                .append("div")
                .classed("progress-bar", true)
                .classed("progress-bar-success", true)
                .style("background-color", function (d) {
                    return color(d[0])
                })
                .attr("role", "progressbar")
                .attr("aria-valuenow", "40")
                .attr("aria-valuemin", "0")
                .attr("aria-valuemax", "100")
                .append("span") // (http://stackoverflow.com/questions/12937470/twitter-bootstrap-center-text-on-progress-bar)
                .text(function (d) {
                    return d[0]
                })
                .on("click", function (d) {

                    // Update the concepts vis
                    d3.json("api/concepts" + "?concept=" + d[0], function (error, relatedConcepts) {
                        if (error) throw error;
                        selection.datum(relatedConcepts).call(chart);       // Update this vis
                        dispatch.conceptClicked(relatedConcepts, color);    // Push the event outside
                    });
                });

            // Enter + Update
            concepts.select(".progress-bar").transition().duration(500)
                .style("width", function (d) {
                    return (d[1] * 100) + "%"
                });

            // Exit
            concepts.exit().select(".progress-bar").transition().duration(500)
                .style("width", "0%");

        });
    }

    d3.rebind(chart, dispatch, "on");
    return chart;
};

ANSWER I ended up doing what meetamit suggested and I added this:

// Getter/setter
chart.colorDomain = function(_) {
    if (!arguments.length) return color.domain();
    color.domain(_);
    return chart;
};

to my conceptsVis function, so that from the outside I can do:

.... = d3.custom.conceptsVis().colorDomain(concepts);

Of course I deleted the line:

color.domain(data.map(function(d){ return d[0]}));
valenz
  • 311
  • 1
  • 2
  • 13

1 Answers1

1

You can check if the domain is an empty array and only populate it if it is:

if(color.domain().length == 0) {
  color.domain(data.map(function(d){ return d[0]}));
}

That being said, this behavior seems fundamentally wrong, or at least bug-prone. It means that the populating of the domain is a side-effect of the first render. But what is it about that first render that makes it different than subsequent calls and therefore worthy of setting the domain? What happens if later, as your app evolves, you decide to render a different dataset first and afterwards render what is currently the first dataset? Then you might end up with a different domain. It seems more sane to compute the domain explicitly, outside of the chart's code, and then pass the domain into the chart via a setter. Something like:

chart.colorDomain(someArrayOfValuesThatYouPreComputeOrHardCode)
meetamit
  • 24,727
  • 9
  • 57
  • 68
  • 1
    Your answer clarifies the deeper issue. I just started playing with this reusable charts and I wasn't so sure about what to write where... thanks. – valenz Feb 25 '16 at 22:49