Это шестой пост в моей серии Визуализация с помощью React. Предыдущий пост: Помимо рендеринга

Играть с codepen весело, но есть вероятность, что у вас есть другие амбиции для ваших проектов визуализации. В реальном мире, по крайней мере, в моей повседневной работе, React используется для создания веб-приложений. Итак, в этой последней части мы собираемся создать собственное веб-приложение, загрузить некоторые реальные данные и использовать некоторые существующие модули для их визуализации, как профессионалы! В этой статье мы собираемся построить это:

Вы можете попробовать демо-версию приложения на https://jckr.github.io/example-react-app/.
Весь код находится на https://github.com/jckr/example- реактивное приложение.

В чем разница между веб-приложением и веб-страницей, спросите вы? Ну, веб-страница — это html-документ с некоторыми ссылками на скрипты или какой-то встроенный javascript. Веб-приложение — это комплексная система файлов кода, работающих вместе, включая сервер. Все эти файлы преобразуются с помощью системы сборки, которая создает скомпилированную версию, которая запускается в браузере. Такие преобразования могут включать поддержку JSX или поддержку синтаксиса ES6/ES7. Ваша работа может быть разделена на множество легко читаемых и простых в обслуживании исходных файлов, но ваш браузер просто прочитает один единственный файл, написанный на понятной ему версии Javascript.

Это может показаться большой работой по настройке. К счастью, Facebook выпустил «create-react-app», инструмент для создания шаблонов, предназначенный для простых приложений React.

Вам потребуется доступ к среде командной строки, такой как Terminal в MacOS или Cygwin в Windows, а также установленные nodejs и npm. (см. https://nodejs.org/ru/). Вам понадобится узел версии 4 и выше. Вы можете использовать nvm, чтобы легко менять версии узла, если это необходимо.

Вот иллюстрированное руководство о том, что вам нужно сделать, чтобы начать:

Сначала установите приложение create-react-app с помощью команды:

npm install create-react-app -g

В родительском каталоге, где вы хотите разместить свое приложение, используйте команду: create-react-app + имя вашего приложения. Это также будет имя каталога, в котором будет находиться это приложение.

Приведенная выше команда скопирует кучу файлов. Когда это будет сделано, перейдите в каталог вашего приложения и введите npm start…

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

Помните, когда мы делали диаграммы рассеяния, я никогда не занимался черной работой по созданию линий сетки, осей и т. д. Именно по этой причине — это может быть много ручной работы.
Но теперь, когда мы собираемся построить профессиональную глядя на веб-приложение, мы собираемся пройти весь путь.

Один компонент, один файл

Приложение, которое мы создаем, состоит из 3 компонентов: приложение, которое является родителем; Точечная диаграмма, представляющая собой собственно диаграмму, и HintContent для точного контроля над тем, как выглядит всплывающая подсказка.
Там есть файлы index.html и index.js, которые очень просты:

index.html

<!doctype html>
  <html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
 
ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Об index.html особо сказать нечего. Все, что он делает, это создает div с идентификатором root, куда все будет помещаться.
index.js заканчивается знакомой командой ReactDOM.render( … ), которая выведет наш компонент в вышеупомянутый корневой div.

Но он начинается с нескольких операторов импорта. Что они делают, так это связывают исходные файлы вместе.
Индекс начинается с импорта функций из реакции: React и ReactDOM. Это было сделано в нашей среде codepen с помощью настроек.
Следующие две строки связывают наш файл index.js с другими файлами, которыми мы управляем: App и index.css. App содержит наш компонент самого высокого уровня, а index.css содержит стили.
Я внес некоторые изменения в index.css для стилей, которых не смог достичь с помощью react — например, стили тела или некоторые стили элементов, созданные библиотеками, над которыми я не имел прямого контроля (подробнее об этом позже). В противном случае я использую встроенные стили в традициях React.

Давайте перейдем к нашему исходному файлу App.js, который описывает компонент App.

Его последняя строка:

export default App;

И это строка, которая соответствует тому, что мы видели ранее в index.js:

import App from './App';

С помощью этой пары операторов внутри index.js приложение будет эквивалентно тому, что было внутри App.js при экспорте.
Используя эту конструкцию, используя импорт и экспорт, файлы могут быть короткими, разборчивыми и сфокусированными. по одной конкретной проблеме.

Но давайте еще раз взглянем на первые две строки App.js:

import React, {Component} from 'react';
import {csv} from 'd3-request';

Что это за фигурные скобки?

Если модуль (файл javascript, который импортирует или экспортирует) имеет экспорт по умолчанию, то при его импорте вы можете просто использовать

import WhatEverNameYouWant from 'module';

Где WhatEverNameYouWant — это любое имя, которое вы хотите. Обычно он начинается с заглавной буквы, но вы делаете это сами.
Но модуль может экспортировать не только экспорт по умолчанию. В этом случае вы должны использовать фигурные скобки. Есть много статей на эту тему, например эта. В основном я привожу это для того, чтобы вы увидели разницу между использованием фигурных скобок и их отсутствием.

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

create-react-app не имеет никаких зависимостей, кроме реакции, а это значит, что ему не нужно устанавливать другие модули. Но этот проект есть! Для этого нужны d3-request и react-vis.
Вы можете установить их из командной строки, набрав npm install d3-request –save и npm install react-vis.

Компонент App — загрузка и передача данных

Наш компонент приложения будет делать две вещи: загружать данные и, когда данные загружены, передавать их компоненту Scatterplot, чтобы он мог нарисовать диаграмму.
Я намекнул на метод жизненного цикла componentWillMount как на отличное место для загрузить данные, так что давайте попробуем это!

Примечание. Опять же, поскольку я написал оригинальную версию этой статьи 2 года назад, React не только отказывается от поддержки своего метода жизненного цикла componentWillMount, но и d3js меняет способ загрузки данных. Этот код все еще работает, но я подправлю его в ближайшем будущем, чтобы сделать его более похожим на 2018 год.

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {};
  }

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

