D3.js это не больно...

Спонсор данного "доклада"

Знакомство

Software Engineer в

iTechArt Group (по состоянию на апрель 2021).

 

Профессиональный формошлёп.

 

JavaScript-разработчик со стажем более 39.4 тыщ часов или 1642 дней и (поверьте мне       ).

Это Я (формошлеп        ).

Биография

Контакты для фидбэка

Телеграм ("нежелателен" на территории РБ в виду своей деструктивной деятельности) поэтому смотри ниже cпособы связи.

Рабочая почта:
yahor.vaziyanau@itechart-group.com.

P.S. Если как-то найдете в телеграме, то кодовое слово - "я дивергент".

Слэк ЖэЭс Клана: Yahor Vaziyanau.

Наш ту ду список

  • Введение (небольшая история моей борьбы).
  • Узнаем что из себя представляет D3.js.
  • Поговорим почему D3.js не любят.
  • Примеры того что можно сделать с помощью D3.js.
  • Гайд для построения почти любого "адекватного графика".
  • Как использовать D3.js внутри фреймворков.
  • Как еще можно использовать D3.js.
  • И Т О Г И.

Обзор ту ду

ВНИМАНИЕ!!!

Дальше пойдет "сухая" теория и монолог

А всем хейтерам и тем кому не понравиться доклад

Но сначала, немного истории ...

Average Chart.js fan

Average D3.js enjoyer

О том как стал D3.js энжоером

Что ты такое, D3?

СЭНДВИЧ БИБЛИОТЕКА

Approved by

tldr; Это библиотека для визуализации данных;

D3 = Data Driven Documents

D3.js API

D3.js представляет из себя систему независимых модулей (около 30) которые были спроектированы чтобы работать "вместе" как трансформеры, т.е. любой график "собирается" на основе определенных модулей.

D3.js поддерживаемые среды

D3.js (последняя версия на момент апреля 2021 - v6.7.0) поддерживает все современные версии браузеров таких как:

 

  • Chrome;
  • Firefox;
  • Safari;
  • Edge;
  • IE9+ => D3.js v4;
  • Node.js + jsdom.

 

Также стоит проверять поддержку для каждого модуля D3.js отдельно, к примеру модуль d3-selection в мажорных версиях D3.js имеет разную имплементацию (Selectors API Level 1).

npm i d3

или yarn add d3

/* USAGE */

// Typical ES6 modules

import * as d3 from "d3";
import * as d3GeoProjection from "d3-geo-projection";

// OR (support "TreeShaking")

import {select, selectAll} from "d3-selection";
import {geoPath} from "d3-geo";
import {geoPatterson} from "d3-geo-projection";

// Node.js
var d3 = require("d3"),
    jsdom = require("jsdom");

var document = jsdom.jsdom(),
    svg = d3.select(document.body).append("svg");

Почему D3.js хейтят и так не любят?

А если подробнее?

P.S. Эта статья всегда в топе выдачи почти-что на любой запрос связанный c D3.js.

  1. "Обманчивая" кривая вхождения.
  2. Достаточно много уже готовых решений, которые "проще".
  3. Создание сложных графиков или визуализаций.
  4. Библиотека создает впечатление "гигантизма".
  5. Достаточное специфичность использования (редкость).
  6. Псевдо "непопулярность".
  7. ... и постоянный вопрос - "С чего начать-то???".

"Обманчивая" кривая вхождения

ВЫ ЗДЕСЬ

Достаточно много уже готовых решений, которые "проще"

Создание сложных графиков или визуализаций

Библиотека создает впечатление "гигантизма"

Достаточное специфичность использования (редкость)

СКОЛЬКО ТЫ ЗАРАБАТЫВАЕШЬ РАЗ ИСПОЛЬЗОВАЛ D3.js?

Псевдо "непопулярность"

Постоянный вопрос - "С чего начать-то???"

Как построить график (DIY) ?

Шаг 1. Найти правильное название графика

Шаг 2. Загуглить

Шаг 3. Скопировать + вставить

<!DOCTYPE html>
<meta charset="utf-8">

<style type="text/css">
/* 13. Basic Styling with CSS */

/* Style the lines by removing the fill and applying a stroke */
.line {
    fill: none;
    stroke: #ffab00;
    stroke-width: 3;
}
  
.overlay {
  fill: none;
  pointer-events: all;
}

/* Style the dots by assigning a fill and stroke */
.dot {
    fill: #ffab00;
    stroke: #fff;
}
  
  .focus circle {
  fill: none;
  stroke: steelblue;
}

</style>
<!-- Body tag is where we will append our SVG and SVG objects-->
<body>
</body>

<!-- Load in the d3 library -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>

// 2. Use the margin convention practice 
var margin = {top: 50, right: 50, bottom: 50, left: 50}
  , width = window.innerWidth - margin.left - margin.right // Use the window's width 
  , height = window.innerHeight - margin.top - margin.bottom; // Use the window's height

// The number of datapoints
var n = 21;

// 5. X scale will use the index of our data
var xScale = d3.scaleLinear()
    .domain([0, n-1]) // input
    .range([0, width]); // output

// 6. Y scale will use the randomly generate number 
var yScale = d3.scaleLinear()
    .domain([0, 1]) // input 
    .range([height, 0]); // output 

// 7. d3's line generator
var line = d3.line()
    .x(function(d, i) { return xScale(i); }) // set the x values for the line generator
    .y(function(d) { return yScale(d.y); }) // set the y values for the line generator 
    .curve(d3.curveMonotoneX) // apply smoothing to the line

// 8. An array of objects of length N. Each object has key -> value pair, the key being "y" and the value is a random number
var dataset = d3.range(n).map(function(d) { return {"y": d3.randomUniform(1)() } })

// 1. Add the SVG to the page and employ #2
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 + ")");

// 3. Call the x axis in a group tag
svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom

// 4. Call the y axis in a group tag
svg.append("g")
    .attr("class", "y axis")
    .call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft

// 9. Append the path, bind the data, and call the line generator 
svg.append("path")
    .datum(dataset) // 10. Binds data to the line 
    .attr("class", "line") // Assign a class for styling 
    .attr("d", line); // 11. Calls the line generator 

// 12. Appends a circle for each datapoint 
svg.selectAll(".dot")
    .data(dataset)
  .enter().append("circle") // Uses the enter().append() method
    .attr("class", "dot") // Assign a class for styling
    .attr("cx", function(d, i) { return xScale(i) })
    .attr("cy", function(d) { return yScale(d.y) })
    .attr("r", 5)
      .on("mouseover", function(a, b, c) { 
  			console.log(a) 
        this.attr('class', 'focus')
		})
      .on("mouseout", function() {  })
//       .on("mousemove", mousemove);

//   var focus = svg.append("g")
//       .attr("class", "focus")
//       .style("display", "none");

//   focus.append("circle")
//       .attr("r", 4.5);

//   focus.append("text")
//       .attr("x", 9)
//       .attr("dy", ".35em");

//   svg.append("rect")
//       .attr("class", "overlay")
//       .attr("width", width)
//       .attr("height", height)
//       .on("mouseover", function() { focus.style("display", null); })
//       .on("mouseout", function() { focus.style("display", "none"); })
//       .on("mousemove", mousemove);
  
//   function mousemove() {
//     var x0 = x.invert(d3.mouse(this)[0]),
//         i = bisectDate(data, x0, 1),
//         d0 = data[i - 1],
//         d1 = data[i],
//         d = x0 - d0.date > d1.date - x0 ? d1 : d0;
//     focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
//     focus.select("text").text(d);
//   }
</script>

А если серьезно?

Разберем пару примеров

Интеграция с фреимворками

Angular

import { Component, OnInit, ViewChild } from '@angular/core';

@Component({
  selector: 'component',
  template: `<div #chartContainer></div>`,
  styleUrls: ['./name.component.scss']
})
export class NameComponent implements OnInit {
  @ViewChild('chartContainer', { static: false }) chartContainer: ElementRef

  constructor() { }

  ngOnInit(): void {
    if (this.chartContainer.nativeElement) {
      // D3 is going BRRRRRRRRRRRRRRRRRRRRR!!!
    }
  }
}
  1. Создать ссылку на шаблон.
  2. Проверить есть ли доступ к DOM элементу этого шаблона.
  3. Использовать D3.js.

* Angular используется только для получения DOM элементов.

React

function App {
  const divRef = React.useRef()
  useEffect(()=>{ d3.init(divRef.current); return d3.destroy(divRef.current) }, [])
  return (
    <div ref={divRef}></div>
  )
}
  1. Создать ссылку на шаблон.
  2. Проверить есть ли доступ к DOM элементу этого шаблона.
  3. Использовать D3.js.

* React используется только для получения DOM элементов.

Другие фреимворки

Суть остается такой же - получение ссылок на DOM элементы с помощью фреимворков, а дальше использовать непосредственно саму библиотеку.

 

