<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Наталия Степанова</title><subtitle>Программистка и соосновательница студии разработки https://slaylines.io.
Веду канал https://t.me/visualize_it про визуализации и не только.</subtitle><author><name>Наталия Степанова</name></author><id>https://teletype.in/atom/gnykka</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/gnykka?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/gnykka?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-10T22:57:59.263Z</updated><entry><id>gnykka:d3-treemap</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/d3-treemap?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Как сделать Treemap график на D3.js</title><published>2024-01-17T08:21:22.687Z</published><updated>2024-01-17T08:21:22.687Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/e6/e7/e6e71fc2-c87b-458a-8a69-8c1c9640e648.png"></media:thumbnail><category term="d3js" label="d3js"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/4e/e0/4ee0f22d-3cd7-4bc6-abbf-46702311a828.png&quot;&gt;Тримап — один из самых мощных и популярных инструментов для визуализации иерархических данных. В этой статье я расскажу, как сделать его с помощью D3.js.</summary><content type="html">
  &lt;p id=&quot;TjU6&quot;&gt;Сегодня я решила написать про создание графика Тримап — одного из самых мощных инструментов для визуализации иерархических данных.&lt;/p&gt;
  &lt;p id=&quot;yqKf&quot;&gt;Тримап позволяет отобразить сложные вложенные структуры в компактной и интуитивно понятной форме и идеально подходит для визуализаций, где каждый элемент является частью большего целого. Это, например, может быть визуализация долей рынка компаний в определенной отрасли, распределение расходов в бюджете или демонстрация различных видов в естественной иерархии.&lt;/p&gt;
  &lt;p id=&quot;2NPZ&quot;&gt;В этой статье я расскажу, как создать базовый Тримап с помощью D3.js и дам советы, как сделать его более информативным и визуально привлекательным.&lt;/p&gt;
  &lt;p id=&quot;fdov&quot;&gt;По традиции сразу делюсь ссылками на результат:&lt;/p&gt;
  &lt;ol id=&quot;3C8Z&quot;&gt;
    &lt;li id=&quot;o6Ga&quot;&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/vYPgpbb&quot; target=&quot;_blank&quot;&gt;Интерактивный Codepen&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;oZxx&quot;&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/treemap-chart.html&quot; target=&quot;_blank&quot;&gt;Код на Github&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;uMnk&quot;&gt;Подготовка данных&lt;/h2&gt;
  &lt;p id=&quot;TFaI&quot;&gt;Каждый отдельный элемент Тримапа — это объект со следующими параметрами:&lt;/p&gt;
  &lt;pre id=&quot;WYzX&quot; data-lang=&quot;javascript&quot;&gt;{
  name: &amp;lt;string&amp;gt;,
  value: &amp;lt;integer&amp;gt;
}&lt;/pre&gt;
  &lt;p id=&quot;1QJf&quot;&gt;Тримап — это много таких объектов, собранных в единую иерархическую структуру. Вот как может выглядеть иерархия с двумя уровнями вложенности:&lt;/p&gt;
  &lt;pre id=&quot;xXsm&quot; data-lang=&quot;javascript&quot;&gt;{
  name: &amp;quot;treemap&amp;quot;,
  children: [
    {
      name: &amp;quot;group1&amp;quot;,
      children: [
        { name: &amp;quot;element1&amp;quot;, value: 20 },
        { name: &amp;quot;element2&amp;quot;, value: 50 },
        { name: &amp;quot;element3&amp;quot;, value: 70 },
      ]
    },
    {
      name: &amp;quot;group2&amp;quot;,
      children: [
        { name: &amp;quot;element4&amp;quot;, value: 10 },
        { name: &amp;quot;element5&amp;quot;, value: 60 },
      ]
    }
  ]
}&lt;/pre&gt;
  &lt;p id=&quot;1bBz&quot;&gt;В этот раз я создам данные случайным образом. Пусть у меня будет как в примере выше два уровня вложенности: Тримап разбит на группы, группы — на элементы.&lt;/p&gt;
  &lt;p id=&quot;Hhwr&quot;&gt;Следующий код создаёт случайное количество групп (от 3 до 5) и случайное количество элементов в каждой группе (от 5 до 10). При этом значения этих элементов могут варьироваться от 20 до 100.&lt;/p&gt;
  &lt;pre id=&quot;jKJQ&quot; data-lang=&quot;javascript&quot;&gt;const data = { name: &amp;quot;treemap&amp;quot;, children: [] };
const groupsCount = Math.round(3 + Math.random() * 2);

for (let i = 0; i &amp;lt; groupsCount; i++) {
  const group = { name: &amp;#x60;group_${i + 1}&amp;#x60;, children: [] };
  const count = Math.round(5 + Math.random() * 5);

  for (let j = 0; j &amp;lt; count; j++) {
    group.children.push({
      name: &amp;#x60;element_${j + 1}&amp;#x60;,
      value: 20 + Math.random() * 80,
    });
  }

  data.children.push(group);
}&lt;/pre&gt;
  &lt;h2 id=&quot;4hDe&quot;&gt;Подготовка графика&lt;/h2&gt;
  &lt;p id=&quot;qsAB&quot;&gt;Я, как и всегда раньше, буду писать всё в одном файле. Cтруктурe этого файла, подключение библиотеки и настройки главного SVG элемента можно подсмотреть в первой статье моего цикла (&lt;a href=&quot;https://teletype.in/@gnykka/d3-line-scatter&quot; target=&quot;_blank&quot;&gt;Как сделать Line Chart&lt;/a&gt;) или в коде примера.&lt;/p&gt;
  &lt;p id=&quot;zjoq&quot;&gt;Я создала данные нужного D3 формата, но перед рисованием их ещё нужно обработать. Сделать это можно с помощью методов &lt;code&gt;d3.hierarchy&lt;/code&gt; и &lt;code&gt;d3.treemap&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;piC1&quot; data-lang=&quot;javascript&quot;&gt;const root = d3.hierarchy(data).sum((d) =&amp;gt; d.value);

d3.treemap().size([width, height]).padding(2)(root);&lt;/pre&gt;
  &lt;p id=&quot;RACG&quot;&gt;Сначала &lt;code&gt;hierarchy&lt;/code&gt; суммирует значения на каждом уровне, а потом &lt;code&gt;treemap&lt;/code&gt; вычисляет размеры и позиции всех элементов. В итоге получается большой и сложный объект как на картинке ниже:&lt;/p&gt;
  &lt;figure id=&quot;LETV&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/fa/5e/fa5e8bd9-d510-448e-aaa1-b0414f047d19.png&quot; width=&quot;1582&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;kQ95&quot;&gt;Хорошо, данные готовы. Кроме них я сделаю простую палитру, чтобы красить разные группы в разные цвета:&lt;/p&gt;
  &lt;pre id=&quot;Zxjo&quot; data-lang=&quot;javascript&quot;&gt;const color = d3.scaleOrdinal(d3.schemeCategory10); &lt;/pre&gt;
  &lt;h2 id=&quot;b94T&quot;&gt;Рисование прямоугольников и подписей&lt;/h2&gt;
  &lt;p id=&quot;V2rX&quot;&gt;Теперь можно рисовать SVG элементы. Сначала прямоугольники:&lt;/p&gt;
  &lt;pre id=&quot;KmYm&quot; data-lang=&quot;javascript&quot;&gt;svg
  .selectAll(&amp;#x27;rect&amp;#x27;)
  .data(root.leaves())
  .join(&amp;#x27;rect&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, (d) =&amp;gt; d.x0)
  .attr(&amp;#x27;y&amp;#x27;, (d) =&amp;gt; d.y0)
  .attr(&amp;#x27;width&amp;#x27;, (d) =&amp;gt; d.x1 - d.x0)
  .attr(&amp;#x27;height&amp;#x27;, (d) =&amp;gt; d.y1 - d.y0)
  .style(&amp;#x27;stroke&amp;#x27;, &amp;#x27;black&amp;#x27;)
  .style(&amp;#x27;fill&amp;#x27;, (d) =&amp;gt; color(d.parent.data.name));&lt;/pre&gt;
  &lt;p id=&quot;Mcod&quot;&gt;Это все элементарные частицы Тримапа, которые можно получить с помощью &lt;code&gt;root.leaves()&lt;/code&gt;. Их позиции и размеры уже вычислены, а цвет мы берём из палитры по имени родителя-группы.&lt;/p&gt;
  &lt;figure id=&quot;Prq7&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/0c/9b/0c9bea66-883f-4fa2-846e-09f7dd4ddbab.png&quot; width=&quot;1650&quot; /&gt;
    &lt;figcaption&gt;Получается подобное разноцветное разбиение&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Ev7Q&quot;&gt;Второй шаг — отображение подписей. И тут сейчас будут нюансы, но сначала сделаем подписи простыми текстовыми элементами:&lt;/p&gt;
  &lt;pre id=&quot;R2n0&quot; data-lang=&quot;javascript&quot;&gt;svg
  .selectAll(&amp;#x27;text&amp;#x27;)
  .data(root.leaves())
  .join(&amp;#x27;text&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, (d) =&amp;gt; d.x0 + padding)
  .attr(&amp;#x27;y&amp;#x27;, (d) =&amp;gt; d.y0 + fontSize)
  .style(&amp;#x27;font-size&amp;#x27;, &amp;#x60;${fontSize}px&amp;#x60;)
  .text((d) =&amp;gt; d.data.name);&lt;/pre&gt;
  &lt;p id=&quot;zKDc&quot;&gt;И результат чаще всего — вылезающие за пределы подписи, как на картинке.&lt;/p&gt;
  &lt;figure id=&quot;lsDN&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/ef/7e/ef7ef077-d58e-498a-aa30-86e60f82aa60.png&quot; width=&quot;1658&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;nzOk&quot;&gt;Сейчас расскажу, как можно это исправить. Основных варианта три:&lt;/p&gt;
  &lt;ol id=&quot;NUiE&quot;&gt;
    &lt;li id=&quot;TPxp&quot;&gt;Прятать вылезающую часть подписи, имитируя свойство ellipsis.&lt;/li&gt;
    &lt;li id=&quot;Jpcz&quot;&gt;Адаптировать подписи, например, менять их размер чтобы они полностью помещались.&lt;/li&gt;
    &lt;li id=&quot;0Fn0&quot;&gt;Использовать DOM элементы и сделать любую более сложную логику.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h3 id=&quot;EMP1&quot;&gt;Обрезание подписей&lt;/h3&gt;
  &lt;p id=&quot;vkKX&quot;&gt;Это самый простой способ и я обычно пользуюсь именно им. Суть в том, чтобы вычислить ширину блока, длину текста и обрезать то, что не влезло, заменив на «...». Ширина блока нам всегда известна, это &lt;code&gt;x1 - x0&lt;/code&gt;, а вот длину текста можно считать по-разному.&lt;/p&gt;
  &lt;p id=&quot;zhsK&quot;&gt;Я часто пользуюсь простым лайфхаком: среднюю ширину одной буквы можно привязать к размеру шрифта и через эту привязку вычислять длину текста как &lt;code&gt;text.length * fontSize * ratio&lt;/code&gt;. Троеточие вместо последних букв тоже, конечно, займёт место, поэтому пусть его длина это примерно один &lt;code&gt;fontSize&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;obWZ&quot; data-lang=&quot;javascript&quot;&gt;  .text((d) =&amp;gt; {
    const ratio = 0.5;
    const text = d.data.name;
    const width = d.x1 - d.x0 - padding * 2;
  
    if (width &amp;gt; text.length * ratio * fontSize) {
      return text;
    }
  
    const length = Math.max(width / (ratio * fontSize) - 1, 0);
    const result = d.data.name.substring(0, length);

    return &amp;#x60;${result}...&amp;#x60;;
  });&lt;/pre&gt;
  &lt;figure id=&quot;VOhW&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/31/b8/31b8b580-c876-4e89-b72d-314946046382.png&quot; width=&quot;1660&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;wug3&quot;&gt;Это решение простое, но не вариативное. Что делать, если размер шрифта или сам шрифт меняется? Если подписи очень длинные? А если они не помещаются по высоте?&lt;/p&gt;
  &lt;h3 id=&quot;EyiK&quot;&gt;Адаптация подписей&lt;/h3&gt;
  &lt;p id=&quot;gM6u&quot;&gt;Тогда на помощь могут прийти другие способы.&lt;/p&gt;
  &lt;p id=&quot;mNva&quot;&gt;Например, можно точно вычислять длину текста. Это делается через невидимый вспомогательный &lt;code&gt;canvas&lt;/code&gt; элемент и метод &lt;code&gt;measureText&lt;/code&gt; его контекста.&lt;/p&gt;
  &lt;p id=&quot;xe3n&quot;&gt;Усложнением этого метода может быть и изменение размера подписей. Например, мой базовый размер — 16 пикселей. Я могу пытаться вписать текст в блок, постепенно уменьшая его размер, а уже потом обрезать непоместившееся:&lt;/p&gt;
  &lt;pre id=&quot;v4jz&quot; data-lang=&quot;javascript&quot;&gt;const canvas = document.createElement(&amp;#x27;canvas&amp;#x27;);
const context = canvas.getContext(&amp;#x27;2d&amp;#x27;);

const measureText = (text, fontSize) =&amp;gt; {
  context.font = &amp;#x60;${fontSize}px serif&amp;#x60;;
  return context.measureText(text).width;
};

const calculateTextSize = (d) =&amp;gt; {
  const text = d.data.name;
  const width = d.x1 - d.x0 - padding * 2;

  let size = fontSize;
  let textWidth = measureText(text, size);
  
  while (size &amp;gt; 10 &amp;amp;&amp;amp; textWidth &amp;gt; width) {
    size--;
    textWidth = measureText(text, size);
  }

  return size;
};

/*
......
*/
  
.style(&amp;#x27;font-size&amp;#x27;, (d) =&amp;gt; &amp;#x60;${calculateTextSize(d)}px&amp;#x60;)
.text((d) =&amp;gt; {
  const text = d.data.name;
  const width = d.x1 - d.x0 - padding * 2;

  const size = calculateTextSize(d);

  if (measureText(text, size) &amp;lt;= width) return text;

  let truncatedText = text;
  textWidth = measureText(&amp;#x60;${truncatedText}...&amp;#x60;, size);

  while (textWidth &amp;gt; width &amp;amp;&amp;amp; truncatedText.length &amp;gt; 0) {
    truncatedText = truncatedText.substring(0, truncatedText.length - 1);
    textWidth = measureText(&amp;#x60;${truncatedText}...&amp;#x60;, size)
  }

  return &amp;#x60;${truncatedText}...&amp;#x60;;
});&lt;/pre&gt;
  &lt;figure id=&quot;qfLB&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/4e/e0/4ee0f22d-3cd7-4bc6-abbf-46702311a828.png&quot; width=&quot;1656&quot; /&gt;
    &lt;figcaption&gt;Пришлось обрезать гораздо меньше подписей, чем раньше.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;nW8s&quot;&gt;Использование DOM элементов&lt;/h3&gt;
  &lt;p id=&quot;bZ8U&quot;&gt;В SVG есть специальный тег, &lt;code&gt;&amp;lt;foreignObject&amp;gt;&lt;/code&gt;, который используется, если нужно вставить любой чужеродный элемент. Раньше у этого тега была не очень хорошая поддержка, но сейчас &lt;a href=&quot;https://caniuse.com/?search=foreignObject&quot; target=&quot;_blank&quot;&gt;ситуация стала сильно лучше&lt;/a&gt; и методом, который я сейчас опишу, уже вполне можно пользоваться.&lt;/p&gt;
  &lt;p id=&quot;D80k&quot;&gt;Идея в том, чтобы вместо нативных для SVG тегов &lt;code&gt;text&lt;/code&gt; использовать обычные &lt;code&gt;div&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;UL46&quot; data-lang=&quot;javascript&quot;&gt;svg
  .selectAll(&amp;#x27;foreignObject&amp;#x27;)
  .data(root.leaves())
  .join(&amp;#x27;foreignObject&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, (d) =&amp;gt; d.x0 + padding)
  .attr(&amp;#x27;y&amp;#x27;, (d) =&amp;gt; d.y0 + padding)
  .attr(&amp;#x27;width&amp;#x27;, (d) =&amp;gt; d.x1 - d.x0 - padding * 2)
  .attr(&amp;#x27;height&amp;#x27;, (d) =&amp;gt; d.y1 - d.y0 - padding * 2)
  .append(&amp;#x27;xhtml:div&amp;#x27;)
  .style(&amp;#x27;height&amp;#x27;, &amp;#x27;100%&amp;#x27;)
  .style(&amp;#x27;font-size&amp;#x27;, &amp;#x60;${fontSize}px&amp;#x60;)
  .style(&amp;#x27;overflow&amp;#x27;, &amp;#x27;hidden&amp;#x27;)
  .style(&amp;#x27;word-wrap&amp;#x27;, &amp;#x27;break-word&amp;#x27;)
  .html((d) =&amp;gt; d.data.name);&lt;/pre&gt;
  &lt;figure id=&quot;EwRz&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/bf/12/bf12ad51-7ded-4724-9fee-e8e1ac65d8b8.png&quot; width=&quot;1668&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;NHH9&quot;&gt;Используя и стилизуя DOM элементы внутри &lt;code&gt;foreignObject&lt;/code&gt;, можно сделать и многострочный текст, и ссылки, и многое другое.&lt;/p&gt;
  &lt;p id=&quot;dPRa&quot;&gt;В своём примере я оставлю метод с вариативными размерами.&lt;/p&gt;
  &lt;h2 id=&quot;xZi6&quot;&gt;Ссылки&lt;/h2&gt;
  &lt;p id=&quot;27G5&quot;&gt;Так у меня получился базовый Тримап. Его есть, куда улучшать и усложнять, но это уже тема для отдельной статьи.&lt;/p&gt;
  &lt;ol id=&quot;Qjf9&quot;&gt;
    &lt;li id=&quot;mzrk&quot;&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/vYPgpbb&quot; target=&quot;_blank&quot;&gt;Пример на Codepen&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;JqAG&quot;&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/treemap-chart.html&quot; target=&quot;_blank&quot;&gt;Код на Github&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;6oFc&quot;&gt;&lt;a href=&quot;https://gnykka.github.io/d3-tutorials/treemap-chart.html&quot; target=&quot;_blank&quot;&gt;Получившийся график&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;IgQc&quot;&gt;Вместо заключения&lt;/h2&gt;
  &lt;p id=&quot;bw2u&quot;&gt;Когда-то давно я делала небольшой эксперимент на D3, &lt;a href=&quot;https://codepen.io/gnykka/pen/mdOyepW&quot; target=&quot;_blank&quot;&gt;генерируемый Тримап в стиле Пита Мондриана&lt;/a&gt;, и заметила, что он всегда имеет частично диагональную структуру. &lt;/p&gt;
  &lt;figure id=&quot;n2XU&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/b4/8a/b48a40e4-e64f-40d1-981b-e8e102b6d4da.png&quot; width=&quot;1310&quot; /&gt;
    &lt;figcaption&gt;Например, тут все элементы имеют одинаковые значения и диагональная&lt;br /&gt;структура явно видна в правой «квадратной» части.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;YypX&quot;&gt;Мне стало интересно, почему так происходит.&lt;/p&gt;
  &lt;p id=&quot;nQSK&quot;&gt;Оказалось, что существует 15 первичных алгоритмов построения Тримапа. D3 по умолчанию использует один из них, &lt;strong&gt;Squarified&lt;/strong&gt;. Он стремится минимизировать у каждого блока отношение его длины к ширине, чтобы блоки становились более квадратными.&lt;/p&gt;
  &lt;p id=&quot;wmku&quot;&gt;Диагональная структура возникает из-за способа, которым алгоритм размещает блоки, пытаясь оптимизировать использование пространства. При добавлении каждого нового блока алгоритм оценивает, как это повлияет на общее отношение размеров всех блоков в ряду. Когда очередное добавление начинает существенно ухудшать это отношение, начинается новый ряд. При этом горизонтальные и вертикальные блоки чередуются.&lt;/p&gt;
  &lt;p id=&quot;0TCX&quot;&gt;Захотелось даже как-нибудь потом реализовать все эти основные алгоритмы и сравнить.&lt;/p&gt;

</content></entry><entry><id>gnykka:d3-sankey</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/d3-sankey?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Как сделать Sankey график на D3.js</title><published>2023-06-19T10:49:01.152Z</published><updated>2023-12-14T20:32:44.844Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/5f/28/5f28dfbb-5662-4925-bcf8-61345ded9835.png"></media:thumbnail><category term="d3js" label="d3js"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/d9/98/d998a274-8415-4b92-a220-4320e6f5afd0.png&quot;&gt;Санкей (sankey) — это график, показывающий поток данных. Чаще всего его можно встретить в визуализациях веб траффика, финансовых транзакций или потребления энергии. Это граф, который отлично отображает, как связаны между собой элементы системы и какие роли они играют.</summary><content type="html">
  &lt;p id=&quot;6LlW&quot;&gt;Санкей (sankey) — это график, показывающий поток данных. Чаще всего его можно встретить в визуализациях веб траффика, финансовых транзакций или потребления энергии. Это граф, который отлично отображает, как связаны между собой элементы системы и какие роли они играют.&lt;/p&gt;
  &lt;p id=&quot;nNJD&quot;&gt;Скорее всего вы хорошо знаете первый знаменитый санкей график — это визуализация Чарльза Минарда наступления армии Наполеона на Россию в войне 1812 года.&lt;/p&gt;
  &lt;figure id=&quot;nbWu&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/d9/98/d998a274-8415-4b92-a220-4320e6f5afd0.png&quot; width=&quot;2003&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;6PJ2&quot;&gt;Тогда у такого графика ещё не было своего названия. Оно появилось только в 1898 году в честь ирландского капитана Мэттью Санкея, который нарисовал схему работы парового двигателя.&lt;/p&gt;
  &lt;figure id=&quot;RnEn&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/68/91/68914e71-cc22-4d10-bd37-50aca0f7eed5.png&quot; width=&quot;640&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;HyHs&quot;&gt;В этой статье я собираюсь рассказать, как сделать санкей график с помощью D3.js и объяснить, как именно он работает. Чтобы пропустить все объяснения и сразу посмотреть код, кликайте на вот эти ссылки:&lt;/p&gt;
  &lt;ol id=&quot;xWen&quot;&gt;
    &lt;li id=&quot;X6ox&quot;&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/RwedjBb&quot; target=&quot;_blank&quot;&gt;Интерактивный Codepen&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;vgzg&quot;&gt;&lt;a href=&quot;https://gnykka.github.io/d3-tutorials/sankey-chart.html&quot; target=&quot;_blank&quot;&gt;Код на Github&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;Avlm&quot;&gt;Подготовка данных&lt;/h2&gt;
  &lt;p id=&quot;66AI&quot;&gt;В предыдущих туториалах я генерировала случайные данные. В этот раз я решила найти что-то не очень объёмное, но вполне реальное. После небольших поисков в открытых источниках я наткнулась на &lt;a href=&quot;https://www.eia.gov/totalenergy/data/flow-graphs/electricity.php&quot; target=&quot;_blank&quot;&gt;визуализацию&lt;/a&gt; производства и потребления электроэнергии в США в 2021 году.&lt;/p&gt;
  &lt;p id=&quot;PyxQ&quot;&gt;Скопировав значения, я собрала &lt;a href=&quot;https://gnykka.github.io/d3-tutorials/sankey-data.json&quot; target=&quot;_blank&quot;&gt;такой файл&lt;/a&gt;. В нём данные уже приведены к нужному D3 формату. Санкей показывает связи между элементами системы, поэтому в данных есть две части: &lt;code&gt;nodes&lt;/code&gt; — сами элементы и &lt;code&gt;links&lt;/code&gt; — связи этих элементов. У каждого элемента есть его номер (id) и название (name). У каждой связи есть сила (value) и ссылки на соответствующие ей элементы (target и source указывают на id).&lt;/p&gt;
  &lt;pre id=&quot;XFk8&quot;&gt;{
  &amp;quot;nodes&amp;quot;: [
    { &amp;quot;id&amp;quot;: 0, &amp;quot;name&amp;quot;: &amp;quot;coal&amp;quot; },
    { &amp;quot;id&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;natural gas&amp;quot; },
    ...
    { &amp;quot;id&amp;quot;: 8, &amp;quot;name&amp;quot;: &amp;quot;fossil fuels&amp;quot; },
    { &amp;quot;id&amp;quot;: 9, &amp;quot;name&amp;quot;: &amp;quot;energy consumed to generate electricity&amp;quot; },
    ... 
  ],
  &amp;quot;links&amp;quot;: [
    { &amp;quot;source&amp;quot;: 0, &amp;quot;target&amp;quot;: 8, &amp;quot;value&amp;quot;: 9.48 },
    { &amp;quot;source&amp;quot;: 1, &amp;quot;target&amp;quot;: 8, &amp;quot;value&amp;quot;: 11.94 },
    ...
    { &amp;quot;source&amp;quot;: 8, &amp;quot;target&amp;quot;: 9, &amp;quot;value&amp;quot;: 21.69 },
    ...
  ]
}&lt;/pre&gt;
  &lt;h2 id=&quot;c2tj&quot;&gt;Подготовка графика&lt;/h2&gt;
  &lt;p id=&quot;IhtI&quot;&gt;Я не буду описывать, как я подготавливаю страницу и SVG элементы, потому что я всегда делаю это одинаково и это можно найти в старой статье &lt;a href=&quot;https://teletype.in/@gnykka/d3-line-scatter&quot; target=&quot;_blank&quot;&gt;«Как сделать LineChart»&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;Uqu1&quot;&gt;Единственное отличие от всех предыдущих туториалов — дополнительный &lt;a href=&quot;https://unpkg.com/d3-sankey@0.12.3/dist/d3-sankey.js&quot; target=&quot;_blank&quot;&gt;скрипт&lt;/a&gt; с кодом для создания санкей графика, который я как и основной скрипт D3 вставляю в тег &lt;code&gt;head&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;V8qj&quot;&gt;Перейду сразу к основной части:&lt;/p&gt;
  &lt;pre id=&quot;EECl&quot;&gt;const sankey = d3.sankey()
  .nodeWidth(20)
  .nodePadding(20)
  .size([width, height]);&lt;/pre&gt;
  &lt;p id=&quot;pNEH&quot;&gt;Функция &lt;code&gt;d3.sankey&lt;/code&gt; создаёт и возвращает мне сложный метод, который будет отвечать за построение графа. Ему заранее, ещё до всех вычислений, нужно передать параметры. Здесь это ширина каждого элемента, расстояние между соседями и размер графика. Это не все возможные настройки, например, можно изменить функцию сортировки связей и вершин или тип выравнивания элементов.&lt;/p&gt;
  &lt;p id=&quot;cdWM&quot;&gt;Чтобы вычислить граф и положение его вершин и связей, я передаю в этот метод данные, прочитанные из json файла:&lt;/p&gt;
  &lt;pre id=&quot;6QI5&quot;&gt;d3.json(filename).then(data =&amp;gt; {
  const graph = sankey(data);
  ...
});&lt;/pre&gt;
  &lt;p id=&quot;5g38&quot;&gt;Здесь и происходит главная магия. Если распечатать вернувшийся объект &lt;code&gt;graph&lt;/code&gt;, то мы увидим все элементы с вычисленными координатами и размерами:&lt;/p&gt;
  &lt;figure id=&quot;YXDl&quot; class=&quot;m_retina&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a0/28/a028b3c0-8cb2-4dea-8869-3e4ba3015411.png&quot; width=&quot;731&quot; /&gt;
    &lt;figcaption&gt;Так выглядят вычисленные связи&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure id=&quot;G196&quot; class=&quot;m_retina&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f2/75/f275a961-f568-40be-b2a0-3a4ada5449c5.png&quot; width=&quot;664&quot; /&gt;
    &lt;figcaption&gt;А так — вычисленные вершины&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;6m5Y&quot;&gt;Рисование связей&lt;/h2&gt;
  &lt;p id=&quot;bZxH&quot;&gt;Теперь у меня есть все значения, и я могу рисовать элементы и их связи. Начну со связей:&lt;/p&gt;
  &lt;pre id=&quot;pnz9&quot;&gt;const link = svg.append(&amp;#x27;g&amp;#x27;)
  .selectAll(&amp;#x27;.link&amp;#x27;)
  .data(graph.links)
  .enter()
  .append(&amp;#x27;g&amp;#x27;)
  .attr(&amp;#x27;class&amp;#x27;, &amp;#x27;link&amp;#x27;);&lt;/pre&gt;
  &lt;p id=&quot;Fnhk&quot;&gt;Можно было бы просто покрасить их в серый или в цвет, соответствующий одной из вершин, но я видела красивый пример с градиентами и решила сделать так же. Градиенты в SVG делаются непросто — в каждой группе нужно создать свой &lt;code&gt;linearGradient&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;7cSA&quot;&gt;  link.append(&amp;#x27;linearGradient&amp;#x27;)
    .attr(&amp;#x27;id&amp;#x27;, d =&amp;gt; &amp;#x60;link-${d.index}&amp;#x60;)
    .attr(&amp;#x27;gradientUnits&amp;#x27;, &amp;#x27;userSpaceOnUse&amp;#x27;)
    .attr(&amp;#x27;x1&amp;#x27;, d =&amp;gt; d.source.x1)
    .attr(&amp;#x27;x2&amp;#x27;, d =&amp;gt; d.target.x0)
    .call(gradient =&amp;gt; gradient.append(&amp;#x27;stop&amp;#x27;)
      .attr(&amp;#x27;offset&amp;#x27;, &amp;#x27;0%&amp;#x27;)
      .attr(&amp;#x27;stop-color&amp;#x27;, ({ source }) =&amp;gt; colorScale(source.name)))
    .call(gradient =&amp;gt; gradient.append(&amp;#x27;stop&amp;#x27;)
      .attr(&amp;#x27;offset&amp;#x27;, &amp;#x27;100%&amp;#x27;)
      .attr(&amp;#x27;stop-color&amp;#x27;, ({ target }) =&amp;gt; colorScale(target.name)));&lt;/pre&gt;
  &lt;p id=&quot;lepF&quot;&gt;Не буду подробно рассказывать, что тут что, про градиенты в SVG можно подробно прочитать, например, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Gradients&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt;. Мне нужны линейные градиенты с двумя цветами, соответствующими вершинам. Для вычисления этих цветов я заранее создала самую простую цветовую шкалу:&lt;/p&gt;
  &lt;pre id=&quot;05YB&quot;&gt;const colorScale = d3.scaleOrdinal(d3.schemeTableau10);&lt;/pre&gt;
  &lt;p id=&quot;3aoB&quot;&gt;Завершаю рисование связей добавлением элементов &lt;code&gt;path&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;SyY1&quot;&gt;link.append(&amp;#x27;path&amp;#x27;)
  .attr(&amp;#x27;d&amp;#x27;, d3.sankeyLinkHorizontal())
  .attr(&amp;#x27;stroke&amp;#x27;, ({ index: i }) =&amp;gt; &amp;#x60;url(#link-${i})&amp;#x60;)
  .attr(&amp;#x27;stroke-width&amp;#x27;, ({ width }) =&amp;gt; Math.max(1, width));&lt;/pre&gt;
  &lt;p id=&quot;cQZm&quot;&gt;Их цвет — созданный ранее градиент, размер — не меньше 1 пикселя, а геометрию я получаю из метода &lt;code&gt;d3.sankeyLinkHorizontal&lt;/code&gt; всё того же скрипта &lt;code&gt;d3-sankey.js&lt;/code&gt;.&lt;/p&gt;
  &lt;figure id=&quot;ulc5&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/b1/55/b1559d46-deeb-4319-977d-b18145b5303b.png&quot; width=&quot;1678&quot; /&gt;
    &lt;figcaption&gt;Получаю вот такие разноцветные линии с дырками между ними&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;7YwO&quot;&gt;Рисование вершин и подписей&lt;/h2&gt;
  &lt;p id=&quot;jCl8&quot;&gt;Осталось нарисовать и подписать вершины графа. Как и со связями добавляю группы и привязываю данные:&lt;/p&gt;
  &lt;pre id=&quot;c1Sv&quot;&gt;const node = svg.append(&amp;#x27;g&amp;#x27;)
  .selectAll(&amp;#x27;.node&amp;#x27;)
  .data(graph.nodes)
  .enter()
  .append(&amp;#x27;g&amp;#x27;)
  .attr(&amp;#x27;class&amp;#x27;, &amp;#x27;node&amp;#x27;);&lt;/pre&gt;
  &lt;p id=&quot;wCrI&quot;&gt;В каждой группе рисую прямоугольник:&lt;/p&gt;
  &lt;pre id=&quot;33Yl&quot;&gt;node.append(&amp;#x27;rect&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, d =&amp;gt; d.x0)
  .attr(&amp;#x27;y&amp;#x27;, d =&amp;gt; d.y0)
  .attr(&amp;#x27;height&amp;#x27;, d =&amp;gt; d.y1 - d.y0)
  .attr(&amp;#x27;width&amp;#x27;, d =&amp;gt; d.x1 - d.x0)
  .style(&amp;#x27;fill&amp;#x27;, d =&amp;gt; colorScale(d.name));&lt;/pre&gt;
  &lt;p id=&quot;4zqt&quot;&gt;Его размеры беру из привязанных вычисленных данных, а цвет — из цветовой шкалы.&lt;/p&gt;
  &lt;p id=&quot;Mhme&quot;&gt;Почти всё, осталось только подписать вершины. Я сделаю это с помощью элементов &lt;code&gt;text&lt;/code&gt; (отступы и параметры шрифта я выбрала на глаз). Подписи слева от центра выровнены по правому краю соответствующего элемента, а подписи справа — по левому:&lt;/p&gt;
  &lt;pre id=&quot;zLrk&quot;&gt;const text = node.append(&amp;#x27;text&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, d =&amp;gt; d.x0 - 3)
  .attr(&amp;#x27;y&amp;#x27;, d =&amp;gt; (d.y1 + d.y0) / 2)
  .attr(&amp;#x27;dy&amp;#x27;, &amp;#x27;0.35em&amp;#x27;)
  .attr(&amp;#x27;text-anchor&amp;#x27;, &amp;#x27;end&amp;#x27;)
  .text(d =&amp;gt; d.name)
  .filter(d =&amp;gt; d.x0 &amp;lt; width / 2)
  .attr(&amp;#x27;x&amp;#x27;, d =&amp;gt; d.x1 + 3)
  .attr(&amp;#x27;text-anchor&amp;#x27;, &amp;#x27;start&amp;#x27;);&lt;/pre&gt;
  &lt;p id=&quot;v4rj&quot;&gt;И это всё, получился вот такой график:&lt;/p&gt;
  &lt;figure id=&quot;QsI7&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/df/73/df73f6f9-0335-43ad-84a6-31fafb3a1556.png&quot; width=&quot;1688&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;l49H&quot;&gt;Ссылки&lt;/h2&gt;
  &lt;ol id=&quot;2jRX&quot;&gt;
    &lt;li id=&quot;8di5&quot;&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/RwedjBb&quot; target=&quot;_blank&quot;&gt;Пример на Codepen&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;PdHk&quot;&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/sankey-chart.html&quot; target=&quot;_blank&quot;&gt;Код на Github&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;ZMkO&quot;&gt;&lt;a href=&quot;https://gnykka.github.io/d3-tutorials/sankey-chart.html&quot; target=&quot;_blank&quot;&gt;Сам получившийся график&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

</content></entry><entry><id>gnykka:piet</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/piet?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Язык Piet: программы, выглядящие как абстрактные картины</title><published>2023-01-06T15:31:50.015Z</published><updated>2023-01-06T17:49:52.370Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/f4/63/f463bc07-34e4-4e77-b84c-6e9c52c5ae9a.png"></media:thumbnail><category term="esolang" label="esolang"></category><summary type="html">&lt;img src=&quot;https://img4.teletype.in/files/f1/b6/f1b6f69c-da0c-4cf3-9cea-3448135821c4.png&quot;&gt;Существует отдельный класс языков программирования, называемых эзотерическими. Они придуманы не для решения реальных задач, а ради шутки, для исследования границ возможностей языков или для доказательства какой-то идеи.</summary><content type="html">
  &lt;p id=&quot;1p4W&quot;&gt;Существует отдельный класс языков программирования, называемых эзотерическими. Они придуманы не для решения реальных задач, а ради шутки, для исследования границ возможностей языков или для доказательства какой-то идеи.&lt;/p&gt;
  &lt;p id=&quot;XmQr&quot;&gt;Например, программы на языке Whitespace состоят только из пробелов и табуляций. Программы на Shakespeare выглядят как сонеты Шекспира, на Rockstar — как power metal баллады. А программу на Malbolge написать практически невозможно — язык специально сделан максимально сложным.&lt;/p&gt;
  &lt;p id=&quot;lHn3&quot;&gt;Piet — это один из эзотерических языков, придуманный физиком и автором комиксов Дэвидом Морган-Маром. Название отсылает к художнику Питу Мондриану и его абстрактным композициям.&lt;/p&gt;
  &lt;figure id=&quot;KFIm&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f1/b6/f1b6f69c-da0c-4cf3-9cea-3448135821c4.png&quot; width=&quot;1200&quot; /&gt;
    &lt;figcaption&gt;Победа Буги-Вуги (Victory Boogie Woogie) — последняя&lt;br /&gt;незаконченная картина Пита Мондриана. Начата в 1944 году.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;N4Vo&quot;&gt;Программы на Piet выглядят как разноцветные геометрические абстрактные картины. При этом существует много возможных способов написать одну и ту же программу. Например, картинка ниже — один из вариантов &amp;quot;Hello World&amp;quot;.&lt;/p&gt;
  &lt;figure id=&quot;kPoO&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://www.dangermouse.net/esoteric/piet/hw3-5.gif&quot; width=&quot;415&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;MOqF&quot;&gt;Сущности языка&lt;/h2&gt;
  &lt;p id=&quot;TF2L&quot;&gt;Простейшая сущность Piet — это кодель (codel) — один пиксель. Слово образовано из слияния слов «код» и «пиксель».&lt;/p&gt;
  &lt;p id=&quot;0dPj&quot;&gt;Несколько находящихся рядом пикселей одного цвета образуют цветовой блок (color block).&lt;/p&gt;
  &lt;p id=&quot;pZx8&quot;&gt;Piet работает только с целыми числами и символами юникода. Поэтому чаще всего количество пикселей одного цвета используют, чтобы задать конкретное целое число.&lt;/p&gt;
  &lt;p id=&quot;CLXV&quot;&gt;В основе языка лежит тип данных стек — это список, работающий по принципу LIFO («последним пришёл — первым вышел»).&lt;/p&gt;
  &lt;h2 id=&quot;d4nF&quot;&gt;Исполнение программы&lt;/h2&gt;
  &lt;p id=&quot;e0a0&quot;&gt;Программа на языке Piet — это изображение, составленное из цветовых блоков по своим особым правилам. Исполняется эта программа начиная с левого верхнего угла: от одного коделя к другому ориентируясь на значения двух указателей.&lt;/p&gt;
  &lt;p id=&quot;TjWv&quot;&gt;Первый — указатель направления (direction pointer). Он может указывать направо, вниз, налево или вверх и нужен, чтобы интерпретатор понимал, какой кодель читать следующим.&lt;/p&gt;
  &lt;p id=&quot;UBRt&quot;&gt;Второй — переключатель коделей (codel chooser). Он может указывать направо или налево и помогает интерпретатору определить, в какую сторону двигаться, если возможных вариантов продолжения программы несколько.&lt;/p&gt;
  &lt;p id=&quot;buKM&quot;&gt;Естественными ограничителями программы являются границы изображения и блоки чёрного цвета — через них пройти нельзя. Если указатель направления показывает на чёрный блок, то направление чтения коделей изменится на следующее возможное.&lt;/p&gt;
  &lt;p id=&quot;JB8P&quot;&gt;Программа завершается, если за восемь последовательных попыток интерпретатор застревает и не может сдвинуться с текущего цветового блока.&lt;/p&gt;
  &lt;h2 id=&quot;Cwnu&quot;&gt;Цветовое кодирование&lt;/h2&gt;
  &lt;p id=&quot;gn5Z&quot;&gt;Как можно понять из примера программы, для написания кода используются шесть основных секторов цветового круга с их оттенками, дополненные белым и чёрным цветами.&lt;/p&gt;
  &lt;figure id=&quot;YNVL&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/66/05/660597c0-2645-4f28-96d7-6002c1bbc160.png&quot; width=&quot;476&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;mLrJ&quot;&gt;Чёрные блоки нужны, чтобы задавать границы исполнения. Белые — просто пустое пространство, интерпретатор их игнорирует.&lt;/p&gt;
  &lt;p id=&quot;3Z0M&quot;&gt;Остальные цвета кодируют команды языка. Команды определяются через изменения в оттенке и насыщенности двух соседних цветовых блоков.&lt;/p&gt;
  &lt;figure id=&quot;KDdO&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/50/a7/50a73b00-2ca4-4f03-9924-f86a0fc09aa8.png&quot; width=&quot;365&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;nnMf&quot;&gt;Я не буду сейчас приводить описания каждой команды, их можно прочитать на &lt;a href=&quot;https://www.dangermouse.net/esoteric/piet.html&quot; target=&quot;_blank&quot;&gt;сайте&lt;/a&gt; автора. Но часть команд я чуть позже подробно объясню на примере.&lt;/p&gt;
  &lt;h2 id=&quot;D6cI&quot;&gt;Интерпретаторы&lt;/h2&gt;
  &lt;p id=&quot;jhc2&quot;&gt;Для написания и отладки кода я использовала программу &lt;a href=&quot;https://github.com/dnek/pietron&quot; target=&quot;_blank&quot;&gt;Pietron&lt;/a&gt;. В сети также можно найти несколько онлайн интерпретаторов, например, &lt;a href=&quot;https://gabriellesc.github.io/piet&quot; target=&quot;_blank&quot;&gt;вот этот&lt;/a&gt;. Сразу предупрежу, что разные интерпретаторы немного вольно трактуют некоторые правила, поэтому программа, написанная в одном редакторе, может сломаться в другом.&lt;/p&gt;
  &lt;h2 id=&quot;bY67&quot;&gt;Как написать программу на Piet&lt;/h2&gt;
  &lt;p id=&quot;wsAW&quot;&gt;Изначально идея освоить Piet у меня возникла, когда я решала задачи из &lt;a href=&quot;https://adventofcode.com/2022&quot; target=&quot;_blank&quot;&gt;Advent of Code&lt;/a&gt; этого года. Я хотела решить одну из них, но в итоге начала с чего-то более простого. Так я написала программу, которая вычисляет сумму любого количества переданных на вход положительных целых чисел.&lt;/p&gt;
  &lt;figure id=&quot;XD30&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/23/41/23413985-2030-4823-ad1d-a25cf107067d.png&quot; width=&quot;380&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;tL3n&quot;&gt;И сейчас я расскажу, из каких блоков она состоит и как работает. Для удобства я визуально разбила содержательную часть программы на части.&lt;/p&gt;
  &lt;figure id=&quot;4pl9&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/95/7e/957e40f9-bbe0-4860-8bca-c1ecda401e5e.png&quot; width=&quot;458&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;2YqB&quot;&gt;1. Инициализация&lt;/h3&gt;
  &lt;p id=&quot;7ojB&quot;&gt;Я буду суммировать входные значения и хранить в стеке промежуточное значение суммы. Изначально сумма равна нулю, поэтому мне нужно поместить в стек 0.&lt;/p&gt;
  &lt;p id=&quot;CMcd&quot;&gt;Это делают три коделя первого блока. Начальный кодель программы может быть любого цвета, это влияет только на цветовую гамму. Я выбрала тёмно-синий цвет. Идущий вторым кодель светло-синего цвета — команда &lt;code&gt;push&lt;/code&gt;, она помещает в стек число, равное количеству коделей предыдущего блока. В результате в стеке окажется число 1. Чтобы превратить 1 в 0 я использовала команду &lt;code&gt;not&lt;/code&gt;, она берёт значение из стека, инвертирует его и помещает обратно в стек.&lt;/p&gt;
  &lt;figure id=&quot;puG1&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/e4/cb/e4cb3591-a696-4290-bdb9-e764c21100d4.png&quot; width=&quot;308&quot; /&gt;
    &lt;figcaption&gt;На подобных картинках (они будут после описания)&lt;br /&gt;я буду показывать, какие команды выполняются и как меняется стек&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;f62S&quot;&gt;Как определяются команды&lt;/h3&gt;
  &lt;p id=&quot;pBqp&quot;&gt;Сразу на примере объясню про цвета и оттенки. Чтобы понять, какой цвет выбрать для следующего блока, нужно смотреть на таблицу команд и цвет предыдущего блока.&lt;/p&gt;
  &lt;p id=&quot;bHgB&quot;&gt;Для первого коделя я выбрала тёмно-синий цвет. В этом нет скрытого смысла, мне просто так захотелось. Чтобы получить следом команду &lt;code&gt;push&lt;/code&gt;, как показано в таблице, мне нужно остаться в том же оттенке, но сдвинуть его насыщенность на один шаг темнее. Темнее тёмно-синего в палитре нет, поэтому идём в начало и берём светло-синий.&lt;/p&gt;
  &lt;p id=&quot;3iS3&quot;&gt;Похоже получается и команда &lt;code&gt;not&lt;/code&gt;. По таблице из верхней левой ячейки нужно сделать сдвиг на два шага по оттенку, получив красный вместо синего, и на два шага по насыщенности, получив тёмный вместо светлого.&lt;/p&gt;
  &lt;p id=&quot;ty3f&quot;&gt;Кстати, можно не пугаться — по таблице вручную ничего определять не придётся. Во всех интерпретаторах языка есть подсказки и расшифровки команд.&lt;/p&gt;
  &lt;figure id=&quot;yT3J&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/56/64/5664e9f0-2594-4d54-844e-aa8b33ff9bad.png&quot; width=&quot;306&quot; /&gt;
    &lt;figcaption&gt;Так из тёмно-синего получается светло-синий, а из светло-синего тёмно-красный&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;WZvG&quot;&gt;2. Суммирование&lt;/h3&gt;
  &lt;p id=&quot;EGvn&quot;&gt;Когда у меня всё заготовлено, я могу начинать суммировать значения.&lt;/p&gt;
  &lt;p id=&quot;K5YH&quot;&gt;Значения я получаю из входящих данных, в интерпретаторах это выглядит как обычное текстовое поле, которое можно заполнить до запуска программы. Мои данные — это положительные числа, каждое на новой строчке. Для удобства последним числом будет цифра 0. Появление нуля я буду использовать как критерий завершения суммирования.&lt;/p&gt;
  &lt;p id=&quot;OXGY&quot;&gt;Вторая часть моей программы тоже состоит из всего трёх коделей. Первый из них — команда &lt;code&gt;dup&lt;/code&gt;, дублирование последнего значения в стеке. Непосредственно для суммирования она мне не нужна, но очень поможет в будущем. Дело в том, что большинство команд Piet берут параметры из стека и меняют их. Поэтому единственный способ что-то сделать со значением, но не потерять его при этом — это скопировать его заранее.&lt;/p&gt;
  &lt;p id=&quot;Yisp&quot;&gt;Второй кодель — это команда &lt;code&gt;in&lt;/code&gt;. Она читает число из входного потока данных и помещает его в стек.&lt;/p&gt;
  &lt;p id=&quot;6w00&quot;&gt;Третий кодель — команда &lt;code&gt;add&lt;/code&gt;. Она вынимает два последних числа из стека, складывает их и помещает результат обратно в стек. В конце в стеке оказывается старое начальное значение 0 и новая промежуточная сумма 0 + N1, где N1 — первое число из входных данных.&lt;/p&gt;
  &lt;figure id=&quot;3dqx&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/72/b1/72b1d183-8867-4b7d-8248-efc372a4cacc.png&quot; width=&quot;312&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;dlRS&quot;&gt;3. Проверка&lt;/h3&gt;
  &lt;p id=&quot;sZ52&quot;&gt;Это самый большой и наиболее сложный для понимания блок.&lt;/p&gt;
  &lt;p id=&quot;7d3h&quot;&gt;Так как количество суммируемых чисел у меня может быть любым, то я не могу бесконечно копировать последовательность команд &lt;code&gt;in-add&lt;/code&gt;, мне нужен цикл. Циклы в Piet делаются визуальным разветвлением цветовых блоков. Исполняется тело цикла, потом делается проверка и если условие продолжения цикла выполняется, то с помощью специальных команд можно направить интерпретатор так, чтобы он вернулся в самое начало программы.&lt;/p&gt;
  &lt;p id=&quot;JuyY&quot;&gt;Условия для цикла можно придумать разные. Здесь я остановилась на таком: если после прибавления последнего введённого значения сумма не изменилась, то надо заканчивать цикл. Неизменная сумма будет означать, что во входных данных встретился 0, и это мой критерий завершения программы.&lt;/p&gt;
  &lt;p id=&quot;ilRO&quot;&gt;На этом этапе у меня в стеке как раз находятся два числа — предыдущее и текущее значения суммы. И есть команда &lt;code&gt;great&lt;/code&gt;, которая может сказать мне, какое из чисел больше. Но не всё так просто: &lt;code&gt;great&lt;/code&gt;, как и многие другие команды Piet, меняет стек. Я не могу сразу сравнить две суммы, потому что тогда я их потеряю — они исчезнут из стека и будут заменены на результат сравнения.&lt;/p&gt;
  &lt;p id=&quot;1NdH&quot;&gt;Поэтому первая команда у меня опять &lt;code&gt;dup&lt;/code&gt;, я хочу продублировать текущее значение суммы, чтобы сохранить его после сравнения. Мой стек станет равен &lt;code&gt;&amp;lt;0, N1, N1&amp;gt;&lt;/code&gt;. Но теперь я уже не могу ничего нормально сравнить. Я ведь хотела сравнивать 0 и N1, а 0 в самом низу.&lt;/p&gt;
  &lt;p id=&quot;4duC&quot;&gt;Чтобы это исправить, мне нужно перемешать стек так, чтобы 0 оказался сверху. Перемешивание делается с помощью команды &lt;code&gt;roll&lt;/code&gt;. Она берёт два последних значения стека &amp;lt;K, L&amp;gt;, где L — последнее, а K — предпоследнее, и циклично сдвигает следующие K позиций в стеке L раз. Например, если K — 4, L — 1, а оставшийся стек &amp;lt;1, 2, 3, 4, 5&amp;gt;, то после &lt;code&gt;roll&lt;/code&gt; стек превратится в &amp;lt;1, 5, 4, 3, 2&amp;gt;.&lt;/p&gt;
  &lt;p id=&quot;cVDJ&quot;&gt;Мне нужно из &lt;code&gt;&amp;lt;0, N1, N1&amp;gt;&lt;/code&gt; получить &lt;code&gt;&amp;lt;N1, N1, 0&amp;gt;&lt;/code&gt;, то есть вызвать &lt;code&gt;roll(3, 2)&lt;/code&gt;. Параметры 3 и 2 берутся из стека, и сначала их нужно туда записать двумя командами &lt;code&gt;push&lt;/code&gt;. Чтобы записать 3, я делаю предыдущую команду &lt;code&gt;dup&lt;/code&gt; блоком из трёх коделей, а чтобы записать 2, я делаю команду &lt;code&gt;push&lt;/code&gt; блоком из двух.  &lt;/p&gt;
  &lt;p id=&quot;MJuu&quot;&gt;Теперь можно сделать &lt;code&gt;roll&lt;/code&gt; и получить нужный мне для сравнения стек. После я вызываю команду &lt;code&gt;great&lt;/code&gt;, она берёт два последних значения из стека, сравнивает их и помещает в стек 1, если второе число больше первого и 0, если наоборот.&lt;/p&gt;
  &lt;figure id=&quot;k7rR&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/69/d3/69d33b22-913d-4e1a-8a63-157f2af29ebb.png&quot; width=&quot;518.1538461538461&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;6ah7&quot;&gt;4. Цикл&lt;/h3&gt;
  &lt;p id=&quot;EAPq&quot;&gt;У меня всё готово для цикла. Условие звучит так: если в стеке сверху лежит 1, то суммирование должно продолжиться, а если лежит 0, то нужно заканчивать.&lt;/p&gt;
  &lt;p id=&quot;xirF&quot;&gt;Как я писала выше, циклы в Piet визуальные — нужно повернуть направление чтения коделей таким образом, чтобы в результате оказаться в самом начале.&lt;/p&gt;
  &lt;p id=&quot;ikU1&quot;&gt;За выбор следующего коделя отвечает указатель направления. Изначально он указывает направо, поэтому программа начинает исполнение из левого верхнего угла по прямой направо.&lt;/p&gt;
  &lt;p id=&quot;X34E&quot;&gt;Указатель направления можно поменять командой &lt;code&gt;point&lt;/code&gt;. Она принимает один числовой параметр и поворачивает указатель нужное число раз. Например, &lt;code&gt;point(1)&lt;/code&gt; изменит указатель со значения «направо» на значение «вниз», а &lt;code&gt;point(2)&lt;/code&gt; — на значение «налево».&lt;/p&gt;
  &lt;p id=&quot;ETDI&quot;&gt;У меня всё подготовлено для блока &lt;code&gt;point&lt;/code&gt; — наверху стека может лежать 0 или 1.  Если там 0, то исполнение программы продолжится направо, а если 1, то повернёт вниз.&lt;/p&gt;
  &lt;p id=&quot;I7XA&quot;&gt;Казалось бы, всё отлично, но есть нюанс. Я хочу вернуться в начало цикла и чтобы первой командой там по-прежнему была команда &lt;code&gt;dup&lt;/code&gt;. Но после блока цвета тёмной мадженты тёмно-красный блок станет командой &lt;code&gt;add&lt;/code&gt;, а тёмно-синий — &lt;code&gt;in&lt;/code&gt;. Если я просто поверну, не сбросив цвета, то я сломаю себе весь цикл.&lt;/p&gt;
  &lt;p id=&quot;3naF&quot;&gt;Но я могу воспользоваться интересной особенностью языка. По таблице команд светло-голубые кодели после тёмной мадженты — команды &lt;code&gt;roll&lt;/code&gt;. По идее они должны менять стек, но если перед ними оказываются блоки белого цвета, то они игнорируются и ничего не делают. Ещё я могу использовать чёрные блоки вокруг для ограничения рабочей области — через них интерпретатор пройти не сможет.&lt;/p&gt;
  &lt;p id=&quot;cJaG&quot;&gt;После команд &lt;code&gt;roll&lt;/code&gt; у меня есть одна команда push, которая поместит в стек 1. И я сразу попадаю в тёмно-красный блок, который оказывается командой &lt;code&gt;point&lt;/code&gt;, забирает единицу из стека и поворачивает указатель направления.&lt;/p&gt;
  &lt;p id=&quot;o23b&quot;&gt;Я пришла в начало, следующим стал тёмно-синий блок, который как мне и нужно, представляет команду &lt;code&gt;dup&lt;/code&gt;.&lt;/p&gt;
  &lt;figure id=&quot;aWJI&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/8e/da/8eda3040-af2b-4e2e-93ce-36e4de7a7c7a.png&quot; width=&quot;450&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;oSKh&quot;&gt;5. Выход из цикла&lt;/h3&gt;
  &lt;p id=&quot;Jdup&quot;&gt;На последней итерации цикла я получу из входных данных 0. Прибавление нуля не изменит предыдущее значение, и команда &lt;code&gt;roll&lt;/code&gt; ничего не перемешает, ведь в стеке окажутся три одинаковых значения. Условие цикла, команда &lt;code&gt;great&lt;/code&gt;, вернёт 0, а команда &lt;code&gt;point&lt;/code&gt; не поменяет указатель — исполнение программы продолжится в том же направлении, напечатав результат с помощью команды &lt;code&gt;out&lt;/code&gt;. Интерпретатор окажется в белой области без каких-либо команд и завершит исполнение программы.&lt;/p&gt;
  &lt;figure id=&quot;HNbV&quot; class=&quot;m_custom&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/24/b2/24b21d3b-a8b2-4c8d-b739-2cc6981ba014.png&quot; width=&quot;467.9227941176471&quot; /&gt;
    &lt;figcaption&gt;Вот, что происходит на последней итерации цикла. Nn — итоговое значение суммы.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;3hqQ&quot;&gt;Итог&lt;/h2&gt;
  &lt;p id=&quot;0Byi&quot;&gt;Ничего из задач Advent of Code я так в итоге на Piet и не решила.&lt;/p&gt;
  &lt;p id=&quot;z9NG&quot;&gt;Но я рада, что наконец реализовала свою давнюю хотелку и написала свою первую полноценную программу, пусть она и довольно простая. Мне было весело и интересно!&lt;/p&gt;
  &lt;p id=&quot;ZgHY&quot;&gt;Возможно, в будущем я и другие эзотерические языки попробую. А вы пробовали?&lt;/p&gt;

</content></entry><entry><id>gnykka:pies</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/pies?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Как сделать простой PieChart на D3.js</title><published>2021-11-29T10:02:59.438Z</published><updated>2023-12-14T20:32:15.568Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/77/5c/775c7354-b24d-4ecd-9bdf-088c84e67cc5.jpeg"></media:thumbnail><category term="d3js" label="d3js"></category><summary type="html">&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/a/a1/Playfair_piecharts.jpg&quot;&gt;Любите ли вы пайчарты? Я вот не очень, но они регулярно мне встречаются и иногда очень даже симпатичные и информативные. Поэтому сегодня я решила написать про них небольшую заметку и разобрать, как сделать простой пайчарт с помощью D3.</summary><content type="html">
  &lt;p id=&quot;O5KV&quot;&gt;Любите ли вы пайчарты? Я вот не очень, но они регулярно мне встречаются и иногда очень даже симпатичные и информативные. Поэтому сегодня я решила написать про них небольшую заметку и разобрать, как сделать простой пайчарт с помощью D3.&lt;/p&gt;
  &lt;h2 id=&quot;qEpX&quot;&gt;Немного истории&lt;/h2&gt;
  &lt;p id=&quot;20mG&quot;&gt;Первый пайчарт нарисовал Уильям Плейфэр в своей книге «Краткое изложение статистики» (&amp;quot;Statistical Breviary&amp;quot;) ещё в 1801 году. Та визуализация показывала владения Османской империи в разных частях света.&lt;/p&gt;
  &lt;figure id=&quot;9nMZ&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/a/a1/Playfair_piecharts.jpg&quot; width=&quot;1050&quot; /&gt;
    &lt;figcaption&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/File:Playfair_piecharts.jpg&quot; target=&quot;_blank&quot;&gt;Визуализация Плейфэра&lt;/a&gt;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;HIcq&quot;&gt;Новый график не был очень популярен в то время, а сам Плейфэр считал, что ему не хватает третьего измерения, негде поместить дополнительную информацию. &lt;/p&gt;
  &lt;p id=&quot;peTw&quot;&gt;Позже, в 1858 году Флоренс Найтингейл переиспользовала пайчарт, сделав визуализацию причин смерти солдат во время Крымской войны. &lt;/p&gt;
  &lt;figure id=&quot;WQu3&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Nightingale-mortality.jpg/2560px-Nightingale-mortality.jpg&quot; width=&quot;2560&quot; /&gt;
    &lt;figcaption&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/File:Nightingale-mortality.jpg&quot; target=&quot;_blank&quot;&gt;Визуализация Найтингейл&lt;/a&gt;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;h2 id=&quot;8qJm&quot;&gt;Чем плохи и хороши пайчарты&lt;/h2&gt;
  &lt;p id=&quot;cRGF&quot;&gt;Если говорить честно, я не люблю пайчарты. Соглашусь тут с Тафти, что они используют избыточное количество чернил для отображения очень простых данных (хотя я и не всегда согласна с идеей Тафти о максимальном упрощении всего).&lt;/p&gt;
  &lt;figure id=&quot;EIfE&quot; class=&quot;m_original&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/3a/e6/3ae6afc3-8c71-4d83-b37f-822693ba4d12.png&quot; width=&quot;442&quot; /&gt;
    &lt;figcaption&gt;&lt;em&gt;Одни и те же данные ([35, 25, 25, 15]) в разных представлениях&lt;/em&gt;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;gBMs&quot;&gt;Но главная проблема пайчартов не в этом. Она в том, что они используют площадь, то есть двумерную величину. Мы легко можем сказать, какой из элементов находится выше или ниже, какой длиннее или короче, но нам сложно определить, какой элемент больше по площади.&lt;/p&gt;
  &lt;p id=&quot;wJwX&quot;&gt;Например, эти прямоугольники разные (не все), но сложно быстро расположить их по убыванию их размера:&lt;/p&gt;
  &lt;figure id=&quot;O8ii&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/40/98/4098ae98-6232-48b3-b29d-b9914fcb69cf.png&quot; width=&quot;1308&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Fwda&quot;&gt;Похожая проблема возникает и с секторами круга: понять, какой сектор больше, можно, сравнив длины дуг окружности, а вот определить, насколько он больше, становится сложнее. Особенно это заметно, когда сектора по-разному повёрнуты.&lt;/p&gt;
  &lt;figure id=&quot;tCjT&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/28/69/28691b63-e33d-43c2-9c36-537d9fc512bb.png&quot; width=&quot;637&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;yw1X&quot;&gt;Но не всё с пайчартами плохо. Их вполне можно эффективно использовать, когда хочется показать отношение небольшого количества величин друг к другу и к их сумме.&lt;/p&gt;
  &lt;h2 id=&quot;ttpq&quot;&gt;Как сделать простой пайчарт на D3&lt;/h2&gt;
  &lt;p id=&quot;pZNy&quot;&gt;Сейчас расскажу, как сделать самый обычный пайчарт на D3.&lt;/p&gt;
  &lt;p id=&quot;7QEC&quot;&gt;В качестве данных я взяла площади континентов (в миллионах квадратных километров):&lt;/p&gt;
  &lt;pre id=&quot;CxnX&quot;&gt;const data = {
  Asia: 44.6,
  Africa: 30,
  North America: 24.5,
  South America: 17.8,
  Antarctica: 14.2,
  Europe: 9.9,
  Australia: 7.7,
}&lt;/pre&gt;
  &lt;p id=&quot;AFZJ&quot;&gt;Сначала подготовим svg для рисования:&lt;/p&gt;
  &lt;pre id=&quot;7z9w&quot;&gt;const size = 300;
const radius = size / 2;

const svg = d3.select(&amp;#x27;svg&amp;#x27;)
  .attr(&amp;#x27;width&amp;#x27;, size)
  .attr(&amp;#x27;height&amp;#x27;, size);

const g = svg.append(&amp;#x27;g&amp;#x27;)
  .attr(&amp;#x27;transform&amp;#x27;, &amp;#x60;translate(${radius}, ${radius})&amp;#x60;);&lt;/pre&gt;
  &lt;p id=&quot;bwA1&quot;&gt;Transform на половину размера нужен, чтобы сегменты отрисовывались из середины всей области, а не из верхнего левого угла.&lt;/p&gt;
  &lt;p id=&quot;c4lB&quot;&gt;Теперь подготовим данные:&lt;/p&gt;
  &lt;pre id=&quot;kc5g&quot;&gt;const pie = d3.pie();
const pieData = pie(Object.values(data));&lt;/pre&gt;
  &lt;p id=&quot;ofNi&quot;&gt;d3.pie — это специальный метод, который по массиву значений вычисляет все параметры сегментов круга, в том числе их начальные и конечные углы. Его код можно посмотреть &lt;a href=&quot;https://github.com/d3/d3-shape/blob/main/src/pie.js&quot; target=&quot;_blank&quot;&gt;здесь&lt;/a&gt; — весь класс занимает всего 80 строчек. &lt;/p&gt;
  &lt;p id=&quot;Ut9P&quot;&gt;В результате получаем данные такого вида:&lt;/p&gt;
  &lt;pre id=&quot;NWZz&quot;&gt;[1]: {
  index: 1,
  data: 30,
  value: 30,
  padAngle: 0,
  startAngle: 1.8845330511110263,
  endAngle: 3.152156179661044,
}&lt;/pre&gt;
  &lt;p id=&quot;Ijnp&quot;&gt;Пайчарт состоит из сегментов, то есть закрашенных дуг окружности. Дуги это простые path элементы. Создадим их и привяжем к ним данные:&lt;/p&gt;
  &lt;pre id=&quot;Pcl5&quot;&gt;const arcs = g.selectAll(&amp;#x27;.arc&amp;#x27;)
  .data(pieData)
  .join(enter =&amp;gt; enter
    .append(&amp;#x27;path&amp;#x27;)
    .attr(&amp;#x27;class&amp;#x27;, &amp;#x27;arc&amp;#x27;)
  );&lt;/pre&gt;
  &lt;p id=&quot;VGpJ&quot;&gt;Теперь можно рисовать. Сначала нам нужны палитра и функция для рисования дуг (arc). По смыслу и действию arc() похожа на line(): она умеет строить линию по элементу массива, подготовленного методом pie(). Для этого ей нужны 2 значения: внутренний и внешний радиус. Кстати, если внутренний радиус сделать не нулевым, то вместо пайчарта будет донат.&lt;/p&gt;
  &lt;p id=&quot;aBNU&quot;&gt;Когда всё готово, мы можем всем дугам (arcs) присвоить атрибуты формы (d) и цвета (fill):&lt;/p&gt;
  &lt;pre id=&quot;zkej&quot;&gt;const color = d3.scaleOrdinal(d3.schemeCategory10);
const arc = d3.arc()
  .outerRadius(radius - 10)
  .innerRadius(0);
  
arcs
  .attr(&amp;#x27;d&amp;#x27;, arc)
  .style(&amp;#x27;fill&amp;#x27;, (_, i) =&amp;gt; color(i));&lt;/pre&gt;
  &lt;figure id=&quot;MvVh&quot; class=&quot;m_retina&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/4c/ff/4cfff2e6-d6e2-47be-8d65-ea8e8a497964.png&quot; width=&quot;316&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;40RI&quot;&gt;Получился вполне неплохой пайчарт. Но в нём ничего не понятно. Что значат цвета, какие значения у какого из сегментов? Можно, конечно, добавить легенду, но я покажу, как подписать каждый из элементов на самом графике.&lt;/p&gt;
  &lt;p id=&quot;QDNM&quot;&gt;Чтобы определить, где расположить подписи, создадим ещё одну функцию arc. Так как сами дуги рисовать мы не будем, можно сделать одинаковые радиусы где-то около границ, но так, чтобы текст точно попадал внутрь:&lt;/p&gt;
  &lt;pre id=&quot;d8uL&quot;&gt;const label = d3.arc()
  .outerRadius(radius - 25)
  .innerRadius(radius - 25);&lt;/pre&gt;
  &lt;p id=&quot;OYUu&quot;&gt;И после уже добавим текстовые элементы, сдвинув их в нужные позиции. Сдвинуть можно с помощью метода centroid, который получает данные о дуге и возвращает массив из двух координат — её центр:&lt;/p&gt;
  &lt;pre id=&quot;OYUu&quot;&gt;const texts = g.selectAll(&amp;#x27;text&amp;#x27;)
  .data(pieData)
  .join(enter =&amp;gt; enter
    .append(&amp;#x27;text&amp;#x27;)
    .attr(&amp;#x27;transform&amp;#x27;, d =&amp;gt; &amp;#x60;translate(${label.centroid(d)})&amp;#x60;)
    .text(d =&amp;gt; &amp;#x60;${Object.keys(data)[d.index]}: ${d.value}&amp;#x60;));&lt;/pre&gt;
  &lt;figure id=&quot;C2kl&quot; class=&quot;m_retina&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f1/7e/f17eaf1a-c881-4d5f-ab04-6e3cff140941.png&quot; width=&quot;318&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;CrZQ&quot;&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/porMaXo&quot; target=&quot;_blank&quot;&gt;Код примера можно рассмотреть здесь.&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>gnykka:d3-maps</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/d3-maps?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Карты в визуализациях</title><published>2021-02-15T15:09:43.581Z</published><updated>2021-02-15T15:18:01.972Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://teletype.in/files/39/1f/391f8b98-88d1-4a14-a3ec-683c7295fbb6.png"></media:thumbnail><category term="d3js" label="d3js"></category><summary type="html">&lt;img src=&quot;https://teletype.in/files/fb/9d/fb9d13cd-a008-4c73-a939-8fcf0c896156.png&quot;&gt;Визуализаций с картами встречается очень много. Но карты в них — не какой-то отдельный тип отображения данных, а больше специальный слой со своей координатной системой.</summary><content type="html">
  &lt;p&gt;Визуализаций с картами встречается очень много. Но карты в них — не какой-то отдельный тип отображения данных, а больше специальный слой со своей координатной системой.&lt;/p&gt;
  &lt;p&gt;Эта статья у меня получилась большой, потому что хотелось охватить всё и сразу. Здесь и теория с проекциями и координатами, и работа с данными, и несколько способов отображения.&lt;/p&gt;
  &lt;p&gt;Ссылки:&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/qBqraar&quot; target=&quot;_blank&quot;&gt;Codepen c D3 картой&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/VwaNGeP&quot; target=&quot;_blank&quot;&gt;Codepen с Leaflet картой&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2&gt;Проекции&lt;/h2&gt;
  &lt;p&gt;Чтобы понять, как использовать карты в визуализациях, надо понимать, как они работают. А для этого стоит немного посмотреть теорию картографии.&lt;/p&gt;
  &lt;p&gt;Я буду говорить про карту в контексте географической карты. Это уменьшенная и упрощённая модель реального объекта, например, Земли. Но планета круглая, поэтому первая задача — представить её на плоской поверхности, с которой будет легче работать.&lt;/p&gt;
  &lt;p&gt;Для этого представления придуманы картографические проекции. Они помогают растянуть трёхмерную поверхность так, чтобы подменить её плоской, делая некоторые упрощения. Любая проекция не может сохранить все параметры изначального объекта, поэтому она всегда искажённая. В ней могут быть неправильные пропорции, размеры или углы. Самая привычная и стандартная проекция — проекция Меркатора, она сохраняет углы, но искажает размеры. Чем область дальше от экватора, тем её площадь будет больше.&lt;/p&gt;
  &lt;p&gt;Я выкладывала в канале хорошее &lt;a href=&quot;https://bl.ocks.org/syntagmatic/ba569633d51ebec6ec6e&quot; target=&quot;_blank&quot;&gt;демо со сравнениями проекций&lt;/a&gt;. Там можно наглядно увидеть, какие параметры и как искажаются.&lt;/p&gt;
  &lt;p&gt;Отдельно упомяну про координаты. Упрощённо Земля воспринимается как шар, поэтому её система координат сферическая, где любая точка задаётся расстоянием от центра сферы и двумя углами. В привычных терминах, расстояние до центра определяет высоту над уровнем моря, а углы — это широта и долгота. Чтобы удобнее было работать на плоскости, в географии принята гауссовская сетка — она превращает систему долгот и широт в ортогональные координаты. Долготы расставлены с одинаковым шагом, а широты с разным, увеличивающимся по мере удаления от экватора. Лично я эти оси вечно путаю, поэтому иногда вместо Москвы получаю Иран, но достаточно запомнить, что экватор — это нулевая широта.&lt;/p&gt;
  &lt;h2&gt;Данные&lt;/h2&gt;
  &lt;p&gt;Для работы с геоданными существуют географические информационные системы или ГИС. Они предоставляют средства для сбора, хранения и анализа данных, могут эти данные визуализировать и преобразовывать.&lt;/p&gt;
  &lt;p&gt;Данные в этих системах могут представляться в разных форматах. Самый простой — обычные растровые изображения (PNG или TIFF), это, например, снимки местности в определённом масштабе. Кроме них есть формат GeoTIFF, это расширенный TIFF формат с метаданными, геотегами и дополнительной информацией. Ещё есть векторные форматы, например Shapefile, он хранит геометрические объекты: точки, линии или полигоны.&lt;/p&gt;
  &lt;p&gt;Но с точки зрения программирования работать с этим всем тяжело. И на помощь приходит стандартный текстовый формат представления данных: JSON (JavaScript Object Notation). Он появился в рамках языка JavaScript, но сейчас уже не зависит от него и может использоваться везде. По структуре это либо набор пар «ключ: значение», либо просто перечисление значений.&lt;/p&gt;
  &lt;p&gt;Для географических данных в 2008 году придумали свой формат, основанный на JSON и назвали его GeoJSON. Этот формат описывает простые типы геоданных (точка, линия, полигон) и позволяет описывать какие-то их параметры. Выглядит он как-то так:&lt;/p&gt;
  &lt;pre&gt;{
  &amp;quot;type&amp;quot;: &amp;quot;FeatureCollection&amp;quot;,
  &amp;quot;features&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;Feature&amp;quot;,
      &amp;quot;geometry&amp;quot;: {&amp;quot;type&amp;quot;: &amp;quot;Point&amp;quot;, &amp;quot;coordinates&amp;quot;: [102.0, 0.5]},
      &amp;quot;properties&amp;quot;: {&amp;quot;prop0&amp;quot;: &amp;quot;value0&amp;quot;}
    }, {
      &amp;quot;type&amp;quot;: &amp;quot;Feature&amp;quot;,
      &amp;quot;geometry&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;LineString&amp;quot;,
        &amp;quot;coordinates&amp;quot;: [
          [102.0, 0.0], [103.0, 1.0], [104.0, 0.0], [105.0, 1.0]
        ]
      },
      &amp;quot;properties&amp;quot;: {
        &amp;quot;prop0&amp;quot;: &amp;quot;value0&amp;quot;,
        &amp;quot;prop1&amp;quot;: 0.0
      }
    }, {
      &amp;quot;type&amp;quot;: &amp;quot;Feature&amp;quot;,
      &amp;quot;geometry&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;Polygon&amp;quot;,
        &amp;quot;coordinates&amp;quot;: [
          [[100.0, 0.0], [101.0, 0.0], [101.0, 1.0],
           [100.0, 1.0], [100.0, 0.0]]
        ]
      },
      &amp;quot;properties&amp;quot;: {
        &amp;quot;prop0&amp;quot;: &amp;quot;value0&amp;quot;,
        &amp;quot;prop1&amp;quot;: {&amp;quot;this&amp;quot;: &amp;quot;that&amp;quot;}
      }
    }
  ]
}&lt;/pre&gt;
  &lt;p&gt;Это пример из &lt;a href=&quot;https://ru.wikipedia.org/wiki/GeoJSON&quot; target=&quot;_blank&quot;&gt;википедии&lt;/a&gt;. Там же хорошо и подробно описана схема всего формата.&lt;/p&gt;
  &lt;p&gt;Ещё есть формат TopoJSON, это расширение GeoJSON, которое позволяет определять и переиспользовать элементы. За счёт этого можно сильно уменьшить размер файла.&lt;/p&gt;
  &lt;p&gt;Сейчас GeoJSON — самый распространённый формат, который используется  при программировании географических данных. Его полностью поддерживают большинство современных картографических сервисов и ГИС.&lt;/p&gt;
  &lt;h2&gt;Как сделать простую карту с D3&lt;/h2&gt;
  &lt;p&gt;Практическую часть этой статьи я начну с D3 и работы с GeoJSON. Я хочу сделать небольшую визуализацию, на которой будет видна карта мира и будут выделены отдельные города.&lt;/p&gt;
  &lt;p&gt;Данные для этой визуализации я возьму из открытых источников, например, &lt;a href=&quot;https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson&quot; target=&quot;_blank&quot;&gt;отсюда&lt;/a&gt;. Это GeoJSON файл со всеми странами мира.&lt;/p&gt;
  &lt;p&gt;Для начала работы мне нужно подключить только скрипт D3, других зависимостей тут нет:&lt;/p&gt;
  &lt;pre&gt;&amp;lt;script src=&amp;quot;https://d3js.org/d3.v6.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/pre&gt;
  &lt;p&gt;В другом скрипте я сначала создам svg элемент и укажу ему нужные размеры:&lt;/p&gt;
  &lt;pre&gt;const width = 600;
const height = 400;

const svg = d3.select(&amp;#x27;body&amp;#x27;).append(&amp;#x27;svg&amp;#x27;)
  .attr(&amp;#x27;width&amp;#x27;, width)
  .attr(&amp;#x27;height&amp;#x27;, height);

const mapGroup = svg.append(&amp;#x27;g&amp;#x27;);&lt;/pre&gt;
  &lt;p&gt;А потом подготовлю проекцию:&lt;/p&gt;
  &lt;pre&gt;const projection = d3.geoMercator()
  .scale([width / (3 * Math.PI)]);&lt;/pre&gt;
  &lt;p&gt;С точки зрения D3 проекция — это функция, которая преобразовывает географические координаты в координаты экрана по каким-то своим правилам. Можно задать свою новую функцию, можно использовать готовые преобразования. Я возьму Меркатор и немного подправлю его, чтобы карта полностью помещалась в контейнер.&lt;/p&gt;
  &lt;p&gt;Следующий шаг — создать функцию, рисующую path элементы, используя подготовленную проекцию:&lt;/p&gt;
  &lt;pre&gt;const path = d3.geoPath().projection(projection);&lt;/pre&gt;
  &lt;p&gt;Эта функция будет получать информацию о каждой стране из загруженных данных, видеть там поле geometry, брать оттуда массив координат и рисовать его, применяя заданную проекцию.&lt;/p&gt;
  &lt;p&gt;Теперь можно загрузить данные и показать страны:&lt;/p&gt;
  &lt;pre&gt;d3.json(&amp;#x27;https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson&amp;#x27;)
  .then(data =&amp;gt; {
    mapGroup.selectAll(&amp;#x27;path&amp;#x27;)
      .data(data.features)
      .enter()
      .append(&amp;#x27;path&amp;#x27;)
      .attr(&amp;#x27;d&amp;#x27;, path)
      .attr(&amp;#x27;fill&amp;#x27;, &amp;#x27;#0aa0aa&amp;#x27;)
      .style(&amp;#x27;stroke&amp;#x27;, &amp;#x27;#ffffff&amp;#x27;);
});&lt;/pre&gt;
  &lt;p&gt;Для загрузки я воспользуюсь методом json, а цвета пропишу прямо в аттрибутах. Созданную выше функцию path я передам аттрибуту d. Получается вот такая картинка:&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/fb/9d/fb9d13cd-a008-4c73-a939-8fcf0c896156.png&quot; width=&quot;1700&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Кстати, тут очень наглядно видно искажение Меркатора — огромная Антарктида внизу.&lt;/p&gt;
  &lt;h2&gt;Как показать дополнительные данные на простой D3 карте&lt;/h2&gt;
  &lt;p&gt;Обычно недостаточно просто нарисовать области на карте, нужно ещё что-то закрасить или пометить маркерами. И сейчас я покажу, как это можно сделать.&lt;/p&gt;
  &lt;p&gt;Допустим, я хочу раскрасить страны в соответствии с численностью их населения. Для этого сначала мне опять нужны данные. Я возьму их &lt;a href=&quot;https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world_population.csv&quot; target=&quot;_blank&quot;&gt;отсюда&lt;/a&gt;.&lt;/p&gt;
  &lt;p&gt;Данных больше, чем мне нужно, поэтому я их отфильтрую и превращу в удобный мне для работы объект. Возьму только те страны, которые есть на моей карте:&lt;/p&gt;
  &lt;pre&gt;d3.csv(&amp;#x27;https://raw.githubusercontent.com/datasets/population/master/data/population.csv&amp;#x27;)
  .then(population =&amp;gt; {
    const ids = data.features.map(d =&amp;gt; d.id);
    const populationData = population
      .filter(d =&amp;gt; d[&amp;#x27;Year&amp;#x27;] === &amp;#x27;2018&amp;#x27; &amp;amp;&amp;amp;
        ids.includes(d[&amp;#x27;Country Code&amp;#x27;]))
      .reduce((res, d) =&amp;gt; {
        res[d[&amp;#x27;Country Code&amp;#x27;]] = +d[&amp;#x27;Value&amp;#x27;];
        return res;
      }, {});
  });&lt;/pre&gt;
  &lt;p&gt;Шкалу я сделаю линейную, где ноль или отсутствие данных будет белым цветом, а максимальное значение — сине-зелёным.&lt;/p&gt;
  &lt;pre&gt;const colorDomain = [
  d3.min(Object.keys(populationData), d =&amp;gt; populationData[d]),
  d3.max(Object.keys(populationData), d =&amp;gt; populationData[d]),
];
const colorScale = d3.scaleLinear()
  .range([&amp;#x27;#ffffff&amp;#x27;, &amp;#x27;#0aa0aa&amp;#x27;])
  .domain(colorDomain);&lt;/pre&gt;
  &lt;p&gt;Теперь осталось только добавить цвет областям при рисовании. Если значение существует, то шкала его определит, если не существует (Антарктида, например), то я указываю белый цвет явно:&lt;/p&gt;
  &lt;pre&gt;attr(&amp;#x27;fill&amp;#x27;, d =&amp;gt; (populationData[d.id]
  ? colorScale(populationData[d.id])
  : &amp;#x27;#ffffff&amp;#x27;
))&lt;/pre&gt;
  &lt;p&gt;Итоговый результат:&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/4b/87/4b878832-0a1b-4ab1-b1a2-858093a52e61.png&quot; width=&quot;1352&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Кроме закрашивания областей иногда может быть нужно нарисовать что-то на карте. Для этого пригодится объект projection, работающий как шкала преобразования систем координат.&lt;/p&gt;
  &lt;p&gt;Например, следующий код добавляет маркер на место Москвы:&lt;/p&gt;
  &lt;pre&gt;const moscow = projection([37.6173, 55.7558]);

svg.append(&amp;#x27;circle&amp;#x27;)
  .attr(&amp;#x27;cx&amp;#x27;, moscow[0])
  .attr(&amp;#x27;cy&amp;#x27;, moscow[1]);&lt;/pre&gt;
  &lt;h2&gt;Как сделать карту c Leaflet&lt;/h2&gt;
  &lt;p&gt;Иногда схематических контуров оказывается недостаточно, и нужна полноценная карта со всеми странами, городами, дорогами и прочим. В этом случае на помощь приходят библиотеки для рисования карт. Их довольно много, но я буду рассказывать про &lt;a href=&quot;https://leafletjs.com/&quot; target=&quot;_blank&quot;&gt;Leaflet&lt;/a&gt;, потому что обычно использую именно её.&lt;/p&gt;
  &lt;p&gt;Сначала я подготовлю вёрстку и зависимости:&lt;/p&gt;
  &lt;pre&gt;&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;script src=&amp;quot;https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://unpkg.com/leaflet@1.7.1/dist/leaflet.css&amp;quot;&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id=&amp;quot;map&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/pre&gt;
  &lt;p&gt;Это минимальный код, с которым можно будет работать. Обратите внимание, что я подгружаю не только скрипт, но и стили, это важно. У div элемента (будущего контейнера карты) я сразу задам идентификатор. Он будет нужен для инициализации.&lt;/p&gt;
  &lt;p&gt;Чтобы показать простую карту, нужно вызвать метод map:&lt;/p&gt;
  &lt;pre&gt;const tilesUrl = &amp;#x27;https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png&amp;#x27;;
const map = L.map(&amp;#x27;map&amp;#x27;).setView([39.25, -48.16], 3);

L.tileLayer(tilesUrl).addTo(map);&lt;/pre&gt;
  &lt;p&gt;L — это Leaflet, имя, по которому я могу обращаться к библиотеке. Методу map я передаю идентификатор контейнера. SetView устанавливает центр видимой области и уровень приближения. Он может быть целым положительным числом.&lt;/p&gt;
  &lt;p&gt;Недостаточно просто создать карту и указать ей область видимости. Нужны тайлы — небольшие картинки одинаковых размеров, которые вместе составляют большое изображение. В контексте карты тайлы — это отрисованные участки области на разных уровнях приближения.&lt;/p&gt;
  &lt;p&gt;У Leaflet это отдельный слой данных, который мы в явном виде добавляем на карту. Определяются они шаблонной строкой как tilesUrl в коде выше. Кроме этого можно указать копирайт и какие-то дополнительные аттрибуты.&lt;/p&gt;
  &lt;p&gt;На OpenStreetMaps есть &lt;a href=&quot;https://wiki.openstreetmap.org/wiki/Tile_servers&quot; target=&quot;_blank&quot;&gt;неплохой список&lt;/a&gt; открытых и доступных для использования тайлов. &lt;/p&gt;
  &lt;p&gt;Когда карта есть, можно легко добавить на неё маркеры:&lt;/p&gt;
  &lt;pre&gt;const cities = [
  { name: &amp;#x27;Rome&amp;#x27;, coords: [41.89, 12.51] },
  { name: &amp;#x27;Chicago&amp;#x27;, coords: [41.74, -87.55] },
  { name: &amp;#x27;San Francisco&amp;#x27;, coords: [37.77, -122.42] },
  { name: &amp;#x27;Athens&amp;#x27;, coords: [37.98, 23.73] },
  { name: &amp;#x27;Toronto&amp;#x27;, coords: [43.67, -79.42] },
  { name: &amp;#x27;Toulouse&amp;#x27;, coords: [43.60, 1.44] },
];
const markers = cities.map(c =&amp;gt; 
  L.marker(c.coords)
    .addTo(map)
    .bindPopup(&amp;#x60;${c.name}: [${c.coords.join(&amp;#x27;, &amp;#x27;)}]&amp;#x60;)
);&lt;/pre&gt;
  &lt;p&gt;Маркер добавляется с помощью функции L.marker, которой передаётся массив из двух координат. Здесь я никак не меняю внешний вид маркеров, но их можно сделать совершенно любыми. Ещё я добавила стандартный тултип, который будет появляться при клике и показывать название города и его координаты.&lt;/p&gt;
  &lt;p&gt;Выглядит всё это в результате так:&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/ef/1d/ef1dc9c5-d8ef-4816-a50a-b964185edff0.png&quot; width=&quot;1990&quot; /&gt;
  &lt;/figure&gt;
  &lt;hr /&gt;
  &lt;p&gt;Это поверхностный обзор работы с картами. Если копнуть глубже, можно делать очень сложные вещи, интерактивные или динамические. Например, можно объединить Leaflet с D3 или добавить изометрию и рисовать трёхмерные визуализации на обычной карте.&lt;/p&gt;

</content></entry><entry><id>gnykka:d3-grouped-bars</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/d3-grouped-bars?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Как сделать BarChart с несколькими сериями данных</title><published>2021-01-11T11:19:23.167Z</published><updated>2021-01-11T11:19:23.167Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://teletype.in/files/55/ee/55ee5bf4-2e16-4bfc-be6b-ea92e5731604.png"></media:thumbnail><category term="d3js" label="d3js"></category><summary type="html">&lt;img src=&quot;https://teletype.in/files/94/66/94666d92-ac9c-429f-984e-b8231d28e255.png&quot;&gt;В этом туториале я разберу создание столбчатой диаграммы (bar chart) сначала для одной, а потом и для нескольких серий данных. Ещё немного расскажу про цветовые шкалы.</summary><content type="html">
  &lt;p&gt;В этом туториале я разберу создание столбчатой диаграммы (bar chart) сначала для одной, а потом и для нескольких серий данных. Ещё немного расскажу про цветовые шкалы.&lt;/p&gt;
  &lt;p&gt;В прошлый раз мне несколько человек написали, что было бы круто, если бы в примеры можно было потыкать. Поэтому теперь я буду не только выкладывать код в гит репозиторий, но и делать небольшие интерактивные codepen&amp;#x27;ы.&lt;/p&gt;
  &lt;p&gt;Ссылки к этому туториалу:&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/VwKdaEr&quot; target=&quot;_blank&quot;&gt;Codepen с графиком&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/bar-chart.html&quot; target=&quot;_blank&quot;&gt;Полный код графика на github&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2&gt;&lt;strong&gt;Подготовка данных&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Подготовку проекта и создание svg элемента я уже описывала в &lt;a href=&quot;https://teletype.in/@gnykka/d3-line-scatter&quot; target=&quot;_blank&quot;&gt;предыдущем туториале&lt;/a&gt;, поэтому не буду повторяться. А вот данные я сгенерирую другие. Пусть у меня будут двенадцать месяцев и столбики значений от 0 до 1 (округлённые до 2 знаков после запятой) для каждого из них:&lt;/p&gt;
  &lt;pre&gt;​const months = [
  &amp;#x27;янв&amp;#x27;, &amp;#x27;фев&amp;#x27;, &amp;#x27;март&amp;#x27;, &amp;#x27;апр&amp;#x27;, &amp;#x27;май&amp;#x27;, &amp;#x27;июнь&amp;#x27;,
  &amp;#x27;июль&amp;#x27;, &amp;#x27;авг&amp;#x27;, &amp;#x27;сент&amp;#x27;, &amp;#x27;окт&amp;#x27;, &amp;#x27;ноя&amp;#x27;, &amp;#x27;дек&amp;#x27;
];
const data = [];

months.forEach(month =&amp;gt; {
  data.push({ month, value: Math.round(Math.random() * 100) / 100 });
}​);&lt;/pre&gt;
  &lt;h2&gt;&lt;strong&gt;Подготовка шкал&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;В прошлом туториале я уже объясняла, что такое линейная шкала и как её сделать. Здесь такая тоже есть — это ось y, она будет определять высоту столбиков. Height тут как и раньше — высота всего графика.&lt;/p&gt;
  &lt;pre&gt;const yDomain = [d3.min(data, d =&amp;gt; d.value), d3.max(data, d =&amp;gt; d.value)];
const yScale = d3.scaleLinear()
  .domain(yDomain)
  .range([height, 0]);&lt;/pre&gt;
  &lt;p&gt;Ось x же не линейная и делается по-другому:&lt;/p&gt;
  &lt;pre&gt;const xScale = d3.scaleBand()
  .domain(months)
  .range([0, width])
  .padding(0.1);&lt;/pre&gt;
  &lt;p&gt;​Это ось групп или диапазонов (bands). В D3 для её создания есть специальный метод d3.scaleBand. Ему передаются диапазоны координат экрана (range, от 0 до ширины графика) и данных (domain, массив всех месяцев). Параметр padding необязательный, он отвечает за промежутки между столбиками.&lt;/p&gt;
  &lt;p&gt;В этой оси каждому значению из данных соответствует некоторый диапазон экранных координат.&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Рисование столбиков&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Рисование шкал я пропущу, потому что оно не зависит от типов этих шкал. Поэтому его можно просто скопировать из предыдущего туториала.&lt;/p&gt;
  &lt;p&gt;Рисование же самого графика похоже на scatter:&lt;/p&gt;
  &lt;pre&gt;svg.selectAll(&amp;#x27;rect&amp;#x27;)
  .data(data)
  .enter()
  .append(&amp;#x27;rect&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, d =&amp;gt; xScale(d.month))
  .attr(&amp;#x27;width&amp;#x27;, xScale.bandwidth())
  .attr(&amp;#x27;y&amp;#x27;, d =&amp;gt; yScale(d.value))
  .attr(&amp;#x27;height&amp;#x27;, d =&amp;gt; height - yScale(d.value))
  .attr(&amp;#x27;fill&amp;#x27;, &amp;#x27;orange&amp;#x27;);&lt;/pre&gt;
  &lt;p&gt;Я создаю объект Selection, выделяя все rect элементы (изначально их нет), и передаю ему данные, используя методы data и enter. Затем, используя метод append, я для каждой записи данных добавляю свой rect элемент.&lt;/p&gt;
  &lt;p&gt;Координаты каждого элемента определяются созданными ранее шкалами xScale и yScale. Ширина находится с помощью специальной функции bandwidth — она вычисляет ширину каждого столбика, зная промежутки и ширину всего графика. Высота вычисляется с помощью шкалы yScale. Хак с &lt;code&gt;height-yScale&lt;/code&gt; нужен, чтобы перевернуть стобики, в системе координат svg они по-умолчанию окажутся сверху вниз.&lt;/p&gt;
  &lt;p&gt;График получается такой:&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/94/66/94666d92-ac9c-429f-984e-b8231d28e255.png&quot; width=&quot;1316&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2&gt;&lt;strong&gt;Несколько серий данных&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Допустим, для каждого месяца у меня не одно, а несколько значений, и я хочу показать их все на одном графике. Сначала я изменю исходные данные, чтобы вместо одного значения value у каждого месяца был бы массив из нескольких значений:&lt;/p&gt;
  &lt;pre&gt;const count = 3;

months.forEach(month =&amp;gt; {
  const values = [];
  
  for (let i = 0; i &amp;lt; count; i++) {
    values.push({
      key: i,
      value: Math.round(Math.random() * 100) / 100,
    });
  }
  data.push({ month, values });
});&lt;/pre&gt;
  &lt;p&gt;Key – это название серии данных. Для простоты я использовала индексы.&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Изменение в шкалах&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Самые важные изменения происходят со шкалами. Раньше у меня по оси x была обычная band шкала, которая определяла, в каком месте и какой ширины рисовать конкретный столбик. Теперь у меня в этом же месте будут три столбика вместо одного. Поэтому появляется ещё одна дополнительная шкала:&lt;/p&gt;
  &lt;pre&gt;const xInnerDomain = data[0].values.keys();
const xInnerScale = d3.scaleBand()
  .domain(xInnerDomain)
  .rangeRound([0, xScale.bandwidth()]);&lt;/pre&gt;
  &lt;p&gt;​Если областью определения (domain) первой шкалы были месяцы, то во второй шкале — это названия серий данных. Так получается, потому что данные вложенные: для каждого месяца есть набор из трёх разных значений. Дополнительная шкала существует в рамках основной и зависит от неё, поэтому её координаты экрана (range) равны диапазону от 0 до ширины любого элемента основной шкалы.&lt;/p&gt;
  &lt;p&gt;Тут я использую метод rangeRound, чтобы при отображении пиксели автоматически округлялись и итоговая картинка получалась чёткой (результат вызова bandwidth может быть нецелым числом).&lt;/p&gt;
  &lt;p&gt;Немного меняется и определение области определения оси y (добавляется вложенность значений):&lt;/p&gt;
  &lt;pre&gt;const yDomain = [
  d3.min(data, d =&amp;gt; d3.min(d.values.map(v =&amp;gt; v.value))),
  d3.max(data, d =&amp;gt; d3.max(d.values.map(v =&amp;gt; v.value)))
];
const yScale = d3.scaleLinear().domain(yDomain).range([height, 0]);&lt;/pre&gt;
  &lt;h2&gt;&lt;strong&gt;Цветовая шкала&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Так как у меня появилось несколько столбиков для каждого месяца, я хочу их как-то различать. Самое простое — это цвет. Я, конечно, могла бы красить их вручную, через css и nth-child, но это очень неуниверсальное решение. Поэтому я хочу сделать шкалу для определения цвета:&lt;/p&gt;
  &lt;pre&gt;​const colorScale = d3.scaleOrdinal(d3.schemeCategory10);&lt;/pre&gt;
  &lt;p&gt;​Метод d3.scaleOrdinal создаёт дискретную шкалу. С её помощью можно определить соответствие одного набора элементов другому. Например, цвета и категории или категории и их последовательность. Отличие от линейной шкалы тут в том, что количество элементов в наборах фиксировано и исчислимо.&lt;/p&gt;
  &lt;p&gt;Шкалу можно определить, вызвав у неё методы range и domain (так я делала раньше), а можно, передав ей массив range в конструкторе. В этом случае domain будет пустым массивом, а его роль будут выполнять индексы массива range.&lt;/p&gt;
  &lt;p&gt;В D3 много вспомогательных методов и объектов, в том числе для создания шкал. Здесь d3.schemeCategory10 — это набор из десяти заготовленных цветов. Это цветовая схема, таких схем в D3 десять штук, вот &lt;a href=&quot;https://github.com/d3/d3-scale-chromatic/blob/90e7c02c6c676d1298dc2cd3d4bda29855fd7232/README.md&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt; их все можно посмотреть.&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Изменения при рисовании&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Я уже писала выше, что данные и шкалы тут можно назвать иерархически вложенными. Эта вложенность влияет и на элементы в html коде. Каждые три столбика для одного месяца я буду группировать с помощью элементов g с классом group:&lt;/p&gt;
  &lt;pre&gt;const barGroup = svg.selectAll(&amp;#x27;.group&amp;#x27;)
  .data(data)
  .enter()
  .append(&amp;#x27;g&amp;#x27;)
  .attr(&amp;#x27;class&amp;#x27;, &amp;#x27;group&amp;#x27;)
  .attr(&amp;#x27;transform&amp;#x27;, d =&amp;gt; &amp;#x60;translate(${xScale(d.month)},0)&amp;#x60;);&lt;/pre&gt;
  &lt;p&gt;Каждый элемент сдвигается по оси x на своё место с помощью аттрибута transform.&lt;/p&gt;
  &lt;p&gt;Внутри каждой группы я буду рисовать по 3 столбика:&lt;/p&gt;
  &lt;pre&gt;barGroup.selectAll(&amp;#x27;rect&amp;#x27;)
  .data(d =&amp;gt; d.values)
  .enter()
  .append(&amp;#x27;rect&amp;#x27;)
  .attr(&amp;#x27;x&amp;#x27;, d =&amp;gt; xInnerScale(d.key))
  .attr(&amp;#x27;width&amp;#x27;, xInnerScale.bandwidth())
  .attr(&amp;#x27;y&amp;#x27;, d =&amp;gt; yScale(d.value))
  .attr(&amp;#x27;height&amp;#x27;, d =&amp;gt; height - yScale(d.value))
  .style(&amp;#x27;fill&amp;#x27;, d =&amp;gt; colorScale(d.key));&lt;/pre&gt;
  &lt;p&gt;В методе data я передаю ​всей группе все данные, а столбикам — только данные, относящиеся к их группе. Каждый столбик использует дополнительную шкалу xInnerScale, чтобы определить свои положение и ширину. Цвет определяется из цветовой шкалы colorScale. Я передаю ей номер столбика и получаю его цвет.&lt;/p&gt;
  &lt;p&gt;И вот итоговый результат:&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/71/33/7133df84-baee-4707-aa4b-30e097bd8dad.png&quot; width=&quot;1306&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2&gt;Ещё раз ссылки&lt;/h2&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;https://codepen.io/gnykka/pen/VwKdaEr&quot; target=&quot;_blank&quot;&gt;Codepen с графиком&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/bar-chart.html&quot; target=&quot;_blank&quot;&gt;Полный код на github&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

</content></entry><entry><id>gnykka:d3-line-scatter</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/d3-line-scatter?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Как сделать LineChart и ScatterChart и в чём разница между методами data() и datum()</title><published>2020-12-17T13:43:28.580Z</published><updated>2021-01-11T11:19:48.166Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://teletype.in/files/4b/a7/4ba7a68a-0da9-4167-a6ff-5b12545f94e8.png"></media:thumbnail><category term="d3js" label="d3js"></category><summary type="html">&lt;img src=&quot;https://teletype.in/files/35/a3/35a35655-7f2f-440e-be77-cf66e7f7c314.png&quot;&gt;Недавно для одного проекта я собирала простые визуализации на D3.js и решила написать небольшой туториал по теме. Я хочу рассказать, как сделать два самых распространённых типа графиков (line и scatter), и объяснить, как работают методы, которые при этом используются.</summary><content type="html">
  &lt;p&gt;Недавно для одного проекта я собирала простые визуализации на D3.js и решила написать небольшой туториал по теме. Я хочу рассказать, как сделать два самых распространённых типа графиков (line и scatter), и объяснить, как работают методы, которые при этом используются.&lt;/p&gt;
  &lt;p&gt;По ссылкам можно найти код графиков:&lt;/p&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/line-chart.html&quot; target=&quot;_blank&quot;&gt;Line chart&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/scatter-chart.html&quot; target=&quot;_blank&quot;&gt;Scatter chart&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2&gt;&lt;strong&gt;Настройка проекта&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Начну с подготовки. Обычно я люблю разделять код, стили и вёрстку. Это удобно, когда проект большой и надо в нём ориентироваться. Но с небольшими одностраничными примерами я буду держать всё в одном файле вёрстки: index.html.&lt;/p&gt;
  &lt;p&gt;Его структура выглядит так:&lt;/p&gt;
  &lt;pre&gt;&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;style&amp;gt;&amp;lt;/style&amp;gt;
    &amp;lt;script src=&amp;quot;https://d3js.org/d3.v6.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;svg&amp;gt;&amp;lt;/svg&amp;gt;
    &amp;lt;script&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/pre&gt;
  &lt;p&gt;Здесь тег style — место для всех стилей (их практически не будет), тег svg — контейнер для графика, а пустой тег script — место для моего JS кода.&lt;/p&gt;
  &lt;p&gt;Я помещаю свой скрипт в конце документа (после скрипта D3 и svg элемента) потому что мне нужно, чтобы он загрузился самым последним. Тогда я смогу использовать глобальную переменную d3, которая станет доступна в JS коде, и все элементы вёрстки. Обычно скрипты и элементы в вёрстке грузятся синхронно и последовательно.&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Подготовка данных&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Общая структура готова, но прежде чем начать рисовать график, мне нужны данные. Я хочу сделать простой пример, поэтому данные я сгенерирую случайным образом.&lt;/p&gt;
  &lt;p&gt;Внутри тега script я добавлю следующий код:&lt;/p&gt;
  &lt;pre&gt;const N = 20;
const data = [];

for (let i = 0; i &amp;lt; N; i++) {
  data.push({ x: i, y: Math.round(Math.random() * N) });
}​&lt;/pre&gt;
  &lt;p&gt;Здесь константа N определяет размер массива данных или число точек будущих графиков. Я люблю задавать это число параметром, а не писать его явно в коде, потому что так проще будет что-то изменить, если потребуется. Data — это массив, который мы заполняем точками с координатами x (просто порядковый номер точки) и y (случайное целое число от 0 до N).&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Подготовка SVG&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Когда данные есть, можно начинать рисовать график. Сначала я определю параметры графика:&lt;/p&gt;
  &lt;pre&gt;const width = 600;
const height = 400;
​const margin = { top: 25, right: 25, bottom: 25, left: 25 };&lt;/pre&gt;
  &lt;p&gt;​Размеры необязательно указывать в явном виде значениями, можно вычислить их из вёрстки или css. Но чтобы сделать проще, я задам их явно. Margin — это внутренние отступы графика, они будут нужны для осей.&lt;/p&gt;
  &lt;p&gt;После я подготовлю svg контейнер. Как видно в html вёрстке выше, он пустой и не стилизованный. Я хочу указать ему выбранные размеры и создать внутри тег g, в котором уже буду что-то рисовать. Созданный элемент g я сдвину на величину отступов.&lt;/p&gt;
  &lt;pre&gt;const svg = d3.select(&amp;#x27;svg&amp;#x27;)
  .attr(&amp;#x27;width&amp;#x27;, width + margin.left + margin.right)
  .attr(&amp;#x27;height&amp;#x27;, height + margin.top + margin.bottom)
  .append(&amp;#x27;g&amp;#x27;)
  .attr(&amp;#x27;transform&amp;#x27;, &amp;#x60;translate(${margin.left},${margin.top})&amp;#x60;);&lt;/pre&gt;
  &lt;p&gt;G используется для группировки элементов. Чаще всего он нужен для структурирования графики и для трансформаций — любая трансформация всей группы применяется ко всем её элементам.&lt;/p&gt;
  &lt;p&gt;&lt;em&gt;Если вам захочется лучше разобраться в том, как устроена векторная графика, то у Мозиллы есть &lt;a href=&quot;https://developer.mozilla.org/ru/docs/Web/SVG/Tutorial&quot; target=&quot;_blank&quot;&gt;хороший и полный гайд&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Подготовка шкал&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Чтобы нарисовать оси и сам график, мне нужны шкалы. Они помогут в работе с масштабом и правильным преобразованием значений данных в точки на экране. &lt;/p&gt;
  &lt;p&gt;Чтобы лучше понимать задачу преобразования, можно представить, что у вас есть диапазон значений данных от 0 до 2 и диапазон экранных координат от 0 до 100. Если вы линейно проецируете один диапазон на другой, то нули будут соответствовать друг другу, 2 будет соответствовать 100, а 0.5 — 25.&lt;/p&gt;
  &lt;figure class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/35/a3/35a35655-7f2f-440e-be77-cf66e7f7c314.png&quot; width=&quot;563.9999999999999&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Можно даже записать общую формулу перевода значения value из диапазона r1 в диапазон r2:&lt;/p&gt;
  &lt;pre&gt;r2.min + ((value - r1.min) / (r1.max - r1.min)) * (r2.max - r2.min)&lt;/pre&gt;
  &lt;p&gt;Для создания шкал я буду использовать метод d3.scaleLinear(). Ему нужно передать два диапазона (данных и координат экрана), а он будет потом выполнять все преобразования.&lt;/p&gt;
  &lt;p&gt;Но сначала я найду диапазоны значений из данных. Я использую методы d3.min и d3.max, чтобы найти минимальное и максимальное значение, но можно делать это любым другим способом.&lt;/p&gt;
  &lt;pre&gt;const xDomain = [d3.min(data, d =&amp;gt; d.x), d3.max(data, d =&amp;gt; d.x)];
const yDomain = [d3.min(data, d =&amp;gt; d.y), d3.max(data, d =&amp;gt; d.y)];&lt;/pre&gt;
  &lt;p&gt;Затем создам шкалы для осей x и y:&lt;/p&gt;
  &lt;pre&gt;​const xScale = d3.scaleLinear().domain(xDomain).range([0, width]);
const yScale = d3.scaleLinear().domain(yDomain).range([height, 0]);​​&lt;/pre&gt;
  &lt;p&gt;У получившихся шкал domain — диапазон значений данных, а range — диапазон координат экрана. Координаты экрана мне известны — это размены графика. Ось y перевёрнута, потому что начало координат в системе svg находится в левом верхнем углу.&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Рисование осей&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Теперь, когда шкалы готовы, можно добавить оси:&lt;/p&gt;
  &lt;pre&gt;svg.append(&amp;#x27;g&amp;#x27;)
  .attr(&amp;#x27;transform&amp;#x27;, &amp;#x60;translate(0,${height})&amp;#x60;)
  .call(d3.axisBottom(xScale));

svg.append(&amp;#x27;g&amp;#x27;)
  .call(d3.axisLeft(yScale));&lt;/pre&gt;
  &lt;p&gt;Ось x у меня будет снизу, а ось y слева. Горизонтальную ось мне нужно сдвинуть на высоту основной части графика, для этого я укажу ей атрибут transform.&lt;/p&gt;
  &lt;p&gt;Call обычно используется в JS, чтобы вызвать метод, подставив ему контекст выполнения. Здесь метод call — это метод объекта класса Selection. Он вызывает метод d3.axisBottom (или d3.axisLeft), передавая ему себя в качестве одного из параметров. А метод d3.axisBottom уже сам рисует внутри ось.&lt;/p&gt;
  &lt;h2&gt;&lt;strong&gt;Рисование линии&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Теперь всё готово для самой линии графика. Чтобы её нарисовать, мне нужна функция, которая будет конструировать строку команд для path элемента из значений данных, используя созданные ранее шкалы. Я создам её с помощью метода d3.line:&lt;/p&gt;
  &lt;pre&gt;const line = d3.line()
  .x(d =&amp;gt; xScale(d.x))
  .y(d =&amp;gt; yScale(d.y));&lt;/pre&gt;
  &lt;p&gt;И вот наконец можно добавить path элемент, который будет отображать сами данные:&lt;/p&gt;
  &lt;pre&gt;svg.append(&amp;#x27;path&amp;#x27;)
  .datum(data)
  .attr(&amp;#x27;d&amp;#x27;, line);&lt;/pre&gt;
  &lt;p&gt;Данные я передаю, вызвав метод datum (про него я ещё расскажу), а атрибуту d присвою созданную функцию line.&lt;/p&gt;
  &lt;p&gt;Теперь, казалось бы, всё готово. Но если открыть страницу, то графика на ней не окажется. Это потому что я не добавила стили. Оси меня устраивают как есть, а вот линия пусть будет чёрная и в 1 пиксель толщиной. Для этого внутри тега style я напишу:&lt;/p&gt;
  &lt;pre&gt;​path {
  fill: none;
  stroke: black;
  stroke-width: 1px;
}&lt;/pre&gt;
  &lt;p&gt;​И вот сейчас если открыть страницу, то можно увидеть вот такой график:&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/71/f1/71f1d6cd-546c-4b4b-be54-90f1e123f16c.png&quot; width=&quot;1312&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2&gt;&lt;strong&gt;Рисование кружочков&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;Допустим, что я хочу сделать не LineChart, а ScatterChart. То есть не соединять точки линией, а показать их как скопление кружочков. Для этого я могу переиспользовать весь код до последней части с рисованием линии.&lt;/p&gt;
  &lt;p&gt;Мне уже не нужна никакая d3.line, вместо неё я возьму свой массив точек и для каждой нарисую кружочек.&lt;/p&gt;
  &lt;pre&gt;svg.selectAll(&amp;#x27;circle&amp;#x27;)
  .data(data)
  .enter()
  .append(&amp;#x27;circle&amp;#x27;)
  .attr(&amp;#x27;cx&amp;#x27;, d =&amp;gt; xScale(d.x))
  .attr(&amp;#x27;cy&amp;#x27;, d =&amp;gt; yScale(d.y));&lt;/pre&gt;
  &lt;p&gt;И опять же добавлю в стили:&lt;/p&gt;
  &lt;pre&gt;​circle {
  r: 5;
  fill: black;
}&lt;/pre&gt;
  &lt;p&gt;И получу такой график (я рисовала графики отдельно, поэтому данные сгенерировались другие):&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/9e/f1/9ef17ce3-f257-43fe-a094-1967f7c21bd5.png&quot; width=&quot;1328&quot; /&gt;
  &lt;/figure&gt;
  &lt;h2&gt;&lt;strong&gt;Как передавать данные графикам&lt;/strong&gt;&lt;/h2&gt;
  &lt;p&gt;А теперь самое интересное — детали и нюансы реализации. Вы заметили, что когда я делала линию, я передавала данные с помощью метода datum, а когда делала кружочки — с помощью data и enter? При этом, если попробовать сделать по-другому, то либо ничего не нарисуется, либо возникнут ошибки.&lt;/p&gt;
  &lt;p&gt;Все эти три метода — методы класса Selection. Их код можно найти в репозитории &lt;a href=&quot;https://github.com/d3/d3-selection/tree/a042a10ce223e6be18a077c224b6e034a1927b5f/src/selection&quot; target=&quot;_blank&quot;&gt;d3-selection&lt;/a&gt;.&lt;/p&gt;
  &lt;p&gt;Вопрос «что такое Selection» — тема для отдельного рассказа. Если коротко, это специальная сущность, которая нужна D3 для работы с DOM элементами. Selection создаётся, когда мы делаем d3.select и модифицируется всеми остальными вызовами.&lt;/p&gt;
  &lt;p&gt;Метод datum очень простой и маленький. Он берёт выбранный через d3.select элемент и добавляет ему атрибут &lt;em&gt;__data__&lt;/em&gt; с переданными данными. Затем при добавлении любого следующего атрибута, если его значение — функция, она получит __&lt;em&gt;data__&lt;/em&gt; массив как один из своих параметров. Так и происходит с методом line: он берёт данные из этого атрибута, преобразовывает их в координаты и рисует по ним линию.&lt;/p&gt;
  &lt;p&gt;Метод data работает по-другому. Когда я передаю ему массив с данными, он анализирует их, определяет, сколько будет добавлено элементов и какая строка данных какому элементу должна соответствовать. Вызванный следом метод enter создаёт из этого новый объект Selection. Любой метод вызванный после этого уже будет работать с целым массивом элементов. Так работает указание атрибутов cx и cy — я пишу всего одну строчку, а вызывается она много раз, для каждого добавляемого кружочка на экране.&lt;/p&gt;
  &lt;h2&gt;Ссылки&lt;/h2&gt;
  &lt;ol&gt;
    &lt;li&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/line-chart.html&quot; target=&quot;_blank&quot;&gt;Код line chart&lt;/a&gt; &lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://gnykka.github.io/d3-tutorials/line-chart.html&quot; target=&quot;_blank&quot;&gt;Результат line chart&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://github.com/gnykka/gnykka.github.io/blob/master/d3-tutorials/scatter-chart.html&quot; target=&quot;_blank&quot;&gt;Код scatter chart&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href=&quot;https://gnykka.github.io/d3-tutorials/scatter-chart.html&quot; target=&quot;_blank&quot;&gt;Результат scatter chart&lt;/a&gt;&lt;/li&gt;
  &lt;/ol&gt;

</content></entry><entry><id>gnykka:mazes</id><link rel="alternate" type="text/html" href="https://teletype.in/@gnykka/mazes?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=gnykka"></link><title>Алгоритмы для создания лабиринтов</title><published>2020-11-23T14:47:05.703Z</published><updated>2020-11-23T14:56:16.539Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://teletype.in/files/bd/95/bd9530c5-1b4e-4f57-86c1-0bc2413354b8.png"></media:thumbnail><summary type="html">&lt;img src=&quot;https://teletype.in/files/a4/5e/a45efbcb-272b-43fa-a9e2-330415c3020b.png&quot;&gt;Это обзор алгоритмов, рассматриваемых в книге Mazes for Programmers.
Мой код алгоритмов на языке Ruby можно найти на Github.</summary><content type="html">
  &lt;p&gt;Это обзор алгоритмов, рассматриваемых в книге &lt;a href=&quot;https://www.amazon.com/Mazes-Programmers-Twisty-Little-Passages/dp/1680500554&quot; target=&quot;_blank&quot;&gt;Mazes for Programmers&lt;/a&gt;.&lt;br /&gt;Мой код алгоритмов на языке Ruby можно найти &lt;a href=&quot;https://github.com/gnykka/mazes-for-programmers&quot; target=&quot;_blank&quot;&gt;на Github&lt;/a&gt;.&lt;/p&gt;
  &lt;h2&gt;Простые алгоритмы&lt;/h2&gt;
  &lt;h3&gt;Binary Tree&lt;/h3&gt;
  &lt;p&gt;Для каждой ячейки сетки по очереди (с юга на север и с запада на восток) строится путь в соседнюю ячейку восточнее или севернее.&lt;/p&gt;
  &lt;figure class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/a4/5e/a45efbcb-272b-43fa-a9e2-330415c3020b.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Характерна диагональная структура, тянущаяся из юго-запада в северо-восток. Крайние северная строка и восточная колонка всегда оказываются коридорами без стен.&lt;/p&gt;
  &lt;h3&gt;Sidewinder&lt;/h3&gt;
  &lt;p&gt;Ячейки каждой строки случайным образом объединяются, а затем из каждого объединения ячеек прокладывается путь в любую из соседних северных. Повторяется это для каждой строки с юга на север, последняя строка становится сплошным коридором.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/96/50/965071f8-a3a0-4ecf-9d1d-367a84770e27.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Получается вертикальная структура с коридором в северной строке.&lt;/p&gt;
  &lt;h2&gt;Алгоритмы для максимально случайных лабиринтов&lt;/h2&gt;
  &lt;h3&gt;Aldous-Broder&lt;/h3&gt;
  &lt;p&gt;Начиная из произвольной ячейки сетки, мы перемещаемся между ячейками случайным образом. Когда попадаем в непосещённую ранее ячейку, прокладывает туда путь. &lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/11/b2/11b2260d-1f9b-49cd-83a7-1bd4d758eed7.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Начало алгоритма быстрое, но в конце он может быть очень медленным. Какой-то особенной структуры или искажений нет, всегда получаются идеальные лабиринты.&lt;/p&gt;
  &lt;h3&gt;Wilson’s&lt;/h3&gt;
  &lt;p&gt;Выбираем произвольную ячейку и добавляем её в лабиринт. Потом из любой другой ячейки строим случайный путь. Когда путь упирается в ячейку, уже добавленную в лабиринт, весь путь добавляется в лабиринт. Создаём такие случайные пути пока все ячейки не окажутся в лабиринте.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/15/bf/15bf221e-4218-4cae-86f1-bc97a5f730ab.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Вначале это алгоритм медленный, но ускоряется с каждой добавляемой ячейкой. Нет отличительной структуры или особенностей, потому что генерирует полностью случайный лабиринт.&lt;/p&gt;
  &lt;h2&gt;Алгоритмы для лабиринтов с длинными путями&lt;/h2&gt;
  &lt;h3&gt;Hunt-and-Kill&lt;/h3&gt;
  &lt;p&gt;Из произвольной ячейки случайным образом двигаемся по сетке, избегая уже посещённые ячейки. Если двигаться оказывается некуда, то находим любую новую непосещённую ячейку рядом с уже посещённой. Если такая нашлась, то соединяем их и продолжаем строить путь. Заканчиваем, когда непосещённых ячеек не остаётся.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/06/1f/061fc1dd-d958-4f05-acb6-d81713655bca.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Создаёт длинные и извилистые коридоры с небольшим количеством тупиков. Это потенциально медленный алгоритм, потому что проверяет одни и те же ячейки много раз.&lt;/p&gt;
  &lt;h3&gt;Recursive Backtracker&lt;/h3&gt;
  &lt;p&gt;Из произвольно выбранной ячейки начинаем строить путь, избегая ранее посещённые ячейки. Когда двигаться некуда, нужно вернуться к предыдущей посещённой ячейке и продолжить строить путь оттуда. Алгоритм завершается, когда мы возвращаемся к той ячейке, с которой начали.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/21/ac/21ac0893-cc2d-4eca-83b9-a2b9853a887a.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;В результате получаются длинные и извилистые пути с небольшим количеством тупиков. Этот алгоритм похож на Hunt-and-Kill, но потенциально работает быстрее, потому что проверяет каждую ячейку только два раза. Хотя ему нужно больше памяти, чтобы следить за уже посещёнными ячейками.&lt;/p&gt;
  &lt;h2&gt;Дополнительные алгоритмы&lt;/h2&gt;
  &lt;h3&gt;Kruskal’s&lt;/h3&gt;
  &lt;p&gt;Начинается с добавления каждой ячейки в отдельное множество. Потом случайным образом соединяются соседние ячейки, если они принадлежат разным множествам. Таким образом множества объединяются, пока не останется только одно.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/87/d0/87d0a88f-ce4b-42fd-9b0b-627031175244.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;В результате получаются очень аккуратные и регулярные лабиринты.&lt;/p&gt;
  &lt;h3&gt;Prim’s (упрощённый)&lt;/h3&gt;
  &lt;p&gt;Создаётся множество с произвольной ячейкой. Каждый раз из этого множества выбирается случайная ячейка. Если у неё нет непосещённых соседей, то она удаляется из множества. Иначе выбирается любой непосещённый сосед, ячейки соединяются, выбранный сосед добавляется в множество. Продолжаем, пока множество не станет пустым.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/05/7a/057a2458-1f32-4330-ad96-02b55a8870ae.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Отличается радиальной структурой вокруг начальной ячейки. Обычно создаются больше тупиков и получаются более короткие пути.&lt;/p&gt;
  &lt;h3&gt;Prim&amp;#x27;s (настоящий)&lt;/h3&gt;
  &lt;p&gt;Сначала каждой ячейке назначается случайный вес, затем создаётся множество со случайной ячейкой. На каждом шаге из множества выбирается ячейка наибольшего веса. Ячейка удаляется, если у неё нет непосещённых соседей. Иначе она соединяется с любым из соседей, а этот сосед добавляется в множество. Продолжаем, пока в множестве есть ячейки.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/00/b2/00b29bf5-28fc-4f7d-b40e-53c4599b42cc.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Очень много тупиков с довольно короткими путями. По текстуре оказывается похож на пазл.&lt;/p&gt;
  &lt;h3&gt;Growing Tree&lt;/h3&gt;
  &lt;p&gt;Это обобщение алгоритма Прима (Prim&amp;#x27;s). Из сетки выбирается произвольная ячейка и добавляется в некоторое множество. Потом из множества выбирается любая ячейка (сначала она там одна, потом будет много). Если у выбранной ячейки нет непосещённых соседей, то она удаляется из множества. Иначе ячейка соединяется с любым из непосещённых соседей, этот сосед добавляется в множество. Шаги повторяются, пока в множестве остаются ячейки.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/25/0a/250ae799-5350-4613-a117-073a973b55f9.png&quot; width=&quot;653&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Сильно зависит от способа выбора следующей ячейки из множества. Можно выбирать случайно, можно брать последнюю, можно комбинировать подходы или вводить веса. Итоговые лабиринты могут получиться как ярко выраженной радиальной структуры, так и очень извилистой.&lt;/p&gt;
  &lt;h3&gt;Eller&amp;#x27;s&lt;/h3&gt;
  &lt;p&gt;Каждая строка сетки обходится последовательно (например, с севера на юг), образовывая отдельные множества ячеек. Для каждого такого множества прокладывается путь на юг в одну из смежных ячеек, эта ячейка становится частью этого множества. В последней строке расположенные рядом множества объединяются.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/ab/85/ab856676-9dd3-4765-ab5a-caa3b136468f.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;Последняя строка обычно имеет меньше стен, потому что в ней происходит объединение множеств.&lt;/p&gt;
  &lt;h3&gt;Recursive Division&lt;/h3&gt;
  &lt;p&gt;Это обратный алгоритм. Начинаем с сеткой без стен между ячейками. Делим сетку на две части стеной, добавляя проход в случайном месте. Повторяем такое разделение до тех пор, пока не останется пустых пространств.&lt;/p&gt;
  &lt;figure class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://teletype.in/files/77/ca/77ca9610-e134-4b7f-941f-bfd6b8e0eed4.png&quot; width=&quot;442&quot; /&gt;
  &lt;/figure&gt;
  &lt;p&gt;В результате получается прямоугольная коробочная структура. Решение обычно легче находить через поиск узких горлышек — переходом между большими областями. Одно из преимуществ: можно создавать большие пустые комнаты.&lt;/p&gt;

</content></entry></feed>