One key requirement needed in any interactive D3 dashboard, is the implementation of logic to render the data based on the user’s choice of one or more applied filters. Its design should be flexible, and provide the ability to include additional filters based on user requirements.

Dashboard Design

The idea for this dashboard design was based on the discovery of this Tableau Project by David Siege, which I came across while helping a student find a dataset based on basketball courts in NYC. The dataset was limited to just Manhattan and Brooklyn. However, it was a good starting point and being that I’ve worked with Tableau in the past, I knew we would be able to access the existing data by downloading the project. Once I began interacting with the dashboard, I had the thought this would be a great project to recreate using D3.

The design itself included several sections, each of which allowed the user to interact with the visualization. This entailed attaching D3 .on() event listeners to each element which would call the appropriate filter function(s). Although there were several visibly distinct filters, the dashboard also allows the user to interact with individual elements which, by themselves may initiate a re-render of the data. Through some trial and error I believe I implemented a solid approach to keeping track of the possible filter options.

Filter Design

The first step in working through the filtering logic was to decide which filters would be incorporated in the final design. Below are the clearly defined and filters available to the user:

Legend

Find A Court

Select A Borough

The user is also able to interact via the following:

Clicking on a specific court on the map or bar chart

Clicking on a specific court in Must See and Stay Away sections

Mouseover a specific court or park to initiate a tooltip

Based on the requirements, I decided to structure the filter options as an array of objects. Each object contains a key that was set to the filter name and a value that would be initially set to an empty string and then updated based on user interaction.

let filters = [

{key:'Overall court grouping',value:''},

{key:'Borough',value:''},

{key:'Name',value:''}

The functions responsible for filtering the dataset based on activating the filter would be:

findActiveFilters(): returns an array of only the active filters runFilter(): returns an array of filtered data based on single filter value filterData(): calls findAcitveFilters and passes the array of items and single filter to runFilter. It then returns the final array of filtered elements

The findActiveFilters() function loops over the filters array and returns only those filters that have a value.

function findActiveFilters() {

return filters.filter(d => d.value);

}

The runFilter(arr, filter) function is doing the actual filtering and returning a new array of filtered values. It will be called for each active filter and passed the array that is to be filtered.

function runFilter(arr,filter){

return arr.filter( d => {

return d[filter.key] == filter.value

})

}

The filterData() function calls the findActiveFilters() function to determine which filters are active and stores those results in activeFilters. It then loops over the array and passes runFilter() the allData array in the first loop and the filteredData array in each additional loop. This process continues to reduce the number of elements based on subsequent filters.

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;

}

Initializing The Legend Filter

With the filtering system in place it was now up to the individual filters, such as the legend, to first call their supporting filtering functions ,which perform actions specific to that filter. Once complete it then calls the filterData() function.

Initializing the legend filter requires attaching a .on(‘click’, filterLegend) event to each rendered legend. The callback here, filterLegend, is the filter specific function and is passed the actual element bound to that object, courtesy of D3’s Data Binding.

let legends = gLegends.enter().append('g')

.on('click', filterLegend)

Although you don’t see the element being passed in the above configuration, if we reconfigured it, as seen below, then it would be clear that filterLegend is indeed being passed the element directly.

let legends = gLegends.enter().append('g')

.on('click', d => filterLegend(d)

Inside the filterLegend() function we first evaluate if there is any need to reset the legend values if the user clicked on the same legend 2x in sequence. The if implements that logic, sets filters[0].value to an empty string, and reruns the renderLegend(legend.domain()) function passing it the array of legend values.

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() }

The else is then responsible for setting the filter[0].value to the chosen value and then calling renderLegend([legendVal]) as an array of only one legend value. The renderLegend() function is what rendered the legend initially and was configured to leverage D3’s update and exit lifecycles to transition the opacity of the legend depending on which one is active.

Additionally the following functions are also called:

call clearFilterParkValue() to reset all the park specific filters

to reset all the park specific filters ternary operator to call either showFilteredData() or showAllData()

or call filterBarChartBasedOnLegend() to reset the bar chart

Additional Filter Functions

There are several more additional filter type functions that have been created to control filtering. Two additional ones I will mention here are filterBorough() and filterPark(). Both essentially do the same as filterLegend(), however, do so within the context of those filters specifically. I’ve included them below as a reference.

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();

}

Conclusion

The overall filter functionality took quite a bit of time to work through as there were several nuances to each one. Several supporting functions were configured to support each filter, which may in turn call their own supporting functions. In the end, a major refactor would be needed to make it reusable for a different dataset which in the end I’ll take into consideration when building out the next dashboard.

Here is the link to the ongoing project. If anyone has any feedback, suggestions or ideas on how to streamline the process even further, I’d love to hear from you.