Skip to content

09. Creating an interaction

Chazzers edited this page Nov 29, 2019 · 2 revisions

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.

Adjusting the SPARQL query

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.

Creating input options

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;
}

Passing the right values to the query

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 };

Placing the chart in the right container

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}`);
}

Creating the chart

If you want to read about how i created the chart, see chapter 8: Visalizing the data

Updating the chart

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

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

Update

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.

Making colors per category consistent through updates

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]
	});

The making of a legend

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.

image