Не удается получить карту дерева для отображения с новыми данными JSON

Я использую d3.js treemap в приложении с backbone.js. Карта дерева правильно отображается с первыми данными JSOn, но последующие вызовы с другими данными JSON не приводят к повторной визуализации карты дерева.

Мой HTML выглядит так:

<html>
<head>
    <title>Jenkins analytics</title>

    <!-- stylesheets -->
    <link rel="stylesheet" href="css/spa.css" type="text/css"/>
    <link rel="stylesheet" href="css/treemap.css" type="text/css"/>
</head>

<body>
    <nav>
        <form>
            <fieldset>
                <label for="chart">Chart:</label>
                <select id="chart" name="chart">
                    <option value="treemap" selected>Treemap</option>
                    <option value="motion">MotionChart</option>
                </select>
            </fieldset>
        </form>
        <form>
            <fieldset>
                <label for="period">Period:</label>
                <select id="period" name="period">
                    <option value="lastday" selected>Day</option>
                    <option value="lastweek">Week</option>
                    <option value="lastmonth">Month</option>
                </select>

                <label for="team">Team:</label>
                <select id="team" name="team">
                    <option value="all" selected>all</option>
                    <option value="spg">spg</option>
                    <option value="beacon">beacon</option>
                    <option value="disco">disco</option>
                </select>

                <label for="status">Status:</label>
                <select id="status" name="status">
                    <option value="" selected>all</option>
                    <option value="SUCCESS">success</option>
                    <option value="FAILURE">failure</option>
                    <option value="ABORTED">aborted</option>
                </select>
            </fieldset>
        </form>
        <form>
            <fieldset>
                <label for="duration">Duration</label>
                <input id="duration" type="radio" name="mode" value="size" checked />

                <label for="count">Count</label>
                <input id="count" type="radio" name="mode" value="count" />

                <label for="average">Average</label>
                <input id="average" type="radio" name="mode" value="avg" />
            </fieldset>
        </form>
    </nav>

    <div id="container" />

    <!-- Third party javascript -->
    <script type="text/javascript" src="http://code.jquery.com/jquery-2.0.3.min.js" charset="utf-8"></script>
    <script type="text/javascript" src="script/underscore/underscore.js" charset="utf-8"></script>
    <script type="text/javascript" src="script/backbone/backbone.js" charset="utf-8"></script>
    <script type="text/javascript" src="script/d3/d3.v3.js" charset="utf-8"></script>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>

    <!-- Application javascript -->
    <script type="text/javascript" src="script/module.js"></script>

    <!-- Startup -->
    <script>
        var navview = new NavViewer();
    </script>
</body>
</html>

script/module.js выглядит так:

var NavViewer = Backbone.View.extend({
    el: 'nav',

    events: {
        "change #chart": "change_chart",
        "change #period": "change_period",
        "change #team": "change_team",
        "change #status": "change_status"
    },

    initialize: function() {
        console.log("NavViewer.initialize");
        this.d3view = new D3Viewer();
        this.active_view = this.d3view;
    },

    change_chart: function(e) {
        console.log("NavViewer.change_chart");
    },
    change_period: function(e) {
        var _period = $('#period').val();
        console.log("NavViewer.change_period to " + _period);
        this.active_view.load();
    },
    change_team: function(e) {
        var _team = $('#team').val();
        console.log("NavViewer.change_team to "+ _team);
        this.active_view.load();
    },
    change_status: function(e) {
        var _status = $('#status').val();
        console.log("NavViewer.change_status to " + _status);
        this.active_view.load();
    }
});

var JenkinsViewer = Backbone.View.extend({
    el: '#container',
    server: "http://192.168.1.100:5000",
    url_fragment: function() {
        var _period = $('#period').val();
        var _team = $('#team').val();
        var _status = $('#status').val();
        return "when=" + _period +
            (_team == "all" ? "" : ("&" + "team=" + _team)) +
            (_status == "" ? "" : ("&" + "status=" + _status));
    }
});

