Skip to content

Integrating D3 in Vue

qiubee edited this page Jan 26, 2021 · 4 revisions

In the article Creating an app with Vue I talked about adding components to the Article component. The two components ProvinceMap and MunicipalityMap weren't covered there because I wanted to go into more detail. I'll explain here how these two components exactly work and how I use the D3 library to create the visualizations.

Parking in provinces of The Netherlands

The component ProvinceMap is the first component I created. In this component I first copied the HTML I created from the data visualization repository where I earlier created the visualization. I added a <div> element with an ID provinces and added a empty <svg> element. Then I added the form and a <select> element, but instead of creating the <option> elements myself I wanted to Vue dynamically create the elements from the data. To do that I used v-for to iterate over each item in the options data and add the text, key, id and have the selected option to either be the first element or the element selected and saved in LocalStorage. On the <select> element I added a OnChange event listener that updates the map based on the selection.

<template>
    <div id="provinces">
        <svg></svg>
        <p>Bron: <i>CBS, RDW</i>.</p>
        <form>
            <label>De verdeling van het
                <select @change="updateMap" name="parking">
                    <option v-for="item in options" :value="item.value" :data-key="item.key" :key="item.id"
                        :selected="mapSelection === item.key">
                        {{ item.text }}
                    </option>
                </select>
            </label>
        </form>
    </div>
</template>

Then in the <script> section I had to get the data and create the visualization. First I added the mounted() method, because I wanted to have the visualization created when it's mounted to the DOM. I then have full control over the component. Inside the method I added the functions I created in the datavisualization repository.

I add the functions setupMap, drawMap, loadGeoJSON, createLegend, createOptions and updateLegend. These functions are the main functions where the visualization is created. First I use the setupMap() function. This function adds attributes to the emtpy <svg> and adds a class map. Then it will create the legend with createLegend(). With the legend options passed on from the setupMap() function it adds a rectangle and creates a linear-gradient with the information of the color scale created later on. It also adds a label showing the color for unknown information about the province. Then back to the setupMap() function where the path is created. This will let D3 know how to represent the geoJSON information and know how to project it. The path will be returned from the function to be used in the drawMap() function.

ProvinceMap.vue:

function setupMap(width, height, legend) {
    // add attributes to svg element
    const svg = select("#provinces svg")
        .attr("viewBox", "0 0 " + 600 + " " + 600)
        .attr("width", width)
        .attr("height", height);

    // create group with class "map"
    svg.append("g")
        .attr("class", "map");

    createLegend(svg, legend);

    const scale = width * height / 50;

    // create geoMercator projection and return path configuration
    const projection = geoMercator()
        .rotate([5.38763888888889, 0])
        .center([0, 52.15616055555555])
        .scale(scale)
        .translate([-990, height / 2]);
    const path = geoPath().projection(projection);
    return path;
}

Then I fetch the geoJSON data with loadGeoJSON() function. This function is quite straight forward. It used the D3 *json()*function to fetch the json data and with the topojson feature() function it adds each feature from the featureCollection to an array and returns it.

// load GeoJSON and return features
async function loadGeoJSON(path) {
    const data = await json(path);
    return feature(data, data.objects);
}

Now I'm able to create the map. But first I want to add the options for the map. That's created with the createOptions() function. In the createOptions function the information of the data is filtered on each single data-point and are saved in the keys array. Now all the keys that are present in the data are mapped with the options data that contains the descriptions and an object is returned that contains the keys with the descriptions and the keys. This will be returned from the function and saved to the Vue instance with this.options. In the data() object the data is saved as Array and used to create the options in the template.

// create select options from data
function createOptions(data, options = null) {
    let keys = [];
    if (options.selection) {
        // get keys from selection
        keys = getDataFromSelection(data, options.selection)
    } else {
        // get keys from data
        keys = listOfKeysWithNumberValue(data)
    }
    // create option objects
    const result = keys.map(function (option, index) {
        const content = options.descriptions 
            ? options.descriptions[index] 
            : option;
        return {
            value: option.toLowerCase(),
            text: content,
            key: option,
            id: index
        }
    });
    // add default option to options & update indexing
    if (options.default) {
        result.splice(0, 0, options.default);
        result.map(function (item, i) {
            return item.id = i;
        });
    }
    return result;
}

Now I actually create the map with the drawMap() function. In the drawMap function I first use a function called getSelectedOption() to get the map selection. It can be the element that is selected or it can look in LocalStorage to get the selected option. This will be returned and saved in a variable parkingSelection. If it hasn't found the selected option from the user it will default to the first option. Then it gets the information of the selection from the GeoJSON data. With this data present it will calculate the maximum amount so it can be used to create a scale and find the correct color for representing the information.

Then it will update the legend with the updateLegend() function and calculates the bottom axis of the gradient from the calculated scale of the selection.

Finally the map is created with the enter, update, exit pattern. All the paths are selected and the GeoJSON data is given as input. Then it will add the shape of each province and adds a fill from the calculated color scale. At last the information is added to a <text> element to show the information when you hover over a province.

Now when the user selects a different option, the map will update. Because I added a OnChange event on the select element it will call the drawMap() function. It will get the selection and filters the data and update the province color and information. If a province doesn't exist in the data it will be removed in the exit function.

function drawMap(path, data, node = null) {
    // get selected option for parking
    let parkingSelection = getSelectedOption(node, parkingLSKey);

    // set default value if no selection is made
    if (!parkingSelection) {
        parkingSelection = "parkingTotal";
    }

    // get numeric values of parking selection
    const selectedData = dataFromKey(data.features, parkingSelection);
    
    // add reference to svg element & map group element
    const svg = select("#provinces svg");
    const map = select("#provinces svg g.map");
    
    // calculate maximum of parking data & create color scale
    const maximum = max(selectedData);
    const n = 10 ** (maximum.toString().length - 2);
    const ceil = Math.ceil(maximum / n) * n;
    const scale = [0, ceil];
    const color = scaleSequential(scale, interpolateBuPu);

    updateLegend(svg, scale);

    // create, update & remove provinces
    map.selectAll("path")
        .data(data.features)
        .join(function (enter) {
            // add path and color scale of province
            enter.append("path")
                .attr("class", "province")
                .attr("d", path)
                .attr("stroke", "rgb(178, 172, 171")
                .attr("fill", function (d) {
                    return color(d.properties[parkingSelection]);
                })
                // show information on hover
                .append("title")
                .text(function (d) {
                    const info = d.properties;
                    return `Provincie ${info.province} \n${info[parkingSelection]} parkeergelegenheden`;
                });
        }, function (update) {
            // update color and information of province
            update.attr("fill", function (d) {
                    return color(d.properties[parkingSelection]);
                })
                .selectAll("title")
                .text(function (d) {
                    const info = d.properties;
                    return `Provincie ${info.province}\n${info[parkingSelection]} parkeergelegenheden`;
                });
        }, function (exit) {
            // remove province(s)
            exit.remove();
    });
}

For the map to update I created a method. In the methods object I added a function called updateMap that will activate when an OnChange on the <select> element occurs. It will get the information saved in the Vue instance like the province data, the current element and the drawMap() function. Then the drawMap function is called with the data and the <select> Node as arguments. This will update the map accordingly.

methods: {
    updateMap(event) { 
        const data = this.provinces;
        const selectedOption = event.target;
        const drawMap = this.drawMap;
        drawMap(null, data, selectedOption)
    }
}

Parking in municipalities of The Netherlands

For the MunicipalityMap component it is almost identical to the ProvinceMap function but it differs only in the drawMap() function and the available options. In the script section I added the option to select a province. I used the same function created for the parking information but instead it filtered on the province names and returned the name of each province. Then inside the drawMap function I used the same function to get the selected option but then for the province selected. This I used to get the current selections and save it to the Vue instance. That will render the <select> options. And now the data filters on province too. The map will zoom in on the province and show the correct information.

Clone this wiki locally