Skip to content

#7 Creating a bar chart

Rick Groot edited this page Nov 12, 2020 · 1 revision

Bar chart visualisation 📊

Uit mijn feedback van de Volkskrant was naar voren gekomen dat ik misschien ook kan beginnen met een andere visualisatie, en later de map kan laten zien. Hierdoor heb ik besloten om een simpele bar chart te maken, die de hoogtes van parkeergarages makkelijk in beeld brengt. Hiervoor heb ik een aantal dingen nodig.

  • Data van parkeergarage hoogtes
  • Hoeveel keer een hoogte voorkomt
  • De bar chart

X-as

De data van de hoogte van alle parkeergarages ga ik gebruiken om op de X-as van mijn visualisatie te zetten. Dus op de X-as zijn straks waardes te zien van de meest voorkomende doorrij hoogtes. Deze data heb ik al verzameld en gebruik ik ook bij mijn map, dus dit is makkelijk aan te roepen.

//gets called when the data is collected
function barchart(data) {

    x.domain(data.map(function (d) {
        return d.maximumVehicleHeight;
    }));

    barSVG.append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x));
}

Deze code gebruik ik om de data te verkrijgen en appenden. Vervolgens kan ik dit met het volgende stuk code toevoegen aan mijn svg:

//append the rectangles for the bar chart
    barSVG.selectAll(".bar")
        .data(data)
        .enter().append("rect")
        .attr("class", "bar")
        .attr("x", function (d) {
            return x(d.maximumVehicleHeight);
        })
        .attr("width", x.bandwidth())

Deze code staat in dezelfde functie barchart(). Nu hoef ik alleen nog maar de Y-as te krijgen, wat iets moeilijker is als de X-as aangezien ik de data daarvoor nog moet aanpassen.

Y-as

Op de Y-as wil ik graag de aantallen plaatsen van de hoogtes, dus bijvoorbeeld van 2.10 meter zijn er 22 parkeergarages. Hiervoor moet ik eerst alle gelijke hoogtes gaan tellen en in een bruikbaar object plaatsen. Eerst heb ik hiervoor mijn data opgeschoond met de functie filterData(). Deze heb ik al eerder in mijn project aangemaakt, en kan ik nu makkelijk hergebruiken omdat het functioneel is geschreven.

function filterData(dataArray, key) {
    return dataArray.map(item => item[key]); //filters column data from array
}

let heights = filterData(data, 'maximumVehicleHeight');
console.log(d3.count(heights))    //logt een array met daarin alle hoogtes

Door deze code houd ik dus een schone array over met alle hoogtes. Nu moet ik de waardes nog met elkaar vergelijken en samenvoegen tot 1 object, zodat het bruikbaar wordt in mijn visualisatie. Dit heb ik op verschillende manieren geprobeerd.

Met een .count:

console.log(d3.count(heights))
//logs 308, because there are 308 values in the array

Met een .rollup:

console.log(d3.rollup(heights, g => g.reduce((p, v) => (p.value += v.value, p), {...g[0], value: 0}), d => d))
//logs object with amount of double values

Voor de rollup code heb ik wel een voorbeeld gebruikt, die is hier te vinden. Deze manier werkt tot nu toe het best, dus ik ga hiermee verder werken.

.rollup code

Ik heb aan mijn rollup een aantal dingen omgegooid, omdat ik niet de juiste dingen terugkreeg. In plaats van direct een .rollup te gebruiken heb ik nu eerst een .groups en .map toegevoegd, zodat ik de data zo kan manipuleren dat het exact zo wordt als ik het wil. Vervolgens heb ik in de .map names en values toegevoegd. De name is de lengte, en de value hoe vaak deze voorkomt door middel van een simpele .length na een .rollup functie.

console.log(d3.groups(heights, d => d).map(([name, group]) => ({name, value: d3.rollup(group, d => d).length})))
//logs (13) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
//        0: {name: "210", value: 65}
//        1: {name: "205", value: 16}
//        2: {name: "190", value: 75}
//etc.

Nu kan ik deze data gaan invoeren op de Y-as, maar ook op de X-as. Op dit moment heb ik namelijk een heel clean object, waarin de namen en waardes staan. Nu zou ik dus van deze data de "name" op de X-as kunnen gaan zetten, en de "value" op de Y-as.

New X&Y

Om deze nieuwe data in mijn visualisatie te krijgen heb ik mijn bestaande code wat herschreven. De code van de barchart zelf is gebaseerd op dit voorbeeld. Als eerste heb ik ervoor gezorgd dat mijn data op de juiste manier in mijn functie komt. Dit heb ik op de volgende manier gedaan:

function barchart(data) {
    let heights = filterData(data, 'maximumVehicleHeight');
    let barData = mergeValues(heights);
    console.log(barData)    //logs the clean data
}

function mergeValues(data) {
    return d3.groups(data, d => d)
        .map(([name, group]) => ({
            name,
            amount: d3.rollup(group, d => d).length
        }))
}

Nu heb ik mijn goede data binnen de functie, en moet ik dit alleen nog in mijn visualisatie zien te krijgen. In mijn code heb ik eerst een paar variabelen aangemaakt die de width, height en margin bevatten. Hier maak ik geruik van om de barchart dynamischer mee te maken, en dus meer flexibel. Eerst voeg ik de X en Y as toe aan mijn barchart functie, dit doe ik onderaan de functie.

    //add the x Axis
    barSVG.append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x));

    //add the y Axis
    barSVG.append("g")
        .call(d3.axisLeft(y));

Vervolgens zet ik een range voor de data die ik in de visualisatie ga stoppen, op basis van de maximale waarden van mijn data.

    //scale the range of the data in the domains
    x.domain(barData.map(function (d) {
        return d.name;
    }));
    y.domain([0, d3.max(barData, function (d) {
        return d.amount;
    })]);

En tot slot laadt ik al mijn data in de svg die ik al in mijn HTML bestand heb staan.

    //append the rectangles for the bar chart
    barSVG.selectAll(".bar")
        .data(barData)
        .enter().append("rect")
        .attr("class", "bar")
        .attr("x", function (d) {
            return x(d.name);
        })
        .attr("width", x.bandwidth())
        .attr("y", function (d) {
            return y(d.amount);
        })
        .attr("height", function (d) {
            return height - y(d.amount);
        });

Nu staat er een simpele barchart op de pagina. Het enige wat ik nu nog zou kunnen doen is de CSS scheiden van de javascript, omdat het niet handig is om deze door elkaar heen te gebruiken. Ik heb hiervoor de width en de height uit mijn CSS gepakt, waardoor bij elke refresh de chart nu op de goede plek van het scherm staat.

let margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 40
    };
let width = parseInt(getComputedStyle(document.querySelector('#bar')).width) - margin.left - margin.right;
let height = parseInt(getComputedStyle(document.querySelector('#bar')).height) - margin.top - margin.bottom;

Kleuren heb ik ook met CSS gedefinieerd, hetzelfde als de hovers. In de CSS heb ik gewoon een :hover toegepast waardoor de kleur veranderd wanneer er wordt gehoverd. Ik heb wel een tooltip toegepast via javascript, op dezelfde manier als bij mijn map.

Clone this wiki locally