Разное

D3 js примеры: Просто о D3.js / Хабр

09.11.2020

Введение в D3 / Хабр


D3.js (или просто D3) это JavaScript-библиотека для обработки и визуализации данных. Она предоставляет удобные утилиты для обработки и загрузки массивов данных и создания DOM-элементов. Эта заметка описывает работу с основными методами библиотеки, она подойдёт для изучения основ библиотеки и погружения в её логику и возможности.

Для понимания статьи пригодятся знания JS, HTML и CSS.



Текучий интерфейс (fluent interface)

D3 реализует подход, называемый fluent interface. При чтении кода он выглядит как цепочка методов. Каждый метод вызывается на объекте, который вернул предыдущий метод. Чтобы код было удобно читать, каждый вызов располагается на отдельной строчке:

d3.select('body') // выбор в документе body
    .append('svg') // добавление в body svg-контейнера
    .append('text') // добавление в svg-контейнер элемента text
    .text('Click somewhere, please...') // изменение текста в элементе text
    . attr('x', 50) // задание координат элемента text
    .attr('y', 50)
    .style("fill","firebrick") // заливка текста цветом

Этот пример на jsfiddle.net

Выборка

В D3, как и в других JS-библиотеках, работающих с DOM-элементами, взаимодействие с документом начинается с поиска элементов в документе и создания выборки — обёртки набора элементов. Она даёт доступ к методам библиотеки для модификации выбранных элементов.

Выборка (selection) в D3 создается с помощью методов d3.select() и d3.selectAll(). Для создания выборки D3 использует querySelector/querySelectorAll или Sizzle, если он подключён к странице (например, с jQuery).

d3.select('span') // выбор первого span в документе
d3.selectAll('span') // выбор всех span в документе

Полученную выборку используют для работы с элементами и для создания выборки из потомков (subselection).

<span>будет зеленым</span>
<p>
    <span>будет красным</span>
    <span>эти</span>
    <span>будут</span>
    <span>жёлтыми</span>
</p>
<p>останется чёрным</p>
d3. select('span')                  // выбор первого span в документе
    .style('color', 'darkgreen') // установка цвета
d3.selectAll('p')                   // выбор всех параграфов
    .selectAll('span')             // выбор всех span в этих параграфах.
    .style('color', 'goldenrod') // установка цвета
d3.select('p')                      // выбор первого параграфа в документе
    .select('span')                // выбор первого span в этом параграфе
    .style('color', 'firebrick')  // установка цвета

Этот пример на jsfiddle.net

Всегда помните, с какой выборкой вы сейчас работаете. Распространённые ошибки при работе с D3: вызов на элементе-потомке вместо родителя и попытка изменения свойств несуществующего (удаленного или ещё не созданного) элемента.

В примере уже используются операции над элементами (selection.style(name[, value])), дальше рассмотрим их более подробно.

Вычисление значений и функторы

Для работы с DOM D3 использует схожий API для всех вызовов. Давайте разберём его на примере популярной задачи: добавления или удаления класса у элемента. Для этого нам понадобятся некоторые методы выборок:

  • selection.classed(name, value) добавляет или удаляет класс name в зависимости от булевого значения value.
  • selection.on(event, callback) используется для обработки событий, передавая название события event (например, «click») и функцию-обработчик callback. Функции-обработчики вызываются с текущим элементом в this, а также data и index в аргументах. Событие можно получить в переменной d3.event. Повторная установка обработчика заменит предыдущий.
var pressed = false
var button = d3.select('button')     // выбор кнопки
    .on('click', function (data, index) {        // установка обработчика нажатия мыши
        button.classed('pressed', pressed = !pressed)  // в обработчике меняем значение переменной и вычисляем класс
    })

Этот пример на jsfiddle.net

Обратите внимание, что мы пользуемся сохранённой в переменную button выборкой: вызовы on (а так же classed, attr, style, property, html, text) возвращают выборку, на которой они вызваны, что типично для «текучих» интерфейсов.

