Выгорание графика в d3.js


Задача создания выгорания график от продажи билетов на Trello

Мы используем на Trello-API в C#, чтобы получить билеты (сделали/создали/и т. д...) Однако это не то, что я хочу пересмотреть, так как это было сделано в основном, прежде чем я присоединился к этому проекту. Однако я немного беспокоюсь о моей JavaScript для код D3.js я создал.

Код

<!DOCTYPE html>
<meta charset="utf-8">
<title>d3 burn chart</title>
<div id="charts"></div>

<style>
    div.tooltip {
        position: absolute;
        text-align: center;
        width: 100px;
        height: 32px;
        padding: 2px;
        font: 12px sans-serif;
        background: lightsteelblue;
        border: 0px;
        border-radius: 8px;
        pointer-events: none;
    }
</style>

<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
    const burnuplist = [
        { Date: "/Date(1476079569717)/", Total:1, Burn:0 },
        { Date: "/Date(1476684369717)/", Total:23, Burn:2 },
        { Date: "/Date(1477289169717)/", Total:32, Burn:17 },
        { Date: "/Date(1477897569717)/", Total:57, Burn:40 },
        { Date: "/Date(1478502369717)/", Total:74, Burn:56 }
      ]

    var formatTime = d3.timeFormat("%d-%m-%Y");
    var drawLine = function(xvalues, yvalues, g, xscale, yscale, options) {
        // Define the div for the tooltip
        var div = d3.select("body").append("div")
            .attr("class", "tooltip")
            .style("opacity", 0);

        // line parts
        const lineparts = yvalues
            .map((y, i, ar) => ({
                i : i,
                x1: xvalues[i - 1],
                x2: xvalues[i],
                y1 : ar[i - 1],
                y2 : y
            }))
            .filter(d => d.i > 0);

        // line
        g.append("g")
            .attr("id", options.id) 
            .selectAll("line")
            .data(lineparts)
            .enter()
            .append("line")
            .attr("x1", d => xscale(d.x1))
            .attr("x2", d => xscale(d.x2))
            .attr("y1", d => yscale(d.y1))
            .attr("y2", d => yscale(d.y2))
            .attr("stroke", options.color);

        if (options.circles) {
            g.append("g")
                .selectAll("circle")
                .data(lineparts)
                .enter()
                .append("circle")
                .attr("r", 5)
                .attr("cx", d => xscale(d.x2))
                .attr("cy", d => yscale(d.y2))
                .attr("fill", options.color)
                .on("mouseover", function (d) {
                    div.transition()
                        .duration(200)
                        .style("opacity", .9);
                    div.html(d.y2 + "</br>" + d.x2)
                        .style("left", (d3.event.pageX) + "px")
                        .style("top", (d3.event.pageY - 28) + "px");
                })
                .on("mouseout", function (d) {
                    div.transition()
                        .duration(500)
                        .style("opacity", 0);
                });
        }
    }

    var burnchart = function(xlabels, burnlist, totallist) {
        const svg = d3.select('svg'),
            margin = {top: 20, right: 40, bottom: 75, left: 50},
            width = +svg.attr("width") - margin.left - margin.right,
            height = +svg.attr("height") - margin.top - margin.bottom,
            g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

        // x-axis
        const x = d3.scalePoint()
            .domain(xlabels)
            .range([0, width]);
        g.append("g")
            .attr("transform", `translate(0,${height})`)
            .call(d3.axisBottom(x))
            .selectAll("text")
            .style("text-anchor", "end")
            .attr("transform", "rotate(-65)")
            .attr("y", 4)
            .attr("x", -10)
            .attr("dy", ".35em");

        // y-axis
        const y = d3.scaleLinear()
            .domain([0, 1.05 * Math.max(d3.max(burnlist), d3.max(totallist))])
            .range([height, 0])
            .nice();
        g.append("g")
            .call(d3.axisLeft(y));

        // total
        drawLine(xlabels, totallist, g, x, y,
            {
                color: "#000000",
                circles: true,
                id: "totallist"
            });

        // burn
        drawLine(xlabels, burnlist, g, x, y,
            {
                color: "#3c763d",
                circles: true,
                id: "burnlist"
            });
    }

    d3.select('#charts')
        .selectAll('svg')
        .data([burnuplist])
        .enter()
        .append('svg')
        .attr('width', 860)
        .attr('height', 480)
        .style('margin', '0.5em')
        .style('border', 'solid 2px #eef6fc')
        .call(burnchart(
                burnuplist.map(d => formatTime(new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10)))),
                burnuplist.map(d => d.Burn),
                burnuplist.map(d => d.Total)
            )
        );
</script>

Результат

burn-up

Вопросы

  • Я использую правильно?
  • Мне как-то нужно сделать массив через JSON данных [burnuplist]
  • Может быть, из области, но есть лучший способ, чтобы изменить то, как я веду на свидание?
  • Общие советы по JavaScript стайлинг?

Я не пишу много на JS, как вы можете видеть ;) любой комментарий приветствуется.



302
4
задан 25 января 2018 в 02:01 Источник Поделиться
Комментарии
2 ответа

Во-первых, ваш код работает, который является хорошей вещью. Однако... для любого опытного разработчика Д3 код выглядит несколько странно: это не идиоматические и он имеет некоторые странные узоры (которые сделают вашу жизнь труднее в будущем).

Здесь я разберусь с вашими пуля-точка 4 вопросы вместе, поскольку они тесно взаимосвязаны.

Итак, давайте сначала посмотрим на основные проблемы:


  1. Вы привязки данных в формате SVG в отбор вступать, но вы никогда не использовать его. Так, просто добавить один СВГ, без привязки любых данных.

  2. Падение burnChart и drawLine функции. Если вы хотите создать отдельную функцию рисования, что является хорошим подходом (относительно сухой), передавать только изменения данных, избегая повторений (Весы всегда одни и те же, например). Но в графике такой один такой шаблон не нужен (посмотрите на each() в моем последнем фрагменте).

  3. Вы связываете единый массив чисел, как линии и круги. Вместо этого, просто фильтровать burnuplist массив соответственно (или создать два отдельных массивов данных, как я делаю здесь), и связывают весь массив объектов в круги и в путь (что приводит нас к следующему пункту). Таким образом, каждый элемент имеет нулевой точки всех свойств в объекте, что мы можем легко получить доступ. Что еще более важно, таким образом, вы не полагаться на индекс каждого элемента в массиве, как вы делаете прямо сейчас и которая является неадекватный подход.

  4. Не добавлять кучу <line> элементы для создания линейной диаграммы. Д3 имеет линию генераторы, которые можно легко использовать для создания <path> элемент: вся линия будет единая СВГ пути. Это намного удобней, и на вершине, что вы можете использовать кривые , чтобы сгладить путь.

  5. У вас есть время на оси X. Тем не менее, вы преобразуете даты объекты в строки и с помощью балльной шкале с этих строк как домен, который, вероятно, не лучшая практика. Относиться к времени как времени, используя временную шкалу, если вы действительно хотите, чтобы лечить каждый отдельный момент в качестве категориальной переменной (например, когда промежуток времени не имеет значения и вы хотите, чтобы пространство между тиками должен быть одинаковым, независимо от фактического времени).

Некоторые незначительные проблемы:


  1. Имя все ваши выборы. Это легче, если вы хотите использовать их в будущем.

  2. Давать осмысленные имена переменным, как xScaleне просто x.

  3. Вы смешиваете const и var, который может показаться странным Фоми некоторые люди, так же как и стрелки функции с регулярными функциями.

  4. Использование строчных свойств: date вместо Date, total вместо Total и т. д...

Все это, как говорится, это моя версия кода. Это крупный рефакторинг (на самом деле, я писал его с нуля).



var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');

var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);

var burnuplist = [{
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
}, {
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
}, {
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
}, {
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
}, {
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
}];

var margin = {
top: 20,
right: 40,
bottom: 75,
left: 50
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

var formatTime = d3.timeFormat("%d-%m-%Y");

burnuplist.forEach(function(d) {
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
});

var totalData = burnuplist.map(function(d) {
return {
date: d.Date,
value: d.Total
}
});

var burnData = burnuplist.map(function(d) {
return {
date: d.Date,
value: d.Burn
}
});

var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d) {
return d.Date
}));

var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d) {
return d.Total
}) * 1.05]);

var lineGenerator = d3.line()
.x(function(d) {
return xScale(d.date)
})
.y(function(d) {
return yScale(d.value)
});

var gX = g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickFormat(function(d) {
return formatTime(d)
}).tickValues(burnuplist.map(function(d) {
return d.Date
})))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");

var gY = g.append("g")
.call(d3.axisLeft(yScale));

var totalLine = g.append("path")
.datum(totalData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#000");

var burnLine = g.append("path")
.datum(burnData)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", "#3c763d");

var totalCircles = g.selectAll(null)
.data(totalData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return xScale(d.date)
})
.attr("cy", function(d) {
return yScale(d.value)
})
.style("fill", "#000");

var burnCircles = g.selectAll(null)
.data(burnData)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return xScale(d.date)
})
.attr("cy", function(d) {
return yScale(d.value)
})
.style("fill", "#3c763d");

d3.selectAll("circle").on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});


div.tooltip {
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}

<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>


В totalLine, burnLine, totalCircles и burnCircles может показаться, что много повторов, но это идиоматические Д3. Если это беспокоит вас, просто использовать each() для сухой, как это:



var svg = d3.select('#charts')
.append('svg')
.attr('width', 860)
.attr('height', 480)
.style('margin', '0.5em')
.style('border', 'solid 2px #eef6fc');

var div = d3.select("#charts").append("div")
.attr("class", "tooltip")
.style("opacity", 0);

var burnuplist = [{
Date: "/Date(1476079569717)/",
Total: 1,
Burn: 0
}, {
Date: "/Date(1476684369717)/",
Total: 23,
Burn: 2
}, {
Date: "/Date(1477289169717)/",
Total: 32,
Burn: 17
}, {
Date: "/Date(1477897569717)/",
Total: 57,
Burn: 40
}, {
Date: "/Date(1478502369717)/",
Total: 74,
Burn: 56
}];

var margin = {
top: 20,
right: 40,
bottom: 75,
left: 50
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

var formatTime = d3.timeFormat("%d-%m-%Y");

burnuplist.forEach(function(d) {
d.Date = new Date(parseInt(d.Date.replace("/Date(", "").replace(")/", ""), 10))
});

var totalData = burnuplist.map(function(d) {
return {
date: d.Date,
value: d.Total
}
});

var burnData = burnuplist.map(function(d) {
return {
date: d.Date,
value: d.Burn
}
});

var xScale = d3.scaleTime()
.range([0, width])
.domain(d3.extent(burnuplist, function(d) {
return d.Date
}));

var yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, d3.max(burnuplist, function(d) {
return d.Total
}) * 1.05]);

var lineGenerator = d3.line()
.x(function(d) {
return xScale(d.date)
})
.y(function(d) {
return yScale(d.value)
});

var gX = g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(xScale).tickFormat(function(d) {
return formatTime(d)
}).tickValues(burnuplist.map(function(d) {
return d.Date
})))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)")
.attr("y", 4)
.attr("x", -10)
.attr("dy", ".35em");

var gY = g.append("g")
.call(d3.axisLeft(yScale));

var groups = g.selectAll(null)
.data([totalData, burnData])
.enter()
.append("g")
.each(function(d, i) {
var line = d3.select(this).append("path")
.datum(d)
.attr("d", lineGenerator)
.style("fill", "none")
.style("stroke", i ? "#3c763d" : "#000");

var circles = g.selectAll(null)
.data(d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d) {
return xScale(d.date)
})
.attr("cy", function(d) {
return yScale(d.value)
})
.style("fill", i ? "#3c763d" : "#000");
})

d3.selectAll("circle").on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Value: " + d.value + "</br>Date: " + formatTime(d.date))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});


  div.tooltip {
position: absolute;
text-align: center;
width: 100px;
height: 32px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}

<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="charts"></div>



4
ответ дан 26 января 2018 в 12:01 Источник Поделиться

С Херардо уже дал отличный отзыв и сосредоточился на код D3, я хотел бы сосредоточиться на JavaScript-код.

Абстрагируясь установки кода непрозрачность - Д. Р. Ю.

Согласно принципу Д. Ю. Р., код, чтобы задать непрозрачность в подсказке может быть абстрагирована в функцию, которая принимает длительность и непрозрачность. Опираясь на Херардо совет, подсказка элемент может также иметь более подходящего названия, как tooltipContainer (Если ты в целом лаконичности вещь... тогда, наверное, просто tooltipхотя, что может быть путаете с CSS-класса).

var tooltipContainer = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);

Херардо указал на смесь const и var использование. Если вы хотели сохранить строгое использование const то, что контейнер мог быть объявлены с использованием const.

После этого функция может быть определено, что задает непрозрачность

var transitionTooltipToOpacity = function(duration, opacity) {    
tooltipContainer.transition()
.duration(duration)
.style("opacity", opacity);
};

Тогда mouseover и mouseout обработчики событий могут использовать эту функцию.
Следующие строки:


.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
});

можно упростить следующим образом:

.on("mouseover", function (d) {
transitionTooltipToOpacity(200, .9);
tooltipContainer.html(d.y2 + "</br>" + d.x2)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", transitionTooltipToOpacity.bind(null, 500, 0));

Обратите внимание, что обработчик mouseout есть частичная функция, которая создается с помощью функции.привязать(). Таким образом, функция будет вызываться с длительность и непрозрачность значения, установленные (и другие аргументы, которые передаются в обработчики событий, как текущий датум dиндекс iи т. д. также передаются этой функции в данном случае).

Границы в CSS

Учитывая, что вы в основном хотели обратной связи на JavaScript, я заметил, что стиль для всплывающей подсказки, содержит: border: 0px. Я не считаю, что это необходимо, если есть какой-то другой стиль декларации, которые должны быть переопределены. Если это необходимо, то можно опустить единиц (т. е. px) или использовать none. Хотя за 9 лет (и о ЭМы), так что этот вопрос представляется актуальным.

2
ответ дан 26 января 2018 в 06:01 Источник Поделиться