componentWillMount() {
  csv('./src/data/birthdeathrates.csv', (error, data) => {
    if (error) {
      this.setState({loadError: true});
    }
    this.setState({
      data: data.map(d => ({...d, x: Number(d.birth), y: Number(d.death)}))
    });
  })
}

Мы используем тот же файл данных, что и раньше. Здесь я жестко закодировал его в строке, но этот адрес может полностью исходить из свойства, переданного в App.

Также обратите внимание, что я использую csv из d3. Раньше это был d3.csv, но теперь это не так, потому что я импортирую только csv из d3, а точнее из его подбиблиотеки d3-request. Одним из больших изменений d3 v4 является то, что его код доступен в виде небольших фрагментов. Существуют и другие способы загрузки файла csv, но метод csv в d3 очень удобен, а также это отличный способ показать, как выбрать одну полезную часть большой библиотеки.

Итак, мы загружаем этот файл. Что дальше? Если загрузка файла вызывает ошибку, я собираюсь сообщить об этом через состояние (setState({loadError: true});). В противном случае я передам содержимое файла в состояние.

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

Оператор map превращает этот объект в: копию всех этих свойств (это то, что делает {…d), плюс свойства x и y, которые просто преобразуют свойства рождения и смерти каждого объекта в числа (т.е. «36,4» в 36,4).
Итак, независимо от того, загрузится этот файл или нет, я собираюсь изменить состояние компонента.

Какие ценности может принимать государство?

При первом создании компонента состояние пусто.
Затем componentWillMount пытается загрузить файл. Состояние по-прежнему пусто. В течение этого очень короткого времени будет запущен рендеринг (подробнее об этом позже).
Затем файл либо загрузится, либо нет. Если он загрузится, состояние теперь будет содержать свойство данных, и, поскольку состояние изменится, компонент будет повторно отображаться. Если он не загружается, у состояния будет свойство loadError, и компонент также будет перерендерен.

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

render() {
    if (this.state.loadError) {
      return <div>couldn't load file</div>;
    }
    if (!this.state.data) {
      return <div />;
    }
    return <div style={{
      background: '#fff',
      borderRadius: '3px',
      boxShadow: '0 1 2 0 rgba(0,0,0,0.1)',
      margin: 12,
      padding: 24,
      width: '350px'
    }}>
      <h1>Birth and death rates of selected countries</h1>
      <h2>per 1,000 inhabitants</h2>
      <Scatterplot data={this.state.data}/>
    </div>;
  }

if (this.state.loadError) — это ситуация, когда данные не загрузились. Именно поэтому я инициировал состояние пустого объекта, потому что, если бы this.state не было определено, этот синтаксис вызвал бы ошибку. (this.state && this.state.error) было бы нормально, но я мог бы просто инициализировать состояние.

if (!this.state.data) позаботится о ситуации, когда данные еще не загружены. Мы также знаем, что ошибки еще не было, иначе сработало бы первое условие. В профессиональных условиях вы бы поместили индикатор выполнения или счетчик. Однако загрузка 70-строчного csv не займет много времени, так что это будет слишком, поэтому там просто пустой div.

Наконец, если ни одно из этих условий не выполняется, мы будем отображать карту с элементом Scatterplot внутри. Мы собираемся визуализировать немного больше, чем просто элемент Scatterplot — мы стилизуем div, на котором он будет стоять, и добавим заголовок.

Компонент Scatterplot: знакомство с реакцией

React-vis — это библиотека диаграмм, которую мы используем в Uber.
Основная идея заключается в том, что мы можем создавать диаграммы, составляя элементы, как веб-страницу:

<XYPlot
  width={300}
  height={300}>
  <HorizontalGridLines />
  <LineSeries
    data={[
      {x: 1, y: 10},
      {x: 2, y: 5},
      {x: 3, y: 15}
    ]}/>
  <XAxis />
  <YAxis />
</XYPlot>

… создает очень простую линейную диаграмму с горизонтальными линиями сетки и осями. Не хотите сетку? удалить часть. Хотите вертикальные линии сетки тоже? Просто добавьте внизу.
Вам нужна еще одна линейная диаграмма? Вы можете добавить еще один элемент LineSeries. Или Вертикальный барсерий. Или RadialChart (круговые или кольцевые диаграммы). И так далее, и тому подобное.
React-vis берет на себя всю рутинную работу по созданию диаграмм, так что нам это не нужно.

Давайте погрузимся в Scatterplot.

import React, {Component} from 'react';
import {
  Hint,
  HorizontalGridLines,
  MarkSeries,
  VerticalGridLines,
  XAxis,
  XYPlot,
  YAxis
} from 'react-vis';

scatterplot.js начинается со знакомых операторов импорта. Мы импортируем только то, что нам нужно, из «React-vis».

import HintContent from './hint-content.js';

Затем мы импортируем HintContent — hint-content.js использует экспорт по умолчанию, поэтому фигурные скобки не нужны. Кстати, это расширение .js не обязательно в имени файла.

export default class Scatterplot extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null
    };
    this._rememberValue = this._rememberValue.bind(this);
    this._forgetValue = this._forgetValue.bind(this);
  }
 
  _rememberValue(value) {
    this.setState({value});
  }
 
  _forgetValue() {
    this.setState({
      value: null
    });
  }

