Введение в SVG и пример — pie chart

SVG (scalable vector graphics) это векторный формат графики подобно EPS, анимации и интерактива с пользователем, разрабатываемый в W3C. Внутри файл не бинарный а обычный XML, описывающий объекты, их эффекты и поведение.

Векторная графика в общем нужна при изменении размера изображения без потери качества, например в полиграфии. В web я это вижу в резиновых сайтах, где размеры блоков установлены в %, размер шрифтов в em, а лого — в SVG. Пересели на более хороший монитор — всё изменилось пропорционально. Практические примеры — иконки, графики, карты, логотипы, интерфейсы. Ниже я привожу пример такого Pie-chart’а.

Интеграция

HTML — Inline

Поскольку SVG по сути XML, то его можно сразу inline-стилем описывать в XHTML-структуре. Однако как я уже убедился, XHTML1.1 doctype подразумевает что MIME документа уже не text-plain. А «ослик» IE6 не понимает XHTML в принципе, с другой стороны Firefox использует два парсера, и если MIME не application/xhtml+xml, то inline SVG не будет распознан. Это палка с двумя концами — IE и FF.

<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
<circle cx="150" cy="100" r="50" />
</svg>

Второй способ и наиболее практичный — ссылаться на существующий файл через object-тэг. Для уменьшения траффика, файл можно сжать и получить SVGZ архив.

<object type="image/svg+xml" data="./mySVGimage.svg" name="owMain" width="400" height="150"></object>

CSS и Javascipt

Как вы увидите ниже — поскольку SVG очень тесно связан с HTML/XML, то естественно что на графические объекты можно не только описывать при помощи CSS, но и писать Javascript-функции на всевозможные onclick и тп. event’ы (отсюда и интерактивность).

<circle onclick="circle_click(evt)" cx="300" cy="225" r="100" fill="red"/>

Конвертирование и редакторы

Конвертировать SVG можно и в png/jpeg, но для этого фактически надо проделать работу обработчика. В PHP этим занимается PEAR XML_svg2image библиотека. Ещё есть сервис по конвертированию растрового изображения в векторный (правда там EPS). Из редакторов — есть Inkscape и Adobe Illustrator, Corel Draw. Самый эффективным способ — использование Imagemagick:

$ convert ok.svg ok.png

Графика

Примитивы

Всякое рисование начинается с определения пространства (двумерного), разделения на координаты (в единицах с плавающей точкой или процентах) и введения примитивных конструкций:

  • line — прямая линия. x1, y1, x2, y2 — координаты
  • polyline — ломанная линия. points — перечисление координат точек
  • rect — прямоугольник. x,y, width, height, rx, ry — верхний левый угол, размеры, радиусы углов
  • polygone — многоугольник. Похож на polyline. points — координаты точек
  • circle — круг. cx, cy, r — координаты центра и радиус
  • ellipse. cx, cy, rx, ry — координаты центра и радиусы
  • text — текстовая надпись. Очень неудобная, потому что без переноса строк и с абсолютным позиционированием. x,y, font-family, font-size
  • tspan — может описывать слова внутри text-элемента. Например сдвигать или раскрашивать
  • tref — повторное использование text-элемента с указанным id
    <defs><text id="myText">Простой текст</text></defs>
    <tref xlink:href="#myText" x="50" y="50"></tref>
  • textPath — текст на path-кривой, связанный по id<defs><path id="myPath" d="M25,75 C25,15 175,15 175,75" /></defs>
    <text><textPath xlink:href="#myPath" startOffset="50%">Водка Vinogradoff</textPath></text>
  • image — растровое изображение
    <image image-rendering="optimizeSpeed" xlink:href"background.jpg" width="100%" height="100%" preserveAspectRatio="xMidYMid slice" filter="url(#blurpane)"/>
Параметры и стили

Как и в html, одни линии мало кому нужны — их надо закрашивать, указывать цвета, и всё это делается параметрами

  • fill, fill-rule — заливка. Например «none»,blue, indigo.
  • stroke, stroke-linecap, stroke-linejoin, stroke-dasharray, stroke-dashoffset, stroke-width — граница, аналог border. Вместе stroke-width.
  • font-family, font-size, font-style, font-weight — шрифты для text-элементов
  • text-anchor

Для заливки используются тэги:

  • pattern
  • linearGradient
  • radialGradient

Эти параметры можно все объединить в один аналог CSS и записать в inline-стиле:
style="stroke-width:1; stroke:blue; fill:none"

Группы и кривые Безье

Элементы можно и нужно группировать друг в друга. Кроме того что tspan устанавливаетсявнутрь text-элемента, группирование происходи благодаря <g> элементу.

Кривые Безье это плавные переходные линии, задающиеся по точкам. В SVG для этого существует элемент path, у которого подобно ломанной линии указываются координаты. Рядом с координатами могут стоять буквы, обозначающие свойства линии. Большие буквы говорят об абсолютном позиционировании, маленькие об относительном<path d="M100,200 C100,100 250,100 250,200 S400,300 400,200">

  • M — начало кривой (x,y)
  • Z — конец кривой (без координат)
  • L -прямая линия (x,y)
  • H — горизонтальная линия
  • V — вертикальная линия
  • Q — квадратичная кривая по одной точке
  • T — продолжение кривой с отражением предыдущей точки — упрощает рисование повторяющихся ритмов
  • С — собственно кривая Безье третьего порядка по двум точкам
  • S — упрощённая версия C
  • A — эллиптическая кривая (радиусы, поворот)

Трансформации и возможности