Так же существуют уже готовые различные "обертки-библиотеки" над D3.js для фреимворков, как пример - react-d3-library.

 

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

D3.js это не только боль и графики

  1. Использовать для работы с датами/временем.
  2. Использовать для работы с форматированием текста.
  3. Использовать для работы с данными во время их "анализа".
  4. Использовать как "Lodash".
  5. Использовать для работы с DOM + SVG (аля jQuery).
  6. Использовать для работы с цветовой палитрой ([domain] область и [range] диапазон).
  7. Использовать для работы с локализацией приложений, как пример работа с валютой.
  8. Импорт данных из csv для графиков и не только.
  9. ... и многое, многое другое.

Использовать для работы с датами/временем

// For ES6 modules
import { timeFormat as d3TimeFormat } from 'd3-time-format'

// Formatter
var formatTime = d3.timeFormat("%B %d, %Y");
formatTime(new Date); // "June 30, 2015"

// Parser
var parseTime = d3.timeParse("%B %d, %Y");
parseTime("June 30, 2015"); // Tue Jun 30 2015 00:00:00 GMT-0700 (PDT)

var expense = {"name":"jim","amount":34,"date":"11/12/2015"};
var parser = d3.timeParse("%m/%d/%Y");

expense.date = parser(expense.date);
console.log(expense);
// => {name: "jim", amount: 34, date: Thu Nov 12 2015 00:00:00 GMT-0500 (EST)}

var date = d3.timeParse("%A, %B %-d, %Y")("Wednesday, November 12, 2014");
console.log(date);
// => Wed Nov 12 2014 00:00:00 GMT-0500 (EST)

time = d3.timeParse("%m/%d/%Y %H:%M:%S %p")("1/2/2014 8:22:05 AM");
console.log(time);
// => Thu Jan 02 2014 08:22:05 GMT-0500 (EST)

var hourParser = d3.timeParse("%I:%M%p");
var time = hourParser("10:34pm");
var hour = d3.timeHour.round(time);
console.log(hour);
// => Mon Jan 01 1900 23:00:00 GMT-0500

var hourFormater = d3.timeFormat("%I:%M%p")
console.log(hourFormater(hour));
// => 11:00PM

Использовать для работы с форматированием текста

// ES6 modules
import { format as d3Format } from 'd3-format'

d3.format(".0%")(0.123);  // rounded percentage, "12%"
d3.format("($.2f")(-3.5); // localized fixed-point currency, "(£3.50)"
d3.format("+20")(42);     // space-filled and signed, "                 +42"
d3.format(".^20")(42);    // dot-filled and centered, ".........42........."
d3.format(".2s")(42e6);   // SI-prefix with two significant digits, "42M"
d3.format("#x")(48879);   // prefixed lowercase hexadecimal, "0xbeef"
d3.format(",.2r")(4223);  // grouped thousands with two significant digits, "4,200"

d3.format("s")(1500);  // "1.50000k"
d3.format("~s")(1500); // "1.5k"

d3.format(".2")(42);  // "42"
d3.format(".2")(4.2); // "4.2"
d3.format(".1")(42);  // "4e+1"
d3.format(".1")(4.2); // "4"

// We can get our own formatters (new FormatSpecifier) => new FormatSpecifier({type: "s"}) and we can cusomize them...
FormatSpecifier {
  "fill": " ",
  "align": ">",
  "sign": "-",
  "symbol": "",
  "zero": false,
  "width": undefined,
  "comma": false,
  "precision": undefined,
  "trim": false,
  "type": "s"
}

Использовать для работы с данными во время их "анализа"

// CSV data
name,amount,date
jim,34.0,11/12/2015
carl,120.11,11/12/2015
jim,45.0,12/01/2015
stacy,12.00,01/04/2016
stacy,34.10,01/04/2016
stacy,44.80,01/05/2016

// After parsing
var expenses = [
  {"name":"jim","amount":34,"date":"11/12/2015"},
  {"name":"carl","amount":120.11,"date":"11/12/2015"},
  {"name":"jim","amount":45,"date":"12/01/2015"},
  {"name":"stacy","amount":12.00,"date":"01/04/2016"},
  {"name":"stacy","amount":34.10,"date":"01/04/2016"},
  {"name":"stacy","amount":44.80,"date":"01/05/2016"}
];

var expensesByName = d3.nest()
  .key(function(d) { return d.name; })
  .entries(expenses);

// Result
expensesByName = [
  {"key":"jim","values":[
    {"name":"jim","amount":34,"date":"11/12/2015"},
    {"name":"jim","amount":45,"date":"12/01/2015"}
  ]},
  {"key":"carl","values":[
    {"name":"carl","amount":120.11,"date":"11/12/2015"}
  ]},
  {"key":"stacy","values":[
    {"name":"stacy","amount":12.00,"date":"01/04/2016"},
    {"name":"stacy","amount":34.10,"date":"01/04/2016"},
    {"name":"stacy","amount":44.80,"date":"01/05/2016"}
  ]}
];

// To sum up
var expensesCount = d3.nest()
  .key(function(d) { return d.name; })
  .rollup(function(v) { return v.length; })
  .entries(expenses);
console.log(JSON.stringify(expensesCount));
// => [{"key":"jim","values":2},{"key":"carl","values":1},{"key":"stacy","values":3}]

// Multi-level nesting
var expensesTotalByDay = d3.nest()
  .key(function(d) { return d.name; })
  .key(function(d) { return d.date; })
  .rollup(function(v) { return d3.sum(v, function(d) { return d.amount; }); })
  .object(expenses);
console.log(JSON.stringify(expensesTotalByDay));
// => {"jim":{"11/12/2015":34,"12/01/2015":45},
//  "carl":{"11/12/2015":120.11},
//  "stacy":{"01/04/2016":46.1,"01/05/2016":44.8}}

Использовать как "Lodash"

var map = d3.map([{name: "foo"}, {name: "bar"}], function(d) { return d.name; });
map.get("foo"); // {"name": "foo"}
map.get("bar"); // {"name": "bar"}
map.get("baz"); // undefined


var map = d3.map()
    .set("foo", 1)
    .set("bar", 2)
    .set("baz", 3);

map.get("foo"); // 1


d3.set(["foo", "bar", "foo", "baz"]).values(); // "foo", "bar", "baz"

var a = [0, 10, 30];
d3.quantile(a, 0); // 0
d3.quantile(a, 0.5); // 10
d3.quantile(a, 1); // 30
d3.quantile(a, 0.25); // 5
d3.quantile(a, 0.75); // 20
d3.quantile(a, 0.1); // 2

const array = [{foo: 42}, {foo: 91}];
d3.least(array, (a, b) => a.foo - b.foo); // {foo: 42}
d3.least(array, (a, b) => b.foo - a.foo); // {foo: 91}
d3.least(array, a => a.foo); // {foo: 42}

Использовать для работы с DOM + SVG (аля jQuery)

d3.selectAll("p")
    .attr("class", "graf")
    .style("color", "red");

// Same as 
const p = d3.selectAll("p");
p.attr("class", "graf");
p.style("color", "red");

d3.select("body")
  .append("svg")
    .attr("width", 960)
    .attr("height", 500)
  .append("g")
    .attr("transform", "translate(20,20)")
  .append("rect")
    .attr("width", 920)
    .attr("height", 460);

d3.selectAll("p").on("click", function(event) {
  d3.select(this).style("color", "red");
});

d3.selectAll("div").append(() => document.createElement("p"));

selection.select(function() {
  return this.parentNode.insertBefore(this.cloneNode(deep), this.nextSibling);
});

d3.create("svg") // equivalent to svg:svg
d3.create("svg:svg") // more explicitly
d3.create("svg:g") // an SVG G element
d3.create("g") // an HTML G (unknown) element

const div = d3.select("body")
  .selectAll("div")
  .data([4, 8, 15, 16, 23, 42])
  .enter().append("div")
    .text(d => d);

<div>4</div>
<div>8</div>
<div>15</div>
<div>16</div>
<div>23</div>
<div>42</div>

Использовать для работы с цветовой палитрой ([domain] область и [range] диапазон)

var data = d3.range(50);

var colors = d3.scaleQuantize()
    .domain([0,50])
    .range(["#5E4FA2", "#3288BD", "#66C2A5", "#ABDDA4", "#E6F598", 
    "#FFFFBF", "#FEE08B", "#FDAE61", "#F46D43", "#D53E4F", "#9E0142"]);

colors(1)


var c = d3.color("steelblue"); // {r: 70, g: 130, b: 180, opacity: 1}
var c = d3.hsl("steelblue"); // {h: 207.27…, s: 0.44, l: 0.4902…, opacity: 1}

... или даже так

var svg = d3.select("#divContinuous").append("svg").attr("width", 1000).attr("height",400)

// Create data
var data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

// Option 1: give 2 color names
var myColor = d3.scaleLinear().domain([1,10])
  .range(["white", "blue"])
svg.selectAll(".firstrow").data(data).enter().append("circle").attr("cx", function(d,i){return 30 + i*60}).attr("cy", 50).attr("r", 19).attr("fill", function(d){return myColor(d) })

// Option 2: Color brewer.
// Include <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> in your code!
var myColor = d3.scaleSequential().domain([1,10])
  .interpolator(d3.interpolatePuRd);
svg.selectAll(".secondrow").data(data).enter().append("circle").attr("cx", function(d,i){return 30 + i*60}).attr("cy", 150).attr("r", 19).attr("fill", function(d){return myColor(d) })

// Option 3: Viridis.
// Include <script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> in your code!
var myColor = d3.scaleSequential().domain([1,10])
  .interpolator(d3.interpolateViridis);
svg.selectAll(".secondrow").data(data).enter().append("circle").attr("cx", function(d,i){return 30 + i*60}).attr("cy", 250).attr("r", 19).attr("fill", function(d){return myColor(d) })

Использовать для работы с локализацией приложений, как пример работа с валютой

// Example of formatting function for Euro locale
var NL = d3.locale ({
  "decimal": ".",
  "thousands": ",",
  "grouping": [3],
  "currency": ["", "€"],
  "dateTime": "%a %b %e %X %Y",
  "date": "%m/%d/%Y",
  "time": "%H:%M:%S",
  "periods": ["AM", "PM"],
  "days": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
  "shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
  "months": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
  "shortMonths": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
})

var eur = NL.numberFormat("$,.2f");

console.log(eur(3))
// => "3.00€"

// For german days and month
d3.timeFormatDefaultLocale({
  "decimal": ",",
  "thousands": ".",
  "grouping": [3],
  "currency": ["€", ""],
  "dateTime": "%a %b %e %X %Y",
  "date": "%d.%m.%Y",
  "time": "%H:%M:%S",
  "periods": ["AM", "PM"],
  "days": ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
  "shortDays": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
  "months": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
  "shortMonths": ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]
})

Импорт данных из csv для графиков и не только

d3.csvParse("foo,bar\n1,2"); // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]
d3.tsvParse("foo\tbar\n1\t2"); // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]

d3.csvFormat([{foo: "1", bar: "2"}]); // "foo,bar\n1,2"
d3.tsvFormat([{foo: "1", bar: "2"}]); // "foo\tbar\n1\t2"

var psv = d3.dsvFormat("|");

console.log(psv.parse("foo|bar\n1|2")); // [{foo: "1", bar: "2"}, columns: ["foo", "bar"]]

// CSV file
cities.csv:

city,state,population,land area
seattle,WA,652405,83.9
new york,NY,8405837,302.6
boston,MA,645966,48.3
kansas city,MO,467007,315.0

d3.csv("/data/cities.csv").then(function(data) {
  console.log(data[0]);
});
// => {city: "seattle", state: "WA", population: "652405", land area: "83.9"}

d3.csv("/data/cities.csv").then(function(data) {
  data.forEach(function(d) {
    d.population = +d.population;
    d["land area"] = +d["land area"];
  });
  console.log(data[0]);
});

// => {city: "seattle", state: "WA", population: 652405, land area: 83.9}

d3.csv("/data/cities.csv", function(d) {
  return {
    city : d.city,
    state : d.state,
    population : +d.population,
    land_area : +d["land area"]
  };
}).then(function(data) {
  console.log(data[0]);
});
// => {city: "seattle", state: "WA", population: 652405, land_area: 83.9}


// JSON file
[
 {"name":"Andy Hunt",
  "title":"Big Boss",
  "age": 68,
  "bonus": true
 },
 {"name":"Charles Mack",
  "title":"Jr Dev",
  "age":24,
  "bonus": false
 }
]

d3.json("/data/employees.json").then(function(data) {
  console.log(data[0]);
});
// => {name: "Andy Hunt", title: "Big Boss", age: 68, bonus: true}

и многое, многое другое...

Подведем И Т О Г И

D3.js

Любая другая библиотека

Меньше 1000 слов ...

Меньше 1000 слов ...

I have the high ground !!!

Меньше 1000 слов ...

Теперь вы все стали энджоерами

Average [name-Chart].js fan

Average D3.js enjoyer

Полезные ссылки

Кем я себя чувствую ...

СЛУШАТЕЛИ

Я все. Спасибо.

Вопросики и ответики?

D3.js это не больно

By Yahor Vaziyanau

D3.js это не больно

Монолог (не реклама) о том что D3.js достаточно "простая" библиотека и что ее можно и нужно использовать не только для визуализации данных.

  • 879