У наших друзей в «Кинжале» вышла статья про выгорание и колесо баланса, в которой было много красивых визуализаций сфер жизни. Колесо баланса — это такая техника, когда круг делится на несколько секторов, по одному на каждую сферу жизни: семью, здоровье, деньги и всё такое. Смысл колеса в том, что оно позволяет наглядно увидеть, в каких сферах есть перекос и что нужно чинить первее всего.
В статье все картинки — красивые, но статичные. Сегодня мы используем Vue.js, чтобы их оживить — сделать колесо интерактивным и удобным в использовании. Вот что у нас получится в итоге:
Что нам понадобится
Так как мы будем использовать Vue.js, то нам нужно будет прописать логику поведения и разместить элементы на странице, а остальное фреймворк сделает сам.
Элементы, которые мы будем использовать:
- Область для рисования, где будет отображаться колесо баланса. Используем для этого SVG-графику — ей легко управлять из скриптов.
- Раздел с управлением — слайдеры со значениями параметров.
- Кнопка для удаления элемента — если какой-то параметр не нужен.
- Форму для добавления новых параметров, в дополнение к стандартным.
- Блок для вывода всех значений элементов — для управления оно не нужно, но выглядит классно и подключается за минуту. В этом и мощь Vue.js.
В итоге у нас должен получиться интерфейс для управления колесом баланса на веб-странице. Используем стандартный шаблон страницы и добавим в него все эти элементы.
<!DOCTYPE html>
<html lang="ru" >
<head>
<meta charset="UTF-8">
<title>Колесо баланса на Vue.js</title>
<body>
<!-- основной блок, внутри которого будет всё остальное -->
<div id="demo">
<!-- рисуем многоугольник в области заданного размера -->
<svg width="250" height="250">
<polygraph :stats="stats"></polygraph>
</svg>
<!-- управление колесом баланса -->
<div v-for="stat in stats">
<!-- выводим название элемента -->
<label>{{stat.label}}</label>
<!-- показываем шкалу -->
<input type="range" v-model="stat.value" min="0" max="100" />
<!-- выводим текущее значение элемента -->
<span>{{stat.value}}</span>
<!-- кнопка для удаления элемента -->
<button @click="remove(stat)" class="remove">❌</button>
</div>
<!-- форма для добавления нового элемента на колесо баланса -->
<form id="add">
<!-- поле ввода -->
<input name="newlabel" v-model="newLabel" />
<!-- кнопка -->
<button @click="add">Добавить направление</button>
</form>
<!-- выводим сбоку все элементы и их значения -->
<pre id="raw">{{ stats }}</pre>
</div>
<!-- подключаем Vue.js -->
<script src="https://unpkg.com/vue@2"></script>
</body>
</html>
Директивы и декораторы
Обратите внимание в коде на команды, которые начинаются с v- — v-for и v-model. Директивы добавляют особые свойства блокам, в котором они прописаны.
Например, директива v-for может использоваться для организации цикла — она переберёт все элементы из указанного источника и для каждого сформирует один и тот же набор тегов, но со своими параметрами. Это произойдёт только при загрузке страницы в браузер — до этого момента на странице будет только один элемент, как раз то, что мы видим на скриншоте выше.
Директива v-model двунаправленно связывает элементы между собой. Двунаправленно — значит изменения в одном будут влиять на другой элемент. В нашем случае мы, например, связываем значение слайдера и текстового поля.
Получается, что нам достаточно указать, что мы хотим сделать с HTML-элементами на странице, а Vue.js сделает остальное за нас.
Декораторы — это то, что начинается с символа @, например @click. Задача декоратора — взять существующую функцию, немного её переделать внутри и вернуть результат её работы. Мы используем декоратор @click="remove(stat)", чтобы добавить обработчик события onclick(), привязать к нему функцию remove и передать в неё параметр stat.
Добавляем шаблоны
Ещё одно преимущество Vue.js — возможность использовать шаблоны. Смысл в том, что если нам нужно определить на странице много однотипных элементов, например подписей на колесе баланса, то можно сделать шаблон и скормить его фреймворку. Vue.js возьмёт шаблон и сгенерирует по нему много HTML-кода, причём каждому элементу добавит свои уникальные свойства.
Используем шаблоны, чтобы описать два повторяющихся элемента на странице: подписи на колесе баланса и углы многоугольника, который лежит внутри колеса. Прямо сейчас они ничего не поменяют, но пригодятся дальше:
<!-- шаблон для каждого элемента колеса баланса -->
<script type="text/x-template" id="polygraph-template">
<!-- используем этот тег для группировки SVG-элементов -->
<g>
<!-- внутри — обычный HTML, где мы задаём параметры многоугольника -->
<polygon :points="points"></polygon>
<!-- параметры круга -->
<circle cx="100" cy="100" r="80"></circle>
<!-- параметры размещения элементов -->
<axis-label
v-for="(stat, index) in stats"
:stat="stat"
:index="index"
:total="stats.length">
</axis-label>
</g>
</script>
<!-- шаблон для подписи каждого элемента в колесе баланса -->
<script type="text/x-template" id="axis-label-template">
<!-- координаты зависят от координат соответствующей точки многоугольника -->
<text :x="point.x" :y="point.y">{{stat.label}}</text>
</script>
Создаём переменные и компоненты
Нам понадобятся стартовые значения на колесе баланса — насколько по шкале от 0 до 100 мы оцениваем состояние каждой области жизни. Можно всё оставить по нулям и вручную управлять каждым значением, но проще отталкиваться от чего-то уже готового.
Также нам понадобится один компонент polygraph — мы его сделаем сами и будем использовать в основном приложении Vue.js. Задача этого компонента — отрисовывать содержимое колеса баланса при изменении любого из параметров.
Компонент состоит из нескольких частей:
- свойств — переменных, которые понадобятся для работы компонента;
- шаблонов — их мы описали выше;
- вычисляемых значений — что будет меняться при работе компонента;
- внутренних компонентов — их можно вкладывать друг в друга и получать компоненты с любым уровнем сложности.
Чуть выше, когда мы описывали многоугольник в SVG-области, то использовали тег <polygraph>. Это как раз оно — с помощью Vue.js мы создали новый тег и объяснили, как он должен работать.
<script>
// элементы на старте
var stats = [
{ label: "Карьера", value: 100 },
{ label: "Саморазвитие", value: 46 },
{ label: "Друзья", value: 100 },
{ label: "Здоровье", value: 90 },
{ label: "Хобби", value: 80 },
{ label: "Деньги", value: 100 },
{ label: "Отдых", value: 40 },
{ label: "Семья", value: 55 }
];
// создаём новый компонент Vie.js для отрисовки многоугольника
Vue.component("polygraph", {
props: ["stats"],
// используем шаблон
template: "#polygraph-template",
// раздел с вычисляемыми значениями
computed: {
// получаем положение каждой вершины многоугольника
points: function() {
var total = this.stats.length;
return this.stats
.map(function(stat, i) {
// пересчитываем значение элемента в круговые координаты
var point = valueToPoint(stat.value, i, total);
return point.x + "," + point.y;
})
.join(" ");
}
},
// внутренние компоненты
components: {
// описываем характеристики и поведение надписей на колесе баланса
"axis-label": {
props: {
stat: Object,
index: Number,
total: Number
},
// используем свой шаблон
template: "#axis-label-template",
computed: {
point: function() {
return valueToPoint(
// смещаем положение надписи на 10 пикселей, чтобы она не прилипала к вершинам многоугольника
+this.stat.value + 10,
this.index,
this.total
);
}
}
}
}
});
// преобразовываем значение элемента в круговые координаты
function valueToPoint(value, index, total) {
var x = 0;
var y = -value * 0.8;
// находим угол
var angle = ((Math.PI * 2) / total) * index;
var cos = Math.cos(angle);
var sin = Math.sin(angle);
// используем тригонометрию и считаем координаты вершины многоугольника
var tx = x * cos - y * sin + 100;
var ty = x * sin + y * cos + 100;
return {
x: tx,
y: ty
};
}
</script>
Переводим значения в координаты
У нас есть параметры и их значения, а теперь нам нужно научить страницу переводить эти значения в координаты на колесе баланса. Для этого вспомним школьную тригонометрию, синусы и число пи:
// преобразовываем значение элемента в круговые координаты
function valueToPoint(value, index, total) {
var x = 0;
var y = -value * 0.8;
// находим угол
var angle = ((Math.PI * 2) / total) * index;
var cos = Math.cos(angle);
var sin = Math.sin(angle);
// используем тригонометрию и считаем координаты вершины многоугольника
var tx = x * cos - y * sin + 100;
var ty = x * sin + y * cos + 100;
return {
x: tx,
y: ty
};
}
Создаём новое приложение
Чтобы всё заработало, нужно создать приложение — объект класса Vue — и привязать его к элементу на странице. Сразу добавим в него два метода — add, который добавит новый параметр, и remove, который удалит то, что уже не нужно.
Прелесть фреймворка в том, что даже в методах мы привязываем их выполнение к определённым тегам на странице — например к кнопкам. Как только мы нажмём на кнопку, сработает метод, который сам выяснит, какая кнопка нажата и как нужно на неё отреагировать. Мы просто задаём логику работы и поведения, не думая о том, как именно Vue.js будет ловить события.
// создаём новое приложение Vue.js
new Vue({
// привязываем его к блоку на странице по id
el: "#demo",
// стартовые значения приложения
data: {
newLabel: "",
stats: stats
},
// объявляем методы — что будет уметь наше приложение
methods: {
// добавить новый элемент
add: function(e) {
e.preventDefault();
// если не нажата кнопка «Добавить направление» — выходим из метода
if (!this.newLabel) return;
this.stats.push({
// иначе — добавляем новый элемент и присваиваем ему значение по умолчанию
label: this.newLabel,
value: 100
});
// очищаем поле ввода
this.newLabel = "";
},
// удалить элемент
remove: function(stat) {
// если элементов больше трёх
if (this.stats.length > 3) {
// удаляем выбранный
this.stats.splice(this.stats.indexOf(stat), 1);
} else {
// иначе выводим сообщение
alert("Должно остаться как минимум три направления");
}
}
}
});
Добавляем красоту
Последнее, что осталось сделать, — добавить стили, чтобы все элементы стали на свои места, а интерфейс получился красивым:
<!-- стилей мало, поэтому добавим их в этот же файл -->
<style type="text/css">
body {
font-family: Helvetica Neue, Arial, sans-serif;
}
/* настройки многоугольника в колесе */
polygon {
/* цвет заливки и прозрачность */
fill: #42b983;
opacity: 0.75;
}
/* настройки круга */
circle {
/* делаем его прозрачным и добавляем рамку */
fill: transparent;
stroke: #999;
}
/* настройки текстовых меток в колесе баланса */
text {
font-family: Helvetica Neue, Arial, sans-serif;
font-size: 10px;
fill: #666;
}
/* настроки текста под колесом */
label {
display: inline-block;
margin-bottom: 10px;
margin-left: 10px;
width: 120px;
}
/* настройки блока с описанием параметров слева от колеса */
#raw {
/* задаём абсолютное позиционирование и сдвигаем блок вправо от колеса */
position: absolute;
top: 0;
left: 370px;
}
</style>
Посмотреть колесо баланса на странице проекта.