D3 обрабатывает переданные значения схожим образом. Если вы видите в документации [value], скорее всего речь идёт о следующем:

  • Если вы подадите значение, являющееся функцией, оно будет вызвано с параметрами data, index (см. ниже), а контекстом (объектом this) будет элемент, DOM-узел.
  • Если вы подадите значение, не являющееся функцией, оно будет обёрнуто в «функтор» (функцию, всегда возвращающую переданное значение)
  • Если вы не подадите значение, функция сработает как getter и вернёт значение, о котором идёт речь (например, selection.style(‘color’) вернёт цвет текста, если он установлен для элемента).

О последнем нюансе важно помнить, если вы строите цепочку вызовов (такой getter обычно должен быть последним элементом цепочки).

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

Обратите внимание на аргументы функции (data и index). Они имеют специальное значение: index — номер элемента в выборке, а data — заданный для него элемент данных. Присутствие этих параметров в каждой функции, вызываемой на выборке является одним из важнейших контрактов в D3. Это позволяет писать лаконичный код, вычисляющий состояния свойств элементов в зависимости от данных.

Типичная работа с выборкой

Рассмотрим популярные методы на более сложном примере, демонстрирующем работу с DOM-узлами документа через выборку:

var svg = d3.select('body').append('svg')
svg
    .append('text')
    .text('click somewhere')
    .attr('x', 50)
    .attr('y', 50)
var events = []
svg.on('click', function () {
    events.push(d3.event)
    if (events.length > 5) events.shift()
    var circles = svg.selectAll('circle')
        .data(events, function (e) { return e.timeStamp })
        .attr('fill', 'gray')
    circles
        .enter()
        . append('circle')
        .attr('cx', function (d) { return d.x || d.pageX })
        .attr('cy', function (d) { return d.y || d.pageY })
        .attr('fill', 'red')
        .attr('r', 10)
    circles
        .exit()
        .remove()
})

Этот пример на jsfiddle.net

  • Методы selection.html() и selection.text() задают или возвращают содержимое элементов в виде HTML или текста.
  • Методы selection.style(), selection.attr() и selection.property() задают или возвращают CSS-свойства элемента, его аттрибуты и свойства. Чаще всего мы будем пользоваться style и attr, особенно при описании свойств новых элементов.
  • Метод selection.remove() удаляет элементы текущей выборки.
  • selection.append() добавляет потомка к каждому элементу текущей выборки.
  • Переданные в selection.data() данные сохраняются в поле __data__ DOM-элемента, при вызове методов на выборке происходит их извлечение из элемента.
  • Получить или записать данные в один элемент можно, используя selection. datum().
Связанные множества

В примере особое внимание стоит обратить на метод data(). В отличие от других методов он возвращает модифицированную выборку, хранящую помимо списка элементов соответствие данных элементам. В переводе статьи Thinking with Joins мы подробно рассказываем о методах enter() и exit() которые есть у такой выборки и возможностях которые они дают.

Анимация и настройка

Анимировать изменение свойств элемента в D3 легко, нужно вызвать метод selection.transition(). Этот метод возвращает выборку, которая постепенно изменяет текущие значения на новые, создавая анимационный эффект. Длительность анимации задаётся методом transition.duration().

Добавим анимацию при добавлении и удалении элементов в предыдущий пример:

var svg = d3.select('body').append('svg')
svg
    .append('text')
    .text('click here')
    .attr('x', 50)
    .attr('y', 50)
var events = []
svg. on('click', function () {
    events.push(d3.event)
    if (events.length > 5) events.shift()
    var circles = svg.selectAll('circle')
        .data(events, function (e) { return e.timeStamp })
        .attr('fill', 'gray')
    circles
        .enter()
        .append('circle')
        .attr('cx', function (d) { return d.x || d.pageX })
        .attr('cy', function (d) { return d.y || d.pageY })
        .attr('fill', 'red')
        .attr('r', 0) // Начальное значение
        .transition()
        .duration(1000) // Длительность перехода от начального значения к конечному
        .attr('r', 10) // Конечное значение
    circles
        .exit()
        .transition()
        .attr('r', 0)
        .remove()
})

Этот пример на jsfiddle.net

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

