Один подход к фильтрации интерактивной информационной панели D3

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

Дизайн приборной панели

Идея этого дизайна приборной панели была основана на открытии этого Tableau Project, сделанном David Siege, с которым я столкнулся, когда помогал студенту найти набор данных, основанный на баскетбольных площадках в Нью-Йорке. Набор данных был ограничен только Манхэттеном и Бруклином. Однако это была хорошая отправная точка, и, поскольку я работал с Tableau в прошлом, я знал, что мы сможем получить доступ к существующим данным, загрузив проект. Как только я начал взаимодействовать с приборной панелью, я подумал, что это будет отличный проект для воссоздания с помощью D3.

Сам дизайн включал несколько разделов, каждый из которых позволял пользователю взаимодействовать с визуализацией. Это повлекло за собой присоединение к каждому элементу прослушивателей событий D3 .on (), которые будут вызывать соответствующие функции фильтра. Хотя было несколько явно отличных фильтров, панель инструментов также позволяет пользователю взаимодействовать с отдельными элементами, которые сами по себе могут инициировать повторную визуализацию данных. Я считаю, что путем проб и ошибок я реализовал надежный подход к отслеживанию возможных вариантов фильтрации.

Дизайн фильтра

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

  • Легенда
  • Найти суд
  • Выберите район

Пользователь также может взаимодействовать с помощью следующего:

  • Щелчок по конкретному корту на карте или столбчатой ​​диаграмме
  • Нажатие на конкретный корт в разделах «Обязательно к просмотру» и «Держитесь подальше»
  • Наведите указатель мыши на конкретный корт или парк, чтобы вызвать всплывающую подсказку.

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

let filters = [
  {key:'Overall court grouping',value:''},
  {key:'Borough',value:''},
  {key:'Name',value:''}

Функции, отвечающие за фильтрацию набора данных на основе активации фильтра, будут:

  1. findActiveFilters (): возвращает массив только активных фильтров.
  2. runFilter (): возвращает массив отфильтрованных данных на основе одного значения фильтра.
  3. filterData (): вызывает findAcitveFilters и передает массив элементов и единственный фильтр в runFilter. Затем он возвращает последний массив отфильтрованных элементов.

Функция findActiveFilters () перебирает массив фильтров и возвращает только те фильтры, которые имеют значение.

function findActiveFilters() {
  return filters.filter(d => d.value);
}

Функция runFilter (arr, filter) выполняет фактическую фильтрацию и возвращает новый массив отфильтрованных значений. Он будет вызываться для каждого активного фильтра и передавать массив, который должен быть отфильтрован.

function runFilter(arr,filter){
  return arr.filter( d => {
    return d[filter.key] == filter.value
  })
}

Функция filterData () вызывает функцию findActiveFilters (), чтобы определить, какие фильтры активны, и сохраняет эти результаты в activeFilters . Затем он перебирает массив и передает runFilter () массив allData в первом цикле и массив filterData в каждом дополнительном цикле. Этот процесс продолжает сокращать количество элементов на основе последующих фильтров.

function filterData() {
  let filteredData = [];
  let activeFilters = findActiveFilters();
  activeFilters.forEach(d => {
    if (filteredData.length == 0) {
      filteredData = runFilter(allData, d);
    } else {
      filteredData = runFilter(filteredData, d);
    }
  });
  return filteredData;
}

Инициализация фильтра легенды

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

Инициализация фильтра легенды требует присоединения события .on (‘click’, filterLegend) к каждой отображаемой легенде. Обратный вызов здесь, filterLegend, является специальной функцией фильтра, и ему передается фактический элемент, привязанный к этому объекту, благодаря привязке данных D3.

let legends = gLegends.enter().append('g')
    .on('click', filterLegend)

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

let legends = gLegends.enter().append('g')
    .on('click', d => filterLegend(d)

Внутри функции filterLegend () мы сначала оцениваем, есть ли необходимость в сбросе значений легенды, если пользователь щелкнул ту же легенду 2 раза подряд. if реализует эту логику, устанавливает filter [0] .value в пустую строку и повторно запускает renderLegend (legend.domain ()) функция, передавая ей массив значений легенды.

let legend = d3.scaleOrdinal()
  .domain(["Very Good", "Mediocre", "Poor"])
  .range(["#008000", "#FF9933", "#003399"]);
function filterLegend(legendVal){
  if(filters[0].value == legendVal) {
    filters[0].value = ''
    renderLegend(legend.domain())
  } else {
      filters[0].value = legendVal
      renderLegend([legendVal])
  } 
  clearFilterParkValue()
  findActiveFilters().length ? 
     showFilteredData(filterData()) :  showAllData();
  filterBarChartBasedOnLegend()
}

Затем else отвечает за установку filter [0] .value выбранного значения и последующий вызов renderLegend ([legendVal]) как массив только одного значения легенды. Функция renderLegend () - это то, что изначально визуализировало легенду, и она была настроена для использования жизненных циклов D3 update и exit для изменения прозрачности легенды в зависимости от того, какая из них активна.

Кроме того, также вызываются следующие функции:

  • вызовите clearFilterParkValue (), чтобы сбросить все фильтры, специфичные для парка.
  • тернарный оператор для вызова showFilteredData () или showAllData ()
  • вызовите filterBarChartBasedOnLegend (), чтобы сбросить гистограмму

Дополнительные функции фильтра

Есть еще несколько дополнительных функций типа фильтра, которые были созданы для управления фильтрацией. Я упомяну два дополнительных: filterBorough () и filterPark (). Оба по сути делают то же самое, что и filterLegend (), однако делают это специально в контексте этих фильтров. Я привел их ниже в качестве справки.

function filterBorough(boroughVal) {
  filters[2].value = "";
  if (boroughVal == "all") {
    filters[1].value = "";
    showAllData()
  } else {
    filters[1].value = boroughVal;
    filteredData = allData.filter(d => d.Borough == boroughVal)
    renderBarChart(nestingData(filteredData));
    renderTopParks(filteredData)
    renderBottomParks(filteredData)
  }
  findActiveFilters().length ? 
     showFilteredData(filterData()) : showAllData();
  clearFilterParkValue()
}
function filterPark(parkVal) {
  if(filters[2].value == parkVal.Name ) {
     clearFilterParkValue()
  } else {
    filterParkValue(parkVal)
  }
  findActiveFilters().length ? 
    showFilteredData(filterData()) :  showAllData();
}

Вывод

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

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