Объекты в SVG пожно искажать, крутить и перемещать при помощи фильтров, которые указываются в качестве параметров:

  • translate — перенос объекта
  • rotate -вращение
  • scale — масштабирование
  • scewX, scewY — искажение
  • matrix — смешанная трансформация

SVG поддерживает фильтры с эффектами освещения. А кроме статичных изображени есть возможность, анимации и интерактивности с пользователем. Например тетрис или нашумевший Microsoft Table и Silverlight реализован программно в SVG , причём при большом желании — заработало и видео (формата Ogg Theora=SVGT). Конечно последние возможности ещё не реализованы во всех браузерах, но первооткрыватели типа Opera имеются.

Тортовый график на SVG и PHP/DB

Несомненно Google API — очень удобная штука. Но не стоит забывать что всё-таки это внешний сервис, пусть и надёжный. Коммерческие разработки не любят рисковать, поэтому генерация графиков всё-таки должно быть локальным (если они не статические).

Генерировать изображение я буду с помощью php, на основе полученных из БД данных для отображения результатов опроса (poll). Поскольку GD-библиотека хоть и понимает размер SWF, генерирует только бинарные файлы. Поэтому генерировать прийдётся XML:header("Content-Type: image/svg+xml");

Посмотрим к чему надо стремится (пока без крутой анимации)..

 

Начнём с резинового расположения двух эллипсов? Сектора видимо идут в порядке убывания по часовой стрелке, что-бы уменьшить количество градиента на мелких участках. Цвет сектора видимо расчитывается пропорционально числу кусочков — тут то и возникает проблема. Как оказывается, сектор нельзя просто-так залить с двумя линиями и существующим эллипсом — фигура должна быть точно очерчена, поэтому надо рисовать всё по отдельности при помощи path, а ellipse нам не понадобится.

Тригонометрия цвета

Для вычисления в цикле координат path-элемента необходимо вспомнить немного математики. Поскольку окружность — частный случай эллипса то в формулах есть много схожего, что нам очень пригодится:

x=cos(angle)*radius; y=sin(angle)*radius; //circle
x=cos(angle)*rx; y=sin(angle)*ry; //ellipse

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

RGB куб, можно по разному резать, делать проекции и вводить свои системы координат типа CMYK и HSL. Разные оттенки выбранной оранжевой заливки получаются при разбиении отрезка между выбранной точкой-цветом и вершиной белого цвета (255,255,255).

if ($intTotalValue) // если сумма значений голосования больше нуля то можно делить и рисовать
foreach((array) $Data as $key=>$recEntry){
$Data[$key]->percent=$recEntry->value/$intTotalValue;
$Data[$key]->color[0]=round($graph->fill[0]+($key/count($Data)*(255-$graph->fill[0])));
$Data[$key]->color[1]=round($graph->fill[1]+($key/count($Data)*(255-$graph->fill[1])));
$Data[$key]->color[2]=round($graph->fill[2]+($key/count($Data)*(255-$graph->fill[2])));

$Data[$key]->degree=360*$Data[$key]->percent;
$Data[$key]->start['x']=$graph->cx+round(cos(deg2rad($intDegreeShift)) * $graph->rx,3);
$Data[$key]->start['y']=$graph->cy+round(sin(deg2rad($intDegreeShift)) * $graph->ry,3);
$Data[$key]->end['x']=$graph->cx+round(cos(deg2rad($intDegreeShift+$Data[$key]->degree)) * $graph->rx,3);
$Data[$key]->end['y']=$graph->cy+round(sin(deg2rad($intDegreeShift+$Data[$key]->degree)) * $graph->ry,3);
$intDegreeShift+=$Data[$key]->degree; //increase degree shift

$boolIsLargeArc=$Data[$key]->degrees>180? 1 : 0;
echo "\n".'<path d="M'.$graph->cx.','.$graph->cy.' L'.$Data[$key]->start['x'].','.$Data[$key]->start['y'].' A'.$graph->rx.','.$graph->ry.' 0 '.$boolIsLargeArc.',1 '.$Data[$key]->end['x'].','.$Data[$key]->end['y'].' z" style="stroke:black;stroke-width: 0;fill:rgb('.implode(',',$Data[$key]->color).');fill-opacity: 1;"/>';
}

Если посмотреть на график внимательно, то видно что объёмность делается при помощи градиента, который в SVG мы сначала зальём в качестве фона, а поверх будем накладывать уже конкретные цвета сектора с прозрачностью:

<defs>
<linearGradient id="MyGradient">
<stop offset="5%" stop-color="rgb(65,65,65)"/>
<stop offset="35%" stop-color="rgb(200,200,200)"/>
<stop offset="95%" stop-color="rgb(65,65,65)"/>
</linearGradient>
</defs>

Основные проблемы

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

 

Абсолютный текст

Кроме того вопрос возникает как располагать текстовые описания секторов? Google попросту ведёт линии с середин секторов, даже если текст не умещается в одну строчку. В SVG позиционировать текст надо от верхнего левого угла — что уже проблема, поскольку ширины мы не знаем. Выхода два — отказаться от относительного позиционирования текста и сделать выноски цветом, либо использовать текст с фиксированной шириной (monospace, Courier) и на этой основе расчитывать длину в пикселях и положение на лету. Благо я наткнулся на параметр text-anchor:end, который странным образом развернул текст как надо.

Интерактивность и будущее

Благодаря интеграции с javascript’ом, можно соответсвенно в реальном времени при помощи AJAX’а обновлять изображение. В моём случае это необходимо когда пользователь голосует и надо обновить распределение голосов на графике.

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