Я преподаю D3 на курсе «Визуализация данных». Если вы хотите освоить этот инструмент и начать применять его в своей работе, приходите к нам. Ближайший курс пройдёт в Москве в эти выходные, запись и отзывы участников январского курса: http://brainwashing.pro/dataviz.

Быстро о главном: визуализация с D3.js | by Jenny V | NOP::Nuances of Programming

D3, сокращенно от Data-Driven Documents, — это библиотека JavaScript, которая позволяет отображать данные самыми разными способами и содействует проверке и управлению элементами DOM.

D3 не относится к разряду типичных библиотек для построения графиков и диаграмм. Она обладает намного большей гибкостью и усилена возможностями Recharts, C3js и Raw graphs. Ее преимущество очевидно: там, где обычная библиотека терпит фиаско, когда вы слишком увлекаетесь индивидуальными настройками, D3 продолжает уверенно работать

В статье мы познакомимся с D3 на основе конкретных примеров. Общий процесс сводится к следующим этапам: загрузка данных — выборка элементов — привязка данных — создание/изменение/ удаление элементов.

Но прежде немного порисуем на веб-странице и попрактикуемся в применении селекторов и фигур разных цветов.

Рисование базовых фигур

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

Сначала для импорта D3 создаем HTML-документ, с который мы будем работать на протяжении всей статьи. Редактироваться будет только код между выделенными тегами скрипта.

В строке 4 импортируем D3. Если у вас frontend-фреймворк с Node.js, то вы можете использовать NPM. В строке 7 определяем элемент SVG, в котором будем рисовать фигуры.

Напишем первые строки кода D3:

В строке 2 задействуем метод d3.select(). Он будет искать первый элемент с CSS-селектором, который вы передали в функцию. В рассматриваемом примере на странице только один тег <svg>, поэтому он вернет единственно имеющийся элемент SVG.

Позже обратимся к другому методу, а именно d3.selectAll(), который вернет не один, а все элементы, соответствующие CSS-селектору.

В строке 4 к элементу SVG добавляем “circle” (круг). Помимо него в нашем распоряжении есть еще множество разных фигур. В строке 5 применяем цепочку методов D3, что позволяет поочередно вызывать несколько функций и избавляет от необходимости писать svg.function(...) в каждой отдельной строке.

В строках 5, 6, 7 и 8 устанавливаем атрибуты с помощью функции d3.attr, которые потребуются для визуализации фигуры. cx и cy — координаты x и y центра круга; r — радиус; fill — цвет заливки. Это фиксированные атрибуты. С примерами их использования для основных геометрических форм вы можете ознакомиться на freeCopeCamp.

Итак, в браузере увидим следующий результат:

Кто-то может подумать, что здесь нет ничего особенного. Однако позволю себе не согласиться. Даже самые сложные графические изображения складываются из нескольких простых фигур.

Теперь дополним изображение квадратом:

Добавляем фигуру rect и устанавливаем нужные атрибуты для ее отображения. Затем выполняем код и получаем следующий результат:

С основными принципами добавления фигур разобрались, переходим к вопросу загрузки данных и графическим изображениям на их основе.

Загрузка данных

Для получения разных типов данных в D3 существует множество функций. Начнем с самых интересных: d3.json('...'), d3.csv('...') и d3.xml('...').

Для вышеуказанной цели все из них предусматривают применение Fetch. CSV, в отличие от JSON и XML, не допускает вложенных объектов.

В последующих разделах мы будем работать с файлом данных (socialnetwork. json) из вымышленной социальной сети и стилями, представленными ниже:

Сначала построим гистограмму для библиотеки. По горизонтали отобразим годы (years), а по вертикали — количество комментариев (comments). Для этой цели воспользуемся соответственно d3.axisBottom() и d3.axisLeft(). В коде загрузка данных и рисование осей происходят следующим образом:

Как много кода! Без паники, рассмотрим каждую строку по отдельности.

В строке 3 получаем JSON. Поскольку d3.json() возвращает промис, нам не обойтись без .then() или await, чтобы дождаться окончания процедуры. Как только это происходит, в строках 5 и 10 приступаем к созданию функций масштабирования.

Функции масштабирования

