D3

Когда я работаю над проектами с визуализацией, мои менеджеры обычно ужасаются, когда я говорю, что не использую D3. Почему они так переживают? Почему я отказался от D3?

Для ответа на эти вопросы мы должны понимать, когда появилась D3. Впервые она была выпущена в 2011 году, и в то время была весьма инновационной.

Обычно вы видите библиотеки, предоставляющие готовые графики с огромным списком функций. Они неплохо работают, но когда появляются новые требования, появляется проблема: нужно добавлять новую функцию. В итоге мы имеем язык, в котором для создания графика нужны несколько объектов. D3 работает иначе: вместо того, чтобы предоставлять целые компоненты, она даёт вам функции, помогающие управлять данными. И вы можете сами создавать эти компоненты.

В то время были популярны библиотеки jQuery и Backbone. Создание графиков при помощи этих инструментов было весьма сложным, особенно если вы хотели сделать графики динамическими. В браузерах только начинали использовать такие новые современные стандарты CSS, как переходы, а до появления флексбоксов было ещё несколько лет.

D3 решила многие проблемы, и, без сомнений, она была самым простым методом визуализации в то время. Однако с тех пор многое поменялось. У нас есть новые современные фреймворки, в которых используются более гибкие и выразительные идеи, например, Virtual DOM. А в CSS есть много новых возможностей при создании макетов страниц и анимаций.

Вместо того, чтобы сразу начинать работать с библиотекой D3, позвольте мне предоставить несколько доводов, почему вам стоит еще раз задуматься над тем, стоит или не стоит ее использовать.

Долгое обучение

Я работал с D3 в течение нескольких последних лет, применял её для созданий всех видов визуализаций. Я понимаю основную идею D3, но при этом мне всё ещё трудно с ней работать. Да и всем моим коллегам, с которыми я работал, от молодых, до опытных разработчиков. Многие люди, и я в том числе, ищут в Интернете образец, который примерно похож на то, что нам нужно. А потом просто переделываем этот образец под наши нужды.

Если мы хотим добавить какие-то индивидуальные функции, нам, скорее всего, придется снова искать в Интернете что-то похожее на правильный вариант. Затем тестировать его работу и продолжать модифицировать, пока не получим то, что хотим.

Вам это знакомо? С другой стороны, в наше время разработчики уже хорошо знакомы с virtual DOM и шаблонами. Не лучше ли использовать их, а не целую библиотеку, которая требует совсем другого образа мыслей?

Это легче, чем вам кажется

Чаще всего, когда люди впервые задумываются о создании своих графиков с нуля, они боятся и волнуются. Возможно, вы думаете, что эти компоненты очень сложно создавать. Но когда вы разберетесь, то поймете, что это не так. Возьмем, например, линейный график. Вот, что вам нужно сделать для его создания в D3:

// устанавливаем размеры и границы графика
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
// парсим время
var parseTime = d3.timeParse("%d-%b-%y");
// устанавливаем диапазоны
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// определяем оси
var valueline = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
// добавляем на страницу объект svg
// добавляем элемент 'group' в 'svg'
// перемещаем элемент 'group' к верхнему левому краю
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");
// загружаем данные
d3.csv("data.csv", function(error, data) {
  if (error) throw error;
// форматируем данные
  data.forEach(function(d) {
      d.date = parseTime(d.date);
      d.close = +d.close;
  });
// масштабируем диапазон данных
  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Добавляем график значений
  svg.append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", valueline);
// Добавляем ось X
  svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));
// Добавляем ось Y
  svg.append("g")
      .call(d3.axisLeft(y));
});

Источник: bl.locks.org

А вот, как бы вы сделали это в Preact:

/* @jsx h */
let { Component, h, render } = preact
function getTicks (count, max) {
    return [...Array(count).keys()].map(d => {
        return max / (count - 1) * parseInt(d);
    });
}
class LineChart extends Component {
    render ({ data }) {
        let WIDTH = 500;
        let HEIGHT = 300;
        let TICK_COUNT = 5;
        let MAX_X = Math.max(...data.map(d => d.x));
        let MAX_Y = Math.max(...data.map(d => d.y));
        
        let x = val => val / MAX_X * WIDTH;
        let y = val => HEIGHT - val / MAX_Y * HEIGHT;
        let x_ticks = getTicks(TICK_COUNT, MAX_X);
        let y_ticks = getTicks(TICK_COUNT, MAX_Y).reverse();
                
        let d = `
          M${x(data[0].x)} ${y(data[0].y)} 
          ${data.slice(1).map(d => {
              return `L${x(d.x)} ${y(d.y)}`;
          }).join(' ')}
        `;
    
        return (
            <div 
                class="LineChart" 
                style={{
                    width: WIDTH + 'px',
                    height: HEIGHT + 'px'
                }}
            >
                <svg width={WIDTH} height={HEIGHT}>
                    <path d={d} />
                </svg>
                <div class="x-axis">
                    {x_ticks.map(v => <div data-value={v}/>)}
                </div>
                <div class="y-axis">
                    {y_ticks.map(v => <div data-value={v}/>)}
                </div>
            </div>
        );
    }
}
let data = [
    {x: 0, y: 10}, 
    {x: 10, y: 40}, 
    {x: 20, y: 30}, 
    {x: 30, y: 70}, 
    {x: 40, y: 0}
];
render(<LineChart data={data} />, document.querySelector("#app"))

И в CSS:

body {
    margin: 0;
    padding: 0;
    font-family: sans-serif;
    font-size: 14px;
}
.LineChart {
    position: relative;
    padding-left: 40px;
    padding-bottom: 40px;
}
svg {
    fill: none;
    stroke: #33C7FF;
    display: block;
    stroke-width: 2px;
    border-left: 1px solid black;
    border-bottom: 1px solid black;
}
.x-axis {
    position: absolute;
    bottom: 0;
    height: 40px;
    left: 40px;
    right: 0;
    display: flex;
    justify-content: space-between;
}
.y-axis {
    position: absolute;
    top: 0;
    left: 0;
    width: 40px;
    bottom: 40px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-end;
}
.y-axis > div::after {
    margin-right: 4px;
    content: attr(data-value);
    color: black;
    display: inline-block;
}
.x-axis > div::after {
    margin-top: 4px;
    display: inline-block;
    content: attr(data-value);
    color: black;
}

Источник: JSFiddle

Простой график при помощи Preact и CSS

Тут достаточно много кода, но я использую только инструменты, которыми я владею. В данном случае это моя библиотека — Preact (в принципе, может быть любая: React, Vue, Angular и т.д.) и современные инструменты CSS, например, флексбоксы.

Пример с D3 требует достаточно много знаний. А последний пример требует только тех знаний о вашей библиотеке, которые у вас итак уже есть. Также я считаю, что он гораздо легче, чем пример с D3. Если вы разбираетесь в библиотеке, то можете перейти к коду и внести какие-то изменения.

Не забывайте о размере пакета данных

В зависимости от типа графиков и того, насколько tree-shaking помог оптимизировать размер данных, D3 будет импортировать, в худшем случае, 70+KB кода. Это может повлиять на время загрузки вашего сайта. То есть, хоть вы и правда пишете больше кода, чем в оригинальном примере с D3, в целом кода обрабатывается меньше.

Используя сторонний код, всегда важно помнить об издержках пользования этим кодом. Если вам нужна всего лишь одна функция из библиотеки, будете ли вы ради этого импортировать всю библиотеку? Да, обычно не стоит изобретать велосипед, но вам нужно принимать в расчет время на изучение библиотеки, размер данных, который она добавляет, лицензию этой библиотеки, её поддержку, добросовестность её создателей, возможность быстро устранять баги и т.д.

Canvas и HTML чаще всего лучше SVG

Как вы могли заметить в приведенном ранее примере, мы использовали комбинацию HTML и SVG. По какой-то причине,многие пытаются делать свои графики при помощи SVG, хотя никакой нужды в этом нет. Сейчас CSS гораздо лучше, чем SVG.

Например, в SVG нет функции обтекания текстом. Если нам нужно его применить, придется рассчитывать его в JavaScript:

function wrap(text, width) {
  text.each(function() {
    var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.1, // ems
        y = text.attr("y"),
        dy = parseFloat(text.attr("dy")),
        tspan = text.text(null)
           .append("tspan")
           .attr("x", 0)
           .attr("y", y)
           .attr("dy", dy + "em");
while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan")
           .attr("x", 0)
           .attr("y", y)
           .attr("dy", ++lineNumber * lineHeight + dy + "em")
           .text(word);
      }
    }
  });
}

Источник: bl.ocks.org

С другой стороны, в HTML при white-space, установленном наnormal, обтекание текстом произойдет само.

В HTML и CSS можно создать такие элементы, как круги и прямоугольники. Вы можете использоватьtransform and border-radiusдля создания любых фигур. Если вы хотите создать столбиковую диаграмму с двумя закругленными углами в D3, вы не сможете использовать rect, потому что тогда закруглятся все 4 угла, а не те 2, которые вы хотели. Вашим единственным вариантом будет использование path.

Тегpath — единственная причина, по которой я использую теги SVG. Это единственный “чистый” способ создания произвольных фигур, которому нет эквивалента в HTML.

Если вам нужно больше производительности, обратите внимание на тег canvas. С canvas вам придется самому прописывать основные взаимодействия, но зато не будет слишком много HTML или SVG, которые требуют много памяти и медленнее обновляются. В canvas вы можете изменять отдельные пиксели, как вам угодно, поэтому легко сможете оптимизировать и масштабировать визуализацию. Новые API браузеров, такие как OffscreenCanvas, также помогут увеличить производительность, когда их используют в Workers.

Но ведь Canvas нельзя масштабировать, как в SVG?

Часто я слышу, что canvas не подходит для визуализации, потому что его нельзя масштабировать так, как SVG. При обычном использовании canvas, если вы приближаете и отдаляете изображение, или используете мониторы с высоким разрешением, ваши изображения в canvas будет размытыми и пиксельными.

Это происходит потому, что при создании canvas вам нужно указать, сколько пикселей он будет прорисовывать. Когда мы устанавливаем параметрыwidth и height, вы можете подумать, что это то же самое, что настройка размера в CSS. Но на самом деле мы настраиваем размер изображения canvas. А это не одно и то же.

Canvas размером 50х50, но растянутый до 200х200. Это приводит к размытию.

Обычно количество пикселей CSS будет настроено на такой же размер, как и изображение canvas. Но при приближении/отдалении страницы, вы снова столкнетесь с этой проблемой. Для её решения используйте window.devicePixelRatio и масштабируйте размер изображения canvas.

onResize() {
    let canvas = this.base.querySelector('canvas');
    let ctx = canvas.getContext('2d');
    let PIXEL_RATIO = window.devicePixelRatio;
canvas.width = canvas.offsetWidth * PIXEL_RATIO;
    canvas.height = canvas.offsetHeight * PIXEL_RATIO;
    ctx.setTransform(PIXEL_RATIO, 0, 0, PIXEL_RATIO, 0, 0);
    
    this.props.onDraw(ctx, canvas.offsetWidth, canvas.offsetHeight);
}

Источник: JSFiddle

Используя соотношение пикселей, мы можем увеличить размер изображения в canvas. Но этого недостаточно, нам нужно сообщить canvas, что последующие операции должны быть масштабированы. Это решит все проблемы с масштабом.

Отличите ли вы canvas от SVG?

Приближенный в браузере обычный canvas, масштабированый canvas и SVG.

Вывод

Как вы видите, есть множество доводов в пользу того, что D3 уже заметно устарел для решения большинства задач. С момента его выпуска Интернет значительно развился. Если вы создаете простые графики, вроде круговых и столбиковых диаграмм, диаграмм разброса и линейных графиков, попробуйте сделать их при помощи используемого вами фреймворка. В D3 нет ничего особенного для решения таких задач. А если говорить о поддержке, то ваш код, скорее всего, даже легче будет поддерживать. А если необходимо будет что-то изменить, это тоже не будет сложной задачей.

У менеджеров нет поводов переживать о том, что кто-то не использует D3, также, как и вам! 
Перевод статьи Paul Sweeney: Why I no longer use D3.js