Почему я больше не использую D3.js

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

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

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

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

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

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

Кривая обучения

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

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

Звучит знакомо? С другой стороны, современные разработчики хорошо знакомы с виртуальными библиотеками DOM и знакомы с шаблонами. Разве не лучше использовать эти навыки, чем представить библиотеку, которая требует совершенно другого образа мышления?

Это проще, чем ты думаешь

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

// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%d-%b-%y");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
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 + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
  if (error) throw error;
// format the data
  data.forEach(function(d) {
      d.date = parseTime(d.date);
      d.close = +d.close;
  });
// Scale the range of the data
  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
  svg.append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", valueline);
// Add the X Axis
  svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));
// Add the Y Axis
  svg.append("g")
      .call(d3.axisLeft(y));
});

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

Вот как я бы сделал что-то подобное с помощью 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 (хотя может быть что угодно, React, Vue, Angular и т. Д.), И современные инструменты CSS, такие как flexbox .

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

Не забываем о размере пачки

В зависимости от типа диаграммы и того, насколько встряхивание дерева может оптимизировать ваш пакет, D3 в худшем случае будет импортировать 70+ КБ кода. Это может повлиять на загрузку вашего сайта. Итак, хотя это правда, что вы пишете больше кода, чем исходный пример D3, в целом вы распределяете меньше кода, чем если бы вы использовали 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 и border-radius для создания любых форм. Если вы хотите создать столбчатую диаграмму в D3 с двумя закругленными углами, вы не можете использовать rect, потому что он закругляет все четыре угла вместо двух углов, которые вы хотите округлить. Ваш единственный вариант - использовать path.

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

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

Но Canvas не масштабируется, как SVG? [Обновить]

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

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

Обычно ваши CSS-пиксели имеют тот же размер, что и пространство для рисования холста, но при увеличении / уменьшении масштаба в браузере вы снова увидите ту же проблему. Решение состоит в том, чтобы использовать window.devicePixelRatio и масштабировать пространство для рисования на холсте.

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

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

Можете ли вы сказать, какие из них - холст, а какие - SVG?

Заключение

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

У менеджеров по продуктам нет причин беспокоиться о том, что они не используют D3, и вам тоже не о чем беспокоиться.

Спасибо за прочтение!

Обновления

  • [17 декабря 2018] В статью добавлено: «Но Canvas не масштабируется, как SVG?» раздел.