Функции масштабирования играют важную роль в отрисовке данных. Допустим, мы рассматриваем временной промежуток с 2015 по 2020 год при разрешении 500 х 500 пикселей. При отрисовке номера года на оси x необходимо сопоставить 2015–2020 в 0–500.

Самое время для функций масштабирования. Одна из них, .scaleLinear(), линейно преобразует один диапазон чисел (domain) в другой (range). В строке 7 мы передаем этой функции диапазон (range)[100,800], поскольку нам нужно начать отрисовку с 100-го пикселя по оси x, а закончить на 800-м.

После этого происходит передача диапазона входных значений (domain), которые надлежит преобразовать. .domain() ожидает массив с минимальным и максимальным значениями. К счастью, у D3 есть функции d3.extent(), d3.max() и d3.min(). В строке 8 устанавливаем минимальное значение minimum of years — 1 и максимальное maximum of years + 1. Это позволит в дальнейшем отцентрировать столбцы гистограммы.

В строке 13 делаем немного по-другому, поскольку ось должна начинаться с 0, а заканчиваться максимальным числом комментариев. Наша задача — отобразить диапазон от 0 до 150, а не от 50 до 150.

d3. extent() вернет массив из максимального и минимального значений [max, min], избавляя от необходимости вызывать их по отдельности.

