Datapoints - lennartdeknikker/frontend-data GitHub Wiki
Defined in datapoints.js
renderDataPoints()
First, data points were rendered by this function:
function renderObjects(objects) {
g.append('g')
.attr('class', 'g-datapoints')
.selectAll('.datapoint')
.data(objects)
.enter()
.append('circle')
.attr('class', 'datapoint')
.attr('data-place', d => d.placeName)
.attr('data-long', d => d.long)
.attr('data-lat', d => d.lat)
.attr('cx', d => projection([d.long, d.lat])[0])
.attr('cy', d => projection([d.long, d.lat])[1])
.attr('fill', '#00827b')
.attr('fill-opacity', 0.5)
.on('click', objectClickHandler);
}
This function worked fine when the application always had the same input, but now it needs to be able to update the drawn circles based on new data or a different amount of data as well as drawing the circles when there's no data yet. To get that working, I visited D3inDepth to learn more about the enter() and exit() functions and update patterns.
First it's necessary to select all datapoints if any are there instead of appending a new group. It took a while to find out that first appending a new group sabotaged the update pattern. To start, I select the group with class g-datapoints that's already there instead of appending a new one. Then I select all datapoints, bind the new data, reset the radius to 0 to have the circles grow from that radius, using a transition later on and stored the selection in const datapoints:
const datapoints = d3
.select('.g-datapoints')
.selectAll('.datapoint')
.data(objects)
.attr('r', 0);
Then I use the enter() function on that selection to select new elements that need to be added if the new amount of data entries exceeds the amount of circles that's already there. Then I append circles to those entries and merge the selection of those new circles with the selection defined in the datapoints variable using merge(). All datapoints then get the necessary class, other attributes and event listeners.
datapoints
.enter()
.append('circle')
.merge(datapoints)
.attr('class', 'datapoint')
.attr('data-place', d => d.placeName)
.attr('data-long', d => d.long)
.attr('data-lat', d => d.lat)
.attr('cx', d => projection([d.long, d.lat])[0])
.attr('cy', d => projection([d.long, d.lat])[1])
.attr('fill', '#00827b')
.on('mouseover', objectMouseoverHandler)
.on('mouseout', objectMouseoutHandler)
.on('click', objectClickHandler);
Lastly, I use exit() and remove() to delete superfluous circles when the new data has less entries than shown before.
datapoints.exit().remove();
}
UpdateDataPoints()
Now the update pattern to render data is in place, I needed to rewrite the function that updates the data points. This function checks if there's a group containing data points and creates one if there isn't one there already.
// If necessary creates a new group for drawing datapoints,
function updateDataPoints(endpoint, query, g, projection, settings) {
if (!document.querySelector('.g-datapoints')) {
g.append('g').attr('class', 'g-datapoints');
}
Then the function uses the query to get the data from the endpoint and calls the renderDataPoints() function on the then transformed data:
// then obtains new data from the server,
d3.json(`${endpoint}?query=${encodeURIComponent(query)}&format=json`).then(
objects => {
// then (re)renders the datapoints.
renderDataPoints(
transformData(objects.results.bindings, settings),
g,
projection,
settings
);
}
);
}