var D3Viewer = JenkinsViewer.extend({
    initialize: function() {
        this.margin = {top: 8, right: 0, bottom: 0, left: 0};
        this.width = 1200 - this.margin.left - this.margin.right;
        this.height = 800 - this.margin.top - this.margin.bottom - 60;
        this.container = d3.select(this.el);
        this.color = d3.scale.category20c();
        this.base_url = this.server + "/team_build";

        this.treemap = d3.layout.treemap()
            .size([this.width, this.height])
            .sticky(true)
            .value(function(d) { return d.size; });

        this.position = function() {
            this.style("left", function(d) { return d.x + "px"; })
                .style("top", function(d) { return d.y + "px"; })
                .style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
                .style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
        };

        /* style the container */
        this.container
            .style("position", "relative")
            .style("width", this.width + "px")
            .style("height", this.height + "px")
            .style("left", this.margin.left + "px")
            .style("top", this.margin.top + "px")
            .style("border", "1px solid black");

        /* tootlip is appended to container */
        this.tooltip = this.container.append("div")
            .attr('class', 'tooltip')
            .style("visibility", "hidden")
            .style("background-color", "#ffffff");

        this.load();
    },

    load: function() {
        var $container = this.container;
        var color = this.color;
        var treemap = this.treemap;
        var position = this.position;
        var tooltip = this.tooltip;
        var url = this.base_url + "?" + this.url_fragment();

        console.log("D3View.load: " + url);

        d3.json(url, function(error, root) {
            /* 'root' actually means the data retrieved by the xhr call */
            var node = $container.datum(root)
                .selectAll(".node")
                .data(treemap.nodes);

            node.enter().append("div")
                .attr("class", "node")
                .call(position)
                .style("background", function(d) { return d.children ? color(d.name) : null; })
                .text(function(d) { return d.children ? null : d.name; })
                .on("mouseover", function(d) {
                    tooltip.html(d.name + ": " + Math.floor(d.value))
                      .style("visibility", "visible");
                    this.style.cursor = "hand";
                })
                .on("mouseout", function(){
                    tooltip.style("visibility", "hidden");
                });

            d3.selectAll("input").on("change", function change() {
                var functions = {
                    count: function(d) { return d.count; },
                    size: function(d) { return d.size; },
                    avg: function(d) { return d.size / d.count; }
                };
                var value = functions[this.value];

                node
                    .data(treemap.value(value).nodes)
                .transition()
                    .duration(1500)
                    .call(position);
            });
        });

        return true;
    }
});

Вот что я сделал:

Я думаю, что проблема может быть связана с залипанием или отсутствием вызова node.exit().remove(). Я пытался изменить оба из них без успеха. Однако, чтобы получить интерактивный пользовательский интерфейс на стороне клиента, я думаю, мне нужно использовать treemap.sticky(true).

Я подтвердил, что каждый раз, когда я обращаюсь к службе REST API, я получаю разные JSON. Я подтвердил, что container.datum().children изменяется в размере между вызовами, подтверждая для меня, что это вопрос древовидной карты, а не повторного рендеринга.

D3.js: How to handle dynamic JSON Data выглядит очень актуально:

  • «Стоит отметить, что если у вас уже есть данные с тем же ключом, d3.js сохранит данные в узле, но по-прежнему будет использовать исходные данные».
  • «Когда я начал играть с d3.js, я думал, что визуализированные данные будут обрабатываться событиями и реагировать на изменение масштабов, доменов и прочего. Это не так. Вы должны указать d3.js обновить текущие данные, иначе они останутся там. , не синхронизировано."
  • «Я присваиваю результат data() переменной, потому что enter() влияет только на новые данные, которые не привязаны к узлам».

