Как генерировать SVG на React

React является одним из самых популярных на сегодня способов создания компонентно-ориентированного UI.

Не знаю как вы, но для меня SVG код становится беспорядочным довольно быстро. Пытаясь понять что случилось с графиком или визуализацией, глядя только на шаблонный генератор SVG (или исходный код SVG), в попытках сохранить внутреннюю структуру, часто оказывается непосильной задачей.

Благодаря Facebook, для этой трудоемкой работы у нас есть новый инструмент React.

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

Главное преимущество React - работа с DOM (не только с HTML). Поэтому, как и с HTML, вы точно также можете работать и с SVG. Например, создание круга на React.js:

import React from 'react';

export default class App extends React.Component {
  render() {
    return (
      <svg>
        <circle cx={50} cy={50} r={10} fill="red" />
      </svg>
    )
  }
}

Как я уже сказал, с точки зрения React, нет никакой разницы между работой с HTML или SVG.

Но давайте попробуем создать что-то немного более сложное чем простой круг, чтобы можно было увидеть, как React помогает в структурировании SVG кода.

Представьте, что мы должны создать инструмент, для визуализации довольно сложного набора данных:

[
  [1, 3],
  [2, 5],
  [3, 2],
  [4, 16],
  [18, 5]
]

Это просто массив X и Y координат.

Я буду использовать React Hot Boilerplate, чтобы сэкономить время на конфигурирование нашего проекта, а также нам понадобится:

  • webpack - мощная утилита для сборки бандлов и оптимизации модулей JavaScript и других ресурсов для фронтенда.
  • babel - транспайлер, позволяет использовать ECMAScript 6 (ES6) в браузерах, которые еще не поддерживают его.
  • react-hot-loader - отличный инструмент, который будет обновлять компоненты React, без перезагрузки страницы.

Начнем с файла script/index.js для начальной загрузки нашего приложения:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
import data from './data';

ReactDOM.render(<App data={data} />, document.getElementById('root'));

В следующем файле script/data.js, находится наш массив данных:

export default [
   [1, 3],
   [2, 5],
   [3, 2],
   [4, 16],
   [18, 5]
 ];

script/app.js для визуализации будущего графика:

import React from 'react';
import Graph from './components/graph';

export default class App extends React.Component {
  render() {
    return (
      <Graph data={this.props.data} />
    )
  }
}

Самая интересная часть разработки с React - мы можем сначала разработать высокоуровневые компоненты, а только потом разбить их на части.

Для примера scripts/components/graph.js:

import React from 'react';
import Axis from './axis';
import GraphBody from './graph_body';

export default class Graph extends React.Component {
  render() {
    return (
      <svg>
        <Axis
          length={width}
          horizontal={true}
        />
        <Axis
          length={height}
          horizontal={false}
        />
        <GraphBody
          data={this.props.data}
        />
      </svg>
    )
  }
}

Здесь мы представили две оси и тело графика, весь код выглядит довольно понятным и логичным. Конечно, он не будет работать. Это просто пример формирования первоначального API нашего графика: мы еще не реализовали дочерние компоненты и у нас есть некоторые неопределенные переменные (width и height).

Необходимо задать размер нашего графика, для этого используем defaultProps:

export default class Graph extends React.Component {
  static defaultProps = { width: 800, height: 600 };

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

Мы можем передать данные значений в SVG:

<svg width={this.props.width} height={this.props.height}>

Затем, мы можем объявить оси и тело графика, установив начальные позиции:

import React from 'react';
import Axis from './axis';
import GraphBody from './graph_body';

export default class Graph extends React.Component {
  static defaultProps = { width: 800, height: 600 };

  render() {
    return (
      <svg width={this.props.width} height={this.props.height}>
        <Axis
          x={20}
          y={this.props.height - 100}
          length={this.props.width}
          horizontal={true}
        />
        <Axis
          x={20}
          y={0}
          length={this.props.height - 100}
          horizontal={false}
        />
        <GraphBody
          x={20}
          y={this.props.height - 100}
          data={this.props.data}
        />
      </svg>
    )
  }
}

Просто взгляните, благодаря React, вы сможете легко разобраться в данном коде, просто прочитав его. Мы закончили с родительским компонентом, перейдем к вложенным.

Оси, должны просто возвращать линии. Согласно спецификации SVG, для создания линии, мы должны установить четыре координаты: x1, y1, x2, y2. Обратите внимание, оси могут быть как вертикальными, так и горизонтальными, а также должны иметь начальную позицию props:

scripts/components/axis.js

import React from 'react';

export default class Axis extends React.Component {
  prepareCords() {
    let coords = {
      x1: this.props.x,
      y1: this.props.y
    }

    if(this.props.horizontal) {
      coords.x2 = coords.x1 + this.props.length;
      coords.y2 = coords.y1;
    } else {
      coords.x2 = coords.x1;
      coords.y2 = coords.y1 + this.props.length;
    }

    return coords;
  }

  render() {
    let coords = this.prepareCords();
    return (
      <line {...coords} stroke="green" strokeWidth={2} />
    )
  }
}

{...coords} - новый способ ES6 записи x1={coords.x1} x2={coords.x2} y1={coords.y1} y2={coords.y2}. Благодаря Babel, мы можем использовать данный способ, не дожидаясь поддержки в браузерах.

Проверим работу оси, установив простую заглушку:

import React from 'react';

export default class GraphBody extends React.Component {
  render() {
    return null;
  }
}

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

Результат проделанной работы:

Следующим шагом будет удаление заглушки и создание полнофункционального графика. Чтобы нарисовать линию графика, мы будем использовать элемент path. Объявим его в качестве d параметра. Создать такую строку очень просто, она состоит из двух частей: начальная команда Moveto и набор команд Lineto:

Moveto - начальная точка: M ${this.props.x} ${this.props.y}. Затем мы подключаем остальные данные при помощи команды L x y.

Однако мы не можем просто передать набор данных x и y. Мы должны суммировать их с начальной точкой x и вычесть из начальной точки y, потому что ось в SVG идет сверху вниз.

Полученный код выглядит следующим образом:

import React from 'react';

export default class GraphBody extends React.Component {
  static defaultProps = { multiplier: 20 };

  prepareData() {
    let d = [`M ${this.props.x} ${this.props.y}`];

    let collector = this.props.data.map(chunk => {
      let xNext = this.props.x + chunk[0] * this.props.multiplier;
      let yNext = this.props.y - chunk[1] * this.props.multiplier;
      return `L ${xNext} ${yNext}`;
    });

    return d.concat(collector).join(' ');
  }

  render() {
    let d = this.prepareData();
    return(
      
    )
  }
}

Я также умножил координаты графика, для наглядности:

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

Обновленный файл data.js выглядит следующим образом:

export default [
 [
   [1, 3],
   [2, 5],
   [3, 2],
   [4, 16],
   [18, 5]
 ],
 [
   [1, 16],
   [2, 23],
   [3, 5],
   [4, 3],
   [5, 1]
 ]
];

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

Обновленный файл index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
import data from './data';

ReactDOM.render(<App datasets={data} />, document.getElementById('root'));

Обновленный файл scripts/app.js:

import React from 'react';
import Graph from './components/graph';

export default class App extends React.Component {
  render() {
    return (
      <Graph data={this.props.datasets[0]} /> # or this.props.datasets[1] just to check that everything is working 
    )
  }
}

Однако, изменять набор данных в коде, не совсем удобно (даже если использовать React Hot Load). Поэтому мы добавим специальную опцию смены данных.

scripts/app.js:

import React from 'react';
import Graph from './components/graph'

export default class App extends React.Component {
  state = { dataSetIndex: 0 }

  selectDataset(event) {
    this.setState({dataSetIndex: event.target.value});
  }

  render() {
    let options = this.props.datasets.map((_, index) => {
      return <option key={index} value={index}>Dataset {index + 1}</option>
    });

    return (
      <div>
        <select
          value={this.state.dataSetIndex}
          onChange={this.selectDataset.bind(this)} >
          {options}
        </select>
        <Graph data={this.props.datasets[this.state.dataSetIndex]} />
      </div>
    )
  }
}

Теперь можно менять данные на лету!

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

Начнем с создания простого приложения, которое обрабатывает входящие запросы для SVG графиков (svg_server.js):

require("babel-register"); var express = require('express'); var app = express(); var data = require('./scripts/data').default; var svgRenderer = require('./scripts/svg_renderer').default; app.get('/svg', function (req, res) { var svg = svgRenderer(data[0]); res.send(svg); }); var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); });

Как видите, нужны только 3 строки чтобы добавить наше приложение, все остальные - простой шаблон.

Файл scripts/svg_renderer.js - практически старая версия нашего основного приложения:

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import Graph from './components/graph'

export default function(data) {
  return ReactDOMServer.renderToStaticMarkup(<Graph data={data}/>);
}

Для проверки:

  1. выполните node svg_server.js,
  2. откройте localhost:3000/svg,
  3. и выполните curl localhost:3000/svg.

Что мы получаем:

<svg width="800" height="600"><line x1="20" y1="500" x2="820" y2="500" stroke="green" stroke-width="2"></line><line x1="20" y1="0" x2="20" y2="500" stroke="green" stroke-width="2"></line><path d="M 20 500 L 40 440 L 60 400 L 80 460 L 100 180 L 380 400" stroke="orange" stroke-width="1" fill="none"></path></svg>

Если у вас возникли ошибки в коде, вы можете скачать репозиторий с GitHub.

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

Но перед началом работы, имейте ввиду, что на данный момент, React поддерживает не все элементы SVG. Также React может установить innerHTML при помощи dangerouslySetInnerHTML, чтобы реализовать недостающие SVG элементы. Скорее всего, этот недочет исправят в следующей версии React.

Перевод статьи Generating SVG With React

Тэги: SVGreact.js

Вход

Уважаемый пользователь! Мы обнаружили, что вы используете AdBlock и вынуждены скрыть часть материалов на нашем сайте. Siteacademy существует и развивается за счет доходов от рекламы. Просим внести наш сайт в список исключений или отключить Блокировщик рекламы на нашем сайте.