DevGang
Авторизоваться

Как реализовать тепловую карту в таблицах с помощью директив в Angular 

Давайте посмотрим, насколько просто добавить тепловые карты в таблицы в Angular с помощью директив. Мы выберем действительно простое и элегантное решение, чтобы иметь отдельные цвета тепловой карты для разных столбцов в таблице.

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

Тепловые карты в таблице

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

Почему директивы?

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

Логика тепловой карты

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

Итак, если у нас есть набор чисел:

[1,2,3,4,5,6,7,8,9,10]

Здесь в зависимости от значения мы можем управлять интенсивностью цвета. Значением 1 будет самый светлый оттенок цвета и 10 сам цвет. Итак, нам просто нужно сопоставить значения с интенсивностью цветов здесь. У нас также может быть противоположное условие.

Есть разные способы реализовать это.

Использование альфа-канала

Мы можем легко реализовать тепловые карты с использованием RGBA или HSLA, просто изменив альфа-канал, означающий прозрачность цвета.

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

Использование цветового выражения HSL

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

HSL означает HueSaturationLightness, и у него также может быть Alpha канал с HSLA

Идея состоит в том, чтобы найти коэффициент Lightness для каждого значения. Вот как мы можем это сделать.

Итак, здесь исходное значение цвета сначала анализируется в HSLA:

hsla(234, 77%, 46%, 1) -> Lightness = 46%

У нас есть минимально возможное значение Lightness, то есть 0,46. Таким образом, самое высокое значение будет иметь яркость 46%, а для других значений оно будет выше. Когда легкость увеличивается, она приближается к White.

Вот формула:

const color = '#1b2dd0';
const [h,s,l,a] = parseHSLA(color); // <-- [234, 0.77,0.46,1]
const highestValue = 10;
const maxLightness = 1 - l; // <-- 1 - 0.46 = 0.54

const lightness = 1 - (value * maxLightness / highestValue);

// 1 --> 1 - (1 * 0.54 / 10) = (1 - 0.05) ~ 95% 
// 5 --> 1 - (5 * 0.46 / 10) = (1 - 0.23) ~ 77%
// 10 -> 1 - (10 * 0.54 / 10) = (1 - 0.54) ~ 46%

Здесь 10 будет наименьшим числом, поэтому нам нужен очень светлый цвет, поэтому 95% будут очень светлыми. Lightness % по мере увеличения делает цвет белее.

Итак, теперь у нас есть логика, давайте начнем с директив!

Создание директив тепловой карты

Итак, я упомянул «Директивы» (во множественном числе), поскольку мы будем создавать несколько директив для этой функции. А точнее их 3. Два из трех предназначены только для маркировки элемента и установки некоторых метаданных:

  1. Таблица тепловой карты
  2. Столбец тепловой карты
  3. Ячейка тепловой карты

Вот как мы будем использовать директивы в шаблоне:

<table heatMapTable>
  <tr>
    <th>Company</th>
    <th>Manager</th>
    <th [heatMapColumn]="options.employees"  id="employees">
        Employees
    </th>
    <th [heatMapColumn]="options.contractors" id="contractors">
        Contractors
    </th>
  </tr>
  <ng-container *ngFor="let item of data">
    <tr>
      <td>{{ item.company }}</td>
      <td>{{ item?.manager }}</td>
      <td [heatMapCell]="item.employees" id="employees">
         {{ item?.employees }}
      </td>
      <td [heatMapCell]="item.contractors" id="contractors">
        {{ item?.contractors }}
      </td>
    </tr>
  </ng-container>
</table>

Директива ячейки тепловой карты

@Directive({
  selector: '[heatMapCell]',
})
export class HeatmapCellDirective {
  @Input('heatMapCell')
  heatMap = 0;

  @Input('id')
  colId = null;

  constructor(public el: ElementRef<HTMLElement>) {}
}

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

Директива столбца тепловой карты

@Directive({
  selector: '[heatMapColumn]',
})
export class HeatmapColumnDirective {
  @Input('id')
  colId = null;

  @Input('heatMapColumn')
  options = {};
}

Здесь мы можем передать параметры стиля, такие как цвет и т.д., а также идентификатор столбца.

Директива таблицы тепловой карты

Это основная директива, по которой выполняется вся работа. Эта директива выложена в таблицу. А остальные директивы размещаются в столбце и ячейках.

Здесь мы можем увидеть, как мы можем получить доступ к дочерним директивам из родительской директивы с помощью ContentChildren.

@Directive({
  selector: '[heatMapTable]',
})
export class HeatmapTableDirective implements AfterViewInit {
  @ContentChildren(HeatmapCellDirective, { descendants: true })
  heatMapCells: QueryList<HeatmapCellDirective>; // <-- Get all the cells
  @ContentChildren(HeatmapColumnDirective, { descendants: true })
  heatMapColumns: QueryList<HeatmapColumnDirective>; // <-- Get all the columns

  highestValues = {};
  cells: HeatmapCellDirective[] = [];
  columns: HeatmapColumnDirective[] = [];
  config = {};

  ngAfterViewInit() {
    this.cells = this.heatMapCells.toArray();
    this.columns = this.heatMapColumns.toArray();
    this.setOptions();
    this.calculateHighestValues();
    this.applyHeatMap();
  }

  private setOptions() {
    this.columns.forEach((col) => {
      this.config = {
        ...this.config,
        [col.colId]: col.options,
      };
    });
  }

  private calculateHighestValues() {
    return this.cells.forEach(({ colId, heatMap }) => {
      if (!Object.prototype.hasOwnProperty.call(this.highestValues, colId)) {
        this.highestValues[colId] = 0;
      }
      if (heatMap > this.highestValues?.[colId])
        this.highestValues[colId] = heatMap;
    });
  }

  private applyHeatMap() {
    this.cells.forEach((cell) => {
      const { bgColor, color } = this.getColor(cell.colId, cell.heatMap);
      if (bgColor) cell.el.nativeElement.style.backgroundColor = bgColor;
      if (color) cell.el.nativeElement.style.color = color;
    });
  }

  private getColor(id: string, value: number) {
    const color = this.config[id].color;
    let textColor = null;
    let bgColor = null;
    if (color != null) {
      const [h, s, l, a] = parseToHsla(color);
      const maxLightness = 1 - l;
      const percentage = (value * maxLightness) / this.highestValues[id];
      const lightness = +percentage.toFixed(3);
      bgColor = hsla(h, s, 1 - lightness, a);
      textColor = readableColor(bgColor);
    }
    return {
      bgColor,
      color: textColor,
    };
  }

Позвольте мне разобрать код.

Получите доступ к ячейкам и столбцам

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

@ContentChildren(HeatmapCellDirective, { descendants: true })
  heatMapCells: QueryList<HeatmapCellDirective>;

Эта переменная heatMapCells будет иметь список td, к которому был применен heatMapCell. Обязательно установите { descendants: true }.

Примечание. Если true, включить всех потомков элемента. Если false, то запрашивать только прямые дочерние элементы элемента.

Сохраните параметры для каждого столбца

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

config = {
    "employees": {
        "color": "#000fff"
    },
    "contractors": {
        "color": "#309c39"
    }
}

Рассчитайте максимальное значение для каждого столбца

Теперь мы можем вычислить максимальное значение для каждого столбца и сохранить его в объекте с ключом colId.

highestValues = {
   employees: 1239,
   contractors: 453
}

Применение стилей тепловой карты

Теперь мы можем перебрать ячейки, а затем применить backgroundColor и color к ячейке. Поскольку мы ввели ElementRef в ячейку, мы можем использовать свойство el для изменения стилей:

cell.el.nativeElement.style.backgroundColor = 'blue';

У нас есть вспомогательная функция, которая находит цвет для каждой ячейки на основе логики, которую мы обсуждали выше:

  private getColor(id: string, value: number) {
    const color = this.config[id].color;
    let textColor = null;
    let bgColor = null;
    if (color != null) {
      const [h, s, l, a] = parseToHsla(color);
      const maxLightness = 1 - l;
      const percentage = (value * maxLightness) / this.highestValues[id];
      const lightness = +percentage.toFixed(3);
      bgColor = hsla(h, s, 1 - lightness, a);
      textColor = readableColor(bgColor);
    }
    return {
      bgColor,
      color: textColor,
    };
  }

Управление цветом выполняется с помощью очень простой библиотеки color2k, которая предоставляет множество утилит для работы с цветами.

Мы использовали так называемый readableColor() метод, который возвращает черный или белый цвет для наилучшего контраста в зависимости от яркости данного цвета. Это сделает нашу тепловую карту более доступной.

Демо и код

Ссылка на Stackblitz

Последние мысли

Как видите, в компоненте не так много кода. Вся логика прекрасно обработана внутри директивы. Единственный сложный момент в директиве - это поиск цветов. В остальном все просто.

Это очень простая реализация, и она тоже не идеальна. Чтобы сделать его лучше, нам, возможно, придется добавить некоторую проверку и обработку ошибок. Кроме того, это можно расширить, предоставив дополнительные параметры, такие как восходящие / нисходящие тепловые карты, цветовые диапазоны, положительные и отрицательные тепловые карты и многое другое.

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

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу