-
Notifications
You must be signed in to change notification settings - Fork 1
09. Creating an interaction
To create an interaction for my datavisualisation I started brainstorming about how i could use my existing concept, which was "Japenese weapons of the NMVW collection plotted in a bubble chart", and create an interaction with it. Quickly i came up with the concept of using dynamic queries to compare the weapons of multiple regions in the world with eachother. To realize this concept i had to adjust my SPARQL query so you could select a region and then fetch the data for that specific region which in turn is then plotted into a bubble chart.
To make sure the right data according to user input was fetched I had to make options for my HTML <select>
elements. So i made a function that would fill the <options>
of my <select>
elements. This function first selects all <select>
elements and then adds options for each select element.
function createOptions(data) {
const selects = document.querySelectorAll(".select");
const options = data.forEach(dataItem => selects.forEach(select => select.add(new Option(dataItem.placeName, dataItem.place1))));
return options;
}
After creating the right options i had to make sure these options would be passed on to the data fetch. I could do this through making an event
parameter in the function down below. The option value is then put into the query when it gets a value and then the fetch starts executing.
function makeQueryWeapons(event) {
const optionValue = event.target.value;
const selectId = event.currentTarget.id;
const query = `
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX edm: <http://www.europeana.eu/schemas/edm/>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?cho ?placeName ?title ?type WHERE {
<${optionValue}> skos:narrower* ?place .
?place skos:prefLabel ?placeName .
VALUES ?type { "zwaard" "Zwaard" "boog" "Boog" "lans" "Lans" "mes" "knots" "Piek" "vechtketting" "dolk" "bijl" "strijdzeis" }
?cho dct:spatial ?place ;
dc:title ?title ;
dc:type ?type .
FILTER langMatches(lang(?title), "ned") .
}
`;
const connectionString = url + "?query=" + encodeURIComponent(query) + "&format=json";
return runQueryWeapons(connectionString, selectId);
}
export { makeQueryWeapons };
To make sure that the chart that the chart is placed in the right location i had to pass a parameter to the function runQueryWeapons. This parameter: selectId
is passed onto runQueryWeapons.
return runQueryWeapons(connectionString, selectId);
The function then passes this variable on to the function createChart.
function runQueryWeapons(connectionString, selectId) {
// json is a method of D3. This method is a promise and therefore can be chained with .then
return json(connectionString)
.then(data => showResults(data))
.then(data => data.map(dataItem => deNestProperties(dataItem)))
.then(data => transformData(data))
.then(data => createChart(data, selectId));
}
export { runQueryWeapons };
The selectId
parameter is then used in the function createChart like this:
function createChart(data, id) {
/*
here ---->*/ const currentChart = select(`#${id}`);
}
If you want to read about how i created the chart, see chapter 8: Visalizing the data
To update the chart when a new data fetch is executed, D3 uses a pattern called: enter, update, exit.
To use this pattern you first bind data to a selection of elements like this:
let node = svg.selectAll(".node")
.data(bubble(nodes).descendants()
.filter(function(d){
return !d.children;
}))
Enter means that you add all the new datapoints and create elements with it. After binding the data to a selection you will tell new elements in the enter selection what to do with the elements like this:
node.join(
enter => {
const nodeEnter = enter.append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
nodeEnter.append("title")
.text(function(d) {
return d.data.key + ": " + Math.round(d.data.amount / nodeTotal * 1000) / 10+ "%";
})
nodeEnter.append("circle")
.attr("r", function(d) {
return d.r;
})
.style("fill", function(d,i) {
return colors[d.data.key]
});
nodeEnter.append("text")
.attr("dy", ".2em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.key.substring(0, d.r / 1);
})
.attr("font-family", "sans-serif")
.attr("font-size", function(d){
return d.r/3;
})
.attr("fill", "white")
}
)
The elements that already exist need to be updated when a data fetch is executed. To do this in your code, you basically copy-paste the enter selection but transform this enter selection to make it so it doesn't add stuff. The update code should only update the dynamic attributes of the elements. The update selection of my enter selection will then look like this:
update => {
update.transition().duration(300)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
update.select("title")
.text(function(d) {
return d.data.key + ": " + Math.round(d.data.amount / nodeTotal * 1000) / 10 + "%";
})
update.select("circle")
.attr("r", function(d) {
return d.r;
})
.style("fill", function(d,i) {
return colors[d.data.key]
});
update.select("text")
.attr("dy", ".2em")
.style("text-anchor", "middle")
.text(function(d) {
return d.data.key.substring(0, d.r / 1);
})
.attr("font-family", "sans-serif")
.attr("font-size", function(d){
return d.r/3;
})
.attr("fill", "white")
}
If you start comparing the two selections you will notice that these two pieces of code look almost the same. This is because enter only enters new elements. This means that after you create a chart twice, enter will basically just add new elements if there are more datapoints, otherwise it won't do anything.
In order to update the existing elements to follow the new data you need to tell the elements what they should contain and look like. This is done with the update function.
The elements.join()
is a new method of D3 to bind and compare datapoints. With this method, exit()
and remove()
are executed automatically.
After testing and seeing how my bubble chart updated on user input, I started noticing that the colors weren't consistent per weapon type. If the weapon type was knife and had a grey color in one of the bubblecharts, in the other the color of the knife bubble was different.
In order to make sure the weapon types all had the same colors when plotted on the chart, i had to make an object where a type has a consistent color assigned to it.
const colors = {
"Bijl": "#1f77b4",
"Boog": "#ff7f0e",
"Dolk": "#9467bd",
"Knots": "#bcbd22",
"Lans": "#e377c2",
"Mes": "#7f7f7f",
"Piek": "#17becf",
"Strijdzeis": "#8c564b",
"Vechtketting": "#d62728",
"Zwaard": "#2ca02c"
}
After which i could determine the color of the bubble by looping over the colors
object through the use of object bracket notation.
nodeEnter.append("circle")
.attr("r", function(d) {
return d.r;
})
.style("fill", function(d,i) {
return colors[d.data.key]
});
After creating my bubble chart and making sure it would be updateable, I thought that a legend would make my visualisation more readable and understandable. So I started working on creating one using the knowledge i had obtained working with D3.
I first created a <div>
element of which the class was: table. The first step to creating the legend is to then select this element, and then select all the nested elements and pass data onto them.
// Select the div with the class: table from the currentChart
const table = currentChart.select(".table")
.selectAll(".table-row")
.data(data)
Next up i had to make an enter selection to make sure when a datafetch is executed, new elements would be created.
table.join(
//enter selection
enter => {
// This creates a div with the class table-row
const tableEnter = enter.append("div")
.attr("class", "table-row")
// This creates a div inside the table-row which contains the color of the element.
tableEnter.append("div")
.attr("class", "legend-color")
.style("background-color", d => colors[d.key])
// This adds a div inside the table-row which contains the weapon type
tableEnter.append("div")
.attr("class", "legend-type")
.append("p")
.text(d => d.key)
// This adds a div inside the table-row which contains the amount of a certain weapon type in the database
tableEnter.append("div")
.attr("class", "legend-amount")
.append("p")
.text(d => Math.round(d.amount / nodeTotal * 1000) / 10 + "%")
}
)
At last i had to make an update selection so the existing elements know what to do with the new data.
table.join(
enter => {/*enter selection*/},
// update selection
update => {
// This selects the table-row
update.select("table-row")
// This selects the div which contains the color
update.select(".legend-color")
.style("background-color", d => colors[d.key])
// This selects the div which contains the weapon type
update.select(".legend-type")
.select("p")
.text(d => d.key)
// this selects the div which contains the amount of a certain weapon type in the database
update.select(".legend-amount")
.select("p")
.text(d => Math.round(d.amount / nodeTotal * 1000) / 10 + "%");
},
)
After this a table would be added when a fetch is executed which looks like this.