person hughdbrown    schedule 09.01.2014    source источник
comment
Чтобы было ясно: используете ли вы одну и ту же функцию load() как для начального, так и для последующих вызовов данных? Потому что как есть, он устанавливает атрибуты только для новых элементов, а не для существующих с измененными данными. Прочитайте этот ответ для более подробного объяснения: stackoverflow.com/a/20754006/3128209   -  person AmeliaBR    schedule 10.01.2014
comment
@AmeliaBR: Да, я использую тот же load(). Я исправлю в эту субботу и опубликую исправленный код. Однако, поскольку это явно вырезанный и вставленный код, было бы неплохо, если бы в этом примере D3 также использовались лучшие практики. Спасибо за вашу помощь.   -  person hughdbrown    schedule 10.01.2014
comment
К сожалению, большинство примеров и руководств предназначены только для демонстрации одного аспекта кода и, как правило, ускользают от других аспектов. Таким образом, они показывают, как раскладывать древовидную карту, но не показывают, как ее обновлять или что-то в этом роде. Если вы начнете с действительно простых вещей и пойдете дальше, вы узнаете, что вам нужно адаптировать. Но большинство людей хотят перейти к сложным примерам визуализации, а затем не знают, где в коде что-то идет не так.   -  person AmeliaBR    schedule 10.01.2014
comment
@hughdbrown, да, действительно, почему этот парень, у которого ты скопировал код, не написал именно то, что тебе нужно? Напишите ему, дайте ему $#$% и скажите ему, что в следующий раз, когда он напишет пример, он должен знать лучше. Для всего, что вы делаете, в сети должен быть пример, чтобы вы могли просто скопировать/вставить, верно?...   -  person VividD    schedule 11.01.2014
comment
@VividD: Спасибо, что нашли время опубликовать свои насмешки. Позвольте мне прояснить: я искренне благодарен разработчикам ОС и работе, которую они вложили в свои библиотеки и документацию. Я знаю, что более продуктивен для этого. Поскольку вы ответили на 50 вопросов о d3.js, вы так же, как и я, знаете, что это был не просто какой-то парень. И API D3 довольно нестандартен в том, как он подходит к рендерингу новых данных. Сравните это, например, с визуализацией данных Google. Это то, что я бы выделил, если бы писал пример кода. Так что по многим причинам я с вами не согласен.   -  person hughdbrown    schedule 13.01.2014
comment
@hughdbrown, мой комментарий был ироничным юмором. Извините, если это, возможно, задело ваши чувства или что-то подобное. Что касается документации, я согласен с вами, что она должна быть более ориентирована на пользователя.   -  person VividD    schedule 13.01.2014


Ответы (1)


Вот что я сделал в своем методе load:

load: function() {
    var $container = this.container;
    var color = this.color;
    var treemap = this.treemap;
    var position = this.position;
    var tooltip = this.tooltip;
    var url = this.base_url + "?" + this.url_fragment();

    console.log("D3View.load: " + url);

    d3.json(url, function(error, root) {
        var tooltip_mouseover = function(d) {
            tooltip.html(d.name + ": " + Math.floor(d.value))
              .style("visibility", "visible");
            this.style.cursor = "hand";
        };
        var tooltip_mouseout = function(){
            tooltip.style("visibility", "hidden");
        };
        var background_color = function(d) { return d.children ? color(d.name) : null; };
        var text_format = function(d) { return d.children ? null : d.name; };

        /*
         * Refresh sticky bit
         * https://github.com/mbostock/d3/wiki/Treemap-Layout
         * "Implementation note: sticky treemaps cache the array of nodes internally; therefore, it
         * is not possible to reuse the same layout instance on multiple datasets. To reset the
         * cached state when switching datasets with a sticky layout, call sticky(true) again."
         */
        treemap.sticky(true);

        /* 'root' actually means the data retrieved by the xhr call */
        var nodes = $container.datum(root)
            .selectAll(".node")
            .data(treemap.nodes);

        var enter = nodes.enter().append("div")
            .attr("class", "node")
            .call(position)
            .style("background", background_color)
            .text(text_format)
            .on("mouseover", tooltip_mouseover)
            .on("mouseout", tooltip_mouseout);

        var exit = nodes.exit().remove();

        var update = nodes.style("background", background_color)
            .call(position)
            .text(text_format);

        d3.selectAll("input").on("change", function change() {
            var functions = {
                count: function(d) { return d.count; },
                size: function(d) { return d.size; },
                avg: function(d) { return d.size / d.count; }
            };
            var value = functions[this.value];

            $container.selectAll(".node")
                .data(treemap.value(value).nodes)
            .transition()
                .duration(1500)
                .call(position);
        });
    });

Есть два важных изменения:

  1. Сбросить липкий бит карты дерева
  2. Используйте выбор входа/выхода/обновления для новых/отсутствующих/измененных узлов/данных

Обратите также внимание, что узлы enter имеют побочный эффект добавления div класса node, а узлы exit и update не ссылаются на этот div-класс, за исключением того, что они делают это при создании nodes. Если добавить дополнительный выбор на node-class в этих местах, ваш выбор будет пустым, а коды exit и update не будут работать.

Спасибо AmeliaBR за публикацию действительно полезного комментария со ссылкой на составленный ею SO-ответ.

Другие полезные материалы:

person hughdbrown    schedule 15.01.2014
comment
Рад, что вы поняли это, и спасибо, что вернулись, чтобы опубликовать свои решения и хорошие ссылки для других. Процесс обновления легко запутать, но как только вы полностью его поймете, он станет автоматическим, и вы легко забудете, насколько запутанным он был в начале! - person AmeliaBR; 16.01.2014