Мы могли бы сделать Scatterplot чисто функциональным компонентом… если бы передали функции обратного вызова для обработки наведения мыши. Эти функции могли изменить состояние компонента App, который повторно отрисовывал бы его дочерние элементы — Scatterplot. Поскольку ни один компонент за пределами Scatterplot не заинтересован в том, чтобы знать, где находится мышь, этот компонент может иметь свое собственное состояние.
Мы также добавляем две частные функции. Они должны быть привязаны к «этому» — мы создаем класс, и эти функции должны быть привязаны к каждому экземпляру этого класса. Другой способ думать об этом таков: вы можете добавить приватные функции в компонент React, но если они используют состояние, свойства или приватные переменные, вам придется привязать их к 'this' в конструктор.

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

render() {
    const {data} = this.props;
    const {value} = this.state;
    return <div>
      <XYPlot
        margin={{top:5, left: 60, right: 5, bottom: 30}}
        width={320}
        height={290}>
        <VerticalGridLines />
        <HorizontalGridLines />
        <XAxis/>
        <YAxis/>
        <MarkSeries
          data={data}
          onValueMouseOver={this._rememberValue}
          onValueMouseOut={this._forgetValue}
          opacity={0.7}
        />
        {value ?
          <Hint value={value}>
            <HintContent value={value} />
          </Hint> :
          null
        }
      </XYPlot>
      <div style={{
        color: '#c6c6c6',
        fontSize: 11,
        lineHeight: '13px',
        textAlign: 'right',
        transform: 'rotate(-90deg) translate(120px, -160px)'
      }}>Death Rates</div>
      <div style={{
        color: '#c6c6c6',
        fontSize: 11,
        lineHeight: '13px',
        textAlign: 'right',
        transform: 'translate(-5px,-14px)',
        width: '320px'
      }}>Birth Rates</div>
    </div>;
  }

Мы возвращаем элемент div. Причина в том, что в самом конце мы пишем название осей в этом div. Но в основном этот div будет содержать компонент XYPlot, полученный из react-vis. Я пропускаю: свойство поля, высоту и ширину. Маржа необязательна, и я использую ее для контроля. Высота является обязательной, как и ширина, хотя у react-vis есть адаптивный компонент, который заставляет диаграммы адаптироваться к ширине страницы (здесь не используется).

Затем я просто добавляю: горизонтальные и вертикальные линии сетки, а также горизонтальную и вертикальную оси. Я использую настройки по умолчанию для всех из них (полное раскрытие — я изменил несколько вещей с помощью таблицы стилей index.css). Но то, как организованы метки и строки, меня устраивает.

Затем мы добавляем компонент MarkSeries, представляющий собой все круги.

<MarkSeries
  data={data}
  onValueMouseOver={this._rememberValue}
  onValueMouseOut={this._forgetValue}
  opacity={0.7}
/>

Свойство данных происходит из свойств, переданных компоненту Scatterplot. Он должен иметь свойства x и y, поэтому я преобразовал наш CSV-файл таким образом. У него также может быть свойство размера или цвета, но мы не будем использовать их в нашем примере.

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

Наконец, мы используем свойства onValueMouseOver и onValueMouseOut для передачи функций, обрабатывающих то, что происходит, когда пользователь собирается, ну, навести указатель мыши на одну из меток или удалить с них курсор мыши. Это наши частные функции, которые были раньше:

_rememberValue(value) {
  this.setState({value});
}
 
_forgetValue() {
  this.setState({
    value: null
  });
}

Когда пользователь проводит мышью по метке, соответствующая точка данных (значение) будет передана в состояние. А когда пользователь убирает свою мышь, свойство value сбрасывается на ноль.

наконец, прямо под компонентом MarkSeries мы вызываем нашу подсказку:

{value ?
  <Hint value={value}>
    <HintContent value={value} />
  </Hint> :
  null
 }

Если значение (из состояния) чего-то стоит, то создаем компонент Hint. Этот исходит от react-vis и обрабатывает позиционирование всплывающей подсказки, а также некоторый контент по умолчанию. Но я хочу точно контролировать то, что я показываю во всплывающей подсказке, поэтому я создал компонент, который делает именно это.
Создание такого специализированного компонента — это здорово, потому что он скрывает эту сложность от компонента Scatterplot. Все, что нужно знать Scatterplot, это то, что он передает свойства компоненту HintContent, который возвращает… что-то хорошее.

Из-за импорта и экспорта обычно рекомендуется создавать такие небольшие специализированные компоненты.

Для победы: компонент содержания подсказки

import React from 'react';
export default function HintContent({value}) {
  const {birth, country, death} = value;
  return <div>
    <div style={{
      borderBottom: '1px solid #717171',
      fontWeight: 'bold',
      marginBottom: 5,
      paddingBottom: 5,
      textTransform: 'uppercase'
    }}>{country}</div>
    {_hintRow({label: 'Birth Rates', value: birth})},
    {_hintRow({label: 'Death Rates', value: death})}
  </div>;
}
 
function _hintRow({label, value}) {
  return <div style={{position: 'relative', height: '15px', width: '100%'}}>
    <div style={{position: 'absolute'}}>{label}</div>
    <div style={{position: 'absolute', right: 0}}>{value}</div>
  </div>;
}

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

Единственная цель HintComponent — обеспечить точное управление внешним видом всплывающей подсказки (которая также получает стили из index.css). Но я хотел точно контролировать, как различные части точки данных будут отображаться внутри.

Итак, у него 3 ряда. Первый содержит название страны (ну, ее трехбуквенный код ISO). Я решил сделать его жирнее и заглавными буквами. Кроме того, у него будет граница, отделяющая его от остальной части карты.

Следующие две строки похожи, поэтому я создал функцию для их рендеринга, а не для повторного ввода.

Это относительный элемент div, занимающий все пространство, с двумя абсолютными элементами в качестве дочерних элементов. Метка one не имеет информации о положении, поэтому она прикреплена к верхнему левому углу, но значение one имеет атрибут right, равный 0, поэтому оно прикреплено к верхнему правому углу. Таким образом, для каждой строки метка будет выровнена по левому краю, а значение — по правому краю.

И это все!

Для нашего грандиозного финала мы собираемся создать более сложное приложение с несколькими взаимодействующими друг с другом диаграммами…