Приступаем к рисованию оси: в строке 15 добавляем ось x. В целях стилизации присваиваем ей класс и затем перемещаем элемент g, поскольку ось x всегда рисуется из начала координат, расположенного в верхнем левом углу. Поэтому сдвигаем g на горизонталь, после чего выполняем .call(d3.axisBottom(x).tickFormat(d3.format("d")). Функция .axisBottom рисует ось. (Обратите внимание, что мы передали ей функцию range, поэтому она знает, в каком диапазоне range/domain нужно выполняться). Затем дополнительная функция .tickFormat() сообщает D3, как отображать деления на оси. В данном случае используем "d" для обозначения простых чисел в интервале от 2014 до 2021.

Далее практически то же самое мы делаем для отрисовки оси y, только меняем атрибут transform, поскольку необходимо переместить эту ось горизонтально для отображения делений.

Полученный результат:

Выборка элементов и привязка данных

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

Сначала воспользуемся функцией d3.select() для получения SVG. (Обратите внимание, что в предыдущих фрагментах кода мы ее сохранили в переменной svg). Затем с помощью селектора CSS selector .barchartrectangle проводим поиск и отбираем все существующие прямоугольники гистограммы. Это связано с процессами обновления: если массив изменится, мы будем выбирать имеющиеся прямоугольники для их корректировки либо удаления.

Далее вызываем функцию .data(), которая содержит один обязательный аргумент, представляющий data, которые нужно отрисовать. При этом второй аргумент является необязательным и определяет ключ. Для этого мы передаем ему функцию, принимающую элемент (item) и индекс (index). В данном примере возвращается индекс.

Однако, как мы увидим в следующем разделе, возможно применение поля элемента для выявления тех элементов, которые подлежат изменению/удалению/вставке. При наличии свойства id в датасете можно написать что-то подобное (item, index)=> item.id.

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

Создание, изменение и удаление элементов

Когда происходит изменение данных в массиве (например, путем фильтрации), D3 сравнивает массивы и решает, что делать.

Здесь мы рассмотрим только 2 наиболее важные функции: добавление .enter() и удаление .exit().

Понять логику поможет следующий код:

Рассмотрим использование этого массива в качестве данных [1,2,3] при первом выполнении кода.

Селектор .barchartrectangle вернет 0 элементов, поэтому D3 в курсе, что элементы в массиве являются тремя новыми фрагментами данных. Следовательно, она задействует функции, измененные на инструкцию .enter(). В ней мы присоединяем новые rect каждому новому фрагменту данных и присваиваем им класс barchartrectangle. Поскольку “удалений” нет, инструкция .exit() будет проигнорирована.

Теперь при изменении массива на [1,2] и повторном выполнении кода селектор .barchartrectangle вернет три существующих элемента. Однако D3 отметит наличие в массиве только двух. Тогда она воспользуется функциями, связанными с инструкцией .exit(), и в этом случае удалит .remove() третий из них.

Как D3 понимает, какой элемент следует удалять? Это зависит от ключа данных, определенного ранее. В нашем примере им по-прежнему является индекс, поэтому он удалит третий добавленный элемент. Но если элемент оказался фактическим id (item, index)=> item, и мы изменили массив на [1,3], то вместо третьего будет удален второй barchartrectangle.

Работа инструкций .enter() и .exit() теперь понятна, переходим к отображению гистограммы.

В строке 4 применяем .enter() и тем самым инструктируем D3, что делать с данными. В строке 5 добавляем новый rect и устанавливаем необходимые атрибуты. Как видно в строках 6 и 7, функция .attr() принимает не только фиксированные значения, но и функцию, которая использует элемент и его индекс, что позволяет отрисовывать элементы в зависимости от данных.

Сначала в строке 6 получаем масштабированное положение x путем вызова функции масштабирования x, определенной ранее с номером года. В ответ вернется значение пикселя, на котором нужно разместить гистограмму. Поскольку далее ширина будет установлена как 100, то мы подстраиваем столбец к центральной точке этого значения, выполняя -50. Для координаты y просто вызываем функцию масштабирования. В строке 9 для определения высоты выполняем 800- y(item. comments) из-за обратной функции масштабирования. Напомню, что диапазон был определен как [800, 100], поэтому малые значения комментариев будут отображаться в большие значения пикселей. В связи с этим мы обращаем эту функцию, выполняя 800 — y(item.comments).

Наконец, для привлекательности задаем синий цвет заливки. Итоговый результат:

Отлично! На основе данных мы отрисовали на экране гистограмму. В этом и состоит вся суть D3. Но у нее намного больше возможностей. На D3.js — Data-Driven Documents (d3js.org) вы можете ознакомиться с интересными графиками и фигурами.

Заключение

Теперь у вас есть общее представление о визуализации посредством библиотеки D3. Вы можете рисовать фигуры в SVG на основе динамически генерируемых данных.

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

Читайте также:

  • notebookJS: JavaScript и D3 в Jupyter Notebook
  • Почему я больше не пользуюсь D3.js
  • ТОП-25 библиотек React 2021–2022: новые, полезные, но малоизвестные пакеты JavaScript

Читайте нас в Telegram, VK и Яндекс.Дзен

d3-hierarchy / D3 / Observable

Алгоритмы 2D компоновки для визуализации иерархических данных.

с показателем 1-30 из 32 списков

D3. Группы в качестве иерархии

D3 • FIL

15 сентября 2020 г. •

54

4

Случайное дерево

D3 • Mike Bostock

Secp 29, Secp 29. 2018•

36

Обход иерархии, анимированный

D3•Fil

6 июля 2020 г.•

21

Bilevel Edge Bundling

Эта записная книжка представляет собой FORK

D3 • Mike Bostock

24 мая 2018 г.

33

СДЕЛАННЫЕ ДЕРЕВО

Эта ноутбук — FOK

D3 • Mike Bostock

1, 2020 •

90

Эта записная книжка представляет собой FORK

D3 • Mike Bostock

Dec 30, 2019 •

86

Иерархический град

Эта ноутбука — BOSK

D3 • Mike Bostock

9 000, 2019 •

99

. График

D3•Mike Bostock

Jan 7, 2019•

42

d3.packEnclose

D3•Mike Bostock

May 26, 2017•

56

Hierarchical Bar Chart

D3•Mike Bostock

5 июля 2019 г.

116

, направленное на силовое дерево

Эта записная книжка-это вилка

D3 • Mike Bostock

26 июня 2019 г. •

91

3

D3. Стратификация

D3 • FIL

14 июня 2019 г. •

31

Посещение D3.Hierarchy

D3 • FIL

14 июня 2019 г. •

24

D3.HIERARCHY Записная книжка — это вилка

D3 • Mike Bostock

10 марта 10, 2019 •

22

Внутренний TreeMap

Эта ноутбук — Bostk

D3 • Mike Bostock

10 марта 2019 г.

38

1 0003

Dreemp по количеству 

Этот блокнот представляет собой вилку

D3 • Mike Bostock

10 марта 2019 г. •

7

Растянутая TreeMap

Эта записная книжка представляет собой вилку

D3 • Mike Bostock

10 марта 2019 •

14

Собработанное дерево

D3 • Mike Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost Bost.

1 октября 2018 г. •

294

Иерархический края объединения

Эта записная книжка — это вилка

D3 • Mike Bostock

23 мая 2018 •

183

3

D3 • Mike Bostock

26 ноября 2018 г.

258

Zoomable Sunburst

Эта ноутбука — вилка

D3 • Mike Bostock

30 апреля 2018 •

390

3

, Sunbure

3

. D3 • Mike Bostock

28 апреля 2018 г. •

139

4

TREEMAP, CSV

D3 • Mike Bostock

Dec 28, 2017 •

126

3

Tree, Radial Tidy

3

Tree, Radial Tidy

3

Tree, Radial Tidy

3

Tree, Radial Tidy

3

Tree, Radial Tidy

3

Tree. • Майк Босток

16 ноября 2017 г. •

167

Tree, Cluster

Эта записная книжка представляет собой Fork

D3 • Mike Bostock

декабря 2017 г. •

76

4

Цильсы. D3 • Mike Bostock

30 апреля 2018 г. •

93

Tree, Tidy

Эта записная книжка — это вилка

D3 • Mike Bostock

Dec 28, 2017 •

169

Кружная упаковка, Bubble Chart

это вилка

D3 • Mike Bostock

28 декабря 2017 г.

112

2

ПЕРЕДИТ, Icicle

Эта записная книжка — Fork

D3 • Mike Bostock

28 апреля 2018 •

33

1 0003

Появление. 1-30 из 32 объявлений

Избранные авторы / Observable / Observable

Observable

@observablehq

ProfileNotebooksCollections

0003 Эта записная книжка представляет собой VOK

Helena Oliveira

сентября 28

Voronoi Cells

Эта ноутбука — вилка

Ben Simonds

Sep 28 •

22

Taylor Polynomials

Amit Sch

SEP 28.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

Euclidean Rhythms

Torben Jansen

24 сентября •

64

2

Население сетки (15 минут) — Соединенные Штаты

Evan Galloway

Sep 2 •

5

0003

Marcas Y Canales

Эта записная книжка представляет собой Fork

Néstor Andrés Peña

сентябрь 14 •

1

Отталкивание

Это ноутбук — FORB

GRAB SUSMANN

SEP 5 •

21

Zoomble

SEP 5 •

9000 21

Zoomble

SEP 5 •

21

Zoom. Schudel

август 18 •

43

Океанские токи

Tom Macwright

Aug 12 •

24

MAROPLETH MAP

Крис Хенрик

Aug 26 •

13

Animated Mapbox Vector Fields (aka Wind Maps)

Corey Guastini

Aug 15•

35

New Yorkers from Canada to the Amboys 

This notebook is a fork

Benjamin Schmidt

Jul 10•

29

2

Калифорнийцы от Орегона до Мексики

Джеффри Бейкер

9 июля 9•

24

Пять лет слежения за свободой прессы

90 Aug• 3 Harris 90 Lapiroff0003

5

Женщины в космосе 👩🏼‍🚀

Raluca Nicola

28 марта, 2021 •

62

Японский от Hokkaido до Okinawa

. Этот ноутбук — FORK

Sorami Hisamoto

Aug 21.

Как адаптировать Dataviz

Tom Larkworthy

АГЛИТАЯ 16 •

22

Голландцы от Eemshaven до Epen

Жюль Блам

Aug 17 •

21

Том. согласование древовидных структур

Матье Жуэ

31 июля •

27

Насколько мужчины выше женщин?

Tereza iofciu

апрель 28 •

6

Атомные агенты: полилины и силы

Эта ноутбук — вилка

Graham McNeill

5 •

65

Строижи

3 мая 2021 г. •

24

Таблица умножения

SUGIMOTO Tatsuo

август 3 •

21

Движение

JO WOOD

13 июля •

60

2

Использование участка вокруг Интернета

Anjana Vakil

Apr 19 •

4

Историческая карта: день: день: день: день: день: день: день: день: день: день: день.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *