Проблема
Поступил запрос на реализацию тепловой карты со следующими условиями по стилизации:
- Цвета ячеек зависят от ранга(значения в строке ранжируются от большего к меньшему, каждому значению присваивается ранг)
- При наведении на элемент в красной зоне в подсказке должен отображаться список показателей, требующих проверки пользователем
Реализация
Подготовка виджета и данных
Для реализации воспользуемся виджетом с типом “HTML” и библиотекой Apache Echarts. Examples - Apache ECharts
- Выбираем тип виджета, в блоке HTML подключаем библиотеку и создаем div-элемент для монтирования диаграммы:
- На вкладке JS будет реализовывать логику отрисовки диаграммы. В первую очередь создаем объект диаграммы через echarts.init. Также для автоматической перерисовки графика при изменении размера окна необходимо вызывать метод resize у экземпляра графика. Для этого зарегистрируем соответствующий обработчик события resize
// Инициализация виджета
var chart = echarts.init(document.getElementById('widget'));
window.addEventListener('resize', function() {
chart.resize();
});
- Для конфигурирования графика используется метод chart.setOption(option). Данный метод будет вызываться в функции render при каждой отрисовке графика.
function render(){
// здесь будем формировать объект option, в соответствии с требованиями
chart.setOption(option);
}
- Перейдем к реализации требований заказчика. В первую очередь получим значения подписей элементов осей. Значения из модели можно получить через глобальный объект window.
- в строки таблицы(группы) добавляем наименования полигонов. Это будут подписи по оси oX.
- в столбцы таблицы(агрегаты) добавляем расчетные значения соответствующих метрик. Названия метрик будут отображаться по оси oY, значение агрегата - это значение в ячейке.
- так как получить значение агрегата в JS можно по названию поля(без кириллицы), то вводим два справочника, в которых пользователь самостоятельно будет задавать текущие используемые поля и их псевдонимы. При добавлении и удалении показателей потребуется внести изменения и в справочники.
// задаем соответствие между показателями и используемыми именами полей в модели
const fieldNamesDictionary = {
group1: 'polygon',
aggr1: 'avg_norm_kpef_z',
aggr2: 'rang_z',
aggr3: 'avg_norm_kpef_ch',
aggr4: 'rang_ch',
}
// прописываем отображаемое название показателя на кириллице
const columnNamesMap = {
[fieldNamesDictionary.aggr1]: 'По затратам (значение)',
[fieldNamesDictionary.aggr2]: 'По затратам (ранг)',
[fieldNamesDictionary.aggr3]: 'По численности (значение)',
[fieldNamesDictionary.aggr4]: 'По численности (ранг)',
}
Через интерфейс объекта window получим значения подписей в константы oX и oY.
// элементы осей
const oX = window.DATA.data.map(item => {
return item[fieldNamesDictionary.group1].value
})
const oY = window.WIDGET.columns
.map(column => {
if (column.field in columnNamesMap) {
return columnNamesMap[column.field]
}
})
.filter(item => Boolean(item))
Преобразуем каждый элемент датасета к объекту с полями name(наименования полигона) и aggr{id}(значение агрегата)
const eachItemToObject = window.DATA.data.map(item => {
const obj = {
name: item[fieldNamesDictionary.group1].value,
};
for (let key in fieldNamesDictionary) {
if (key.includes('aggr')) {
obj[columnNamesMap[fieldNamesDictionary[key]]] = item["level_agg_" + fieldNamesDictionary[key]]
}
}
return obj;
})
Условное форматирование ячеек
Для ранжирования значений ячеек создадим хранилище rangedData
const rangedData = {};
for (let key in fieldNamesDictionary) {
if (key.includes('aggr')) {
// obj[columnNamesMap[fieldNamesDictionary[key]]] = item["level_agg_" + fieldNamesDictionary[key]]
rangedData[columnNamesMap[fieldNamesDictionary[key]]] = [];
}
}
Текущие значения показателей добавляем в хранилище:
eachItemToObject.forEach(item => {
for (let key in item) {
if (key !== 'name') {
rangedData[key].push(item[key])
}
}
});
Сортируем массивы значений:
for (let key in rangedData) {
rangedData[key].sort((a, b) => {
a = Number(a)
b = Number(b)
if (a > b) {
return 1
} else if (a < b) {
return -1
} else return 0
});
};
Логику определения цвета для ячейки реализуем в отдельной функции. Аргументами в функцию передаются название показателя и значение. В примере представлена функция для определения цвета из 16 значений разделенных по 4 группам. Также дополнительно введена проверка для поля ранга.
function getColor(name, value) {
const colorsByIndex = {
1: '#6ce1c3',
2: '#fff1c9',
3: '#fabc74',
4: '#fa5274'
}
const currentIndex = rangedData[name].indexOf(value);
let colorGroup;
let isRank = false;
if (name.indexOf('(ранг)') !== -1) {
isRank = true
}
if (currentIndex < 4) {
colorGroup = isRank ? 1 : 4
} else if (currentIndex < 8) {
colorGroup = isRank ? 2 : 3
} else if (currentIndex < 12) {
colorGroup = isRank ? 3 : 2
} else {
colorGroup = isRank ? 4 : 1
}
return {
color: colorsByIndex[colorGroup]
}
}
Итоговые значения ячеек для передачи в объект option определим в массиве data. Каждая ячейка представлена в виде:
{
value: [значение по oX, значение по oY, значение ячейки],
itemStyle: getColor(значение по oY, значение ячейки)
}
const data = [];
eachItemToObject.forEach(item => {
for (let key in item) {
if (key !== 'name') {
data.push({
value: [item.name, key, item[key]],
itemStyle: getColor(key, item[key])
})
}
}
})
Карта будет окрашена в следующем виде:
Стилизация подсказки
Для определения содержимого подсказки при наведении на элемент у объекта option есть свойство tooltip. В свою очередь у tooltip есть коллбек-свойство formatter, которое аргументом принимает выбранный элемент(на который наведен курсор мыши).
tooltip: {
position: 'bottom',
formatter: (item) => {
if (item.data.itemStyle.color === '#fa5274') {
return `<div>${messageForLowRank}</div>`
}
return `<div>${item.data.value[0]} ${item.data.value[1]} ${item.data.value[2]}</div>`
}
},
Проверим попадает ли элемент в “красную зону”(if (item.data.itemStyle.color === ‘#fa5274’)). Если да, то выведем преднастроенный html-шаблон messageForLowRank.
const messageForLowRank = `<span style="color: #fa5274;">Анализ СЗП в регионе <br></span>
<li>Сравнение СЗП по регионам <br></li>
<li>Сравнение СЗП в отрасли <br></li>
<li>Сравнение СЗП в динамике по регионам <br></li>
<li>Сравнение СЗП в динамике по подразделению ЖД <br></li>
<span style="color: #fa5274;">Тарифная часть <br></span>
<li>Уровень оплаты труда (КСОТ) <br></li>
<li>Уровень особых условий труда <br></li>
<span style="color: #fa5274;">Анализ заработной платы <br></span>
<li>Динамика заработной платы за период <br></li>
<li>Динамика заработной платы по видам оплат</li>`
В противном случае выводим обычную подсказку.
Стоит отметить, что в шаблоне подсказки доступен inline-css, поэтому возможна гибкая настройка стилей в зависимости от пользовательского условия.
Итоговая конфигурация
option = {
tooltip: {
position: 'bottom',
formatter: (item) => {
if (item.data.itemStyle.color === '#fa5274') {
return `<div>${messageForLowRank}</div>`
}
return `<div>${item.data.value[0]} ${item.data.value[1]} ${item.data.value[2]}</div>`
}
},
grid: {
height: '50%',
top: '0%',
left:"200px",
right:"150px"
},
xAxis: {
type: 'category',
data: oX,
splitArea: {
show: true
}
},
yAxis: {
type: 'category',
data: oY,
splitArea: {
show: true
}
},
series: [
{
// name: 'Punch Card',
type: 'heatmap',
data: data,
label: {
show: true
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};