Creating Interactive Map with Svg.js and Vue

Creating Interactive Map with Svg.js and Vue

Introduction

This is a beginners guide to generate interactive maps using svg.js in vue framework. We will generate map of Austria and bind click event handler to display state properties.

Interactive Svg Maps

Project Setup

We will use following things for our setup

Create Project

 vue create svg-interactive 

Install Dependencies

npm install --save vue-notification
npm install --save svg.js 

Add above dependencies to main.js in vue project but before we do so we need to create a simple svg.js plugin in vue so we can refer to svg.js from any component in our app

Create Svg Plugin

Create a plugin directory under src and add vueSvgPlugin.js file with following contents

import svgJs from "svg.js/dist/svg"

export default {
    install: Vue => {
        Vue.prototype.$svg = svgJs
    }
} 

Registering Plugins

Add following lines to your main.js file after imports to register plugins

import svgJs from "./plugin/vueSvgPlugin"
import Notifications from 'vue-notification'

Vue.use(svgJs);
Vue.use(Notifications); 

Preparing Map Data

We extract the austria map data from amp charts website link, Next we convert the data from svg to json using any online xml to svg converter, remember we are only interested in the paths related data. Next we put it under assets directory with filename austriaMap.js

export default {
    "g": {
      "path": [
        {
          "-id": "AT-1",
          "-title": "Burgenland",
          "-class": "land",
          "-d": "M604.79,109.01L604.71,118.94L612,121.87L606.72,126.22L606,137.55L599.97,139.53L605.03,145.48L606.35,157.35L589.78,159.71L588.38,155.86L585.3,160.37L579.02,160.52L576.67,154.45L567.67,151.11L552.97,162.43L571.21,167.45L576.26,177.29L571.21,181.99L571.96,187.17L565.23,192.92L554.85,192.85L553.81,199.33L558.24,207.82L552.34,216.63L555.43,224.02L561.43,225.87L556.07,229.7L560.71,233.96L554.2,237.2L560.11,240.42L542.46,239.06L541.04,245.03L529.73,255.14L518.99,259.93L518.99,259.93L531.66,241.55L526.28,231.51L528.41,223.5L521.68,196.11L532.67,191.09L532.67,191.09L541.57,187.24L546.25,180.17L544.23,174.25L548.95,165L543.71,159.87L543.37,148.4L540.57,147.41L541.97,145.26L545.1,147.9L550.13,141.48L547.12,138.74L552.09,136.92L554.95,130.58L560.61,129.54L565.45,136.23L574.99,126.82L575.67,121.25L585.39,119.47L590.33,113.86L597.48,119.94L600.27,113.9L597.04,110.26L601.88,112.51L604.45,108.6L604.45,108.6z"
        },
        {
          "-id": "AT-2",
          "-title": "Carinthia",
          "-class": "land",
          "-d": "M251.29,228.77L257.91,225.28L258.33,228.57L272.88,231.14L283.96,238.56L299.2,235.05L301.36,229.6L308.4,229.25L311.23,232.55L334.76,236.03L342,247.17L342,247.17L347.27,251.15L353.81,249.84L360.55,240.13L375.56,231.98L378,237.54L388.61,237.88L391.3,243.05L397.4,237.99L408.39,241.02L426.68,234.89L441.34,251.15L437.22,265.53L441.49,267.53L440.96,278.72L444.47,281.18L444.47,281.18L437.4,286.87L435.54,283.88L428.53,286.87L424.58,297.77L416.34,298.6L415.21,302.96L406.13,306.99L404.28,313.5L401.01,307.54L394.93,307.83L393.66,305.01L371.92,306.4L368.68,301.47L352.25,296.32L342.89,298.12L309.17,289.84L298.46,293.01L292.76,288.72L267.1,286.55L254.67,280.54L254.67,280.54L256.63,271.15L273.85,268.24L276.68,264.8L259.1,245.21L257.32,241.15L261.18,235.86L256.71,235.41z"
        },
        {
          "-id": "AT-3",
          "-title": "Lower Austria",
          "-class": "land",
          "-d": "M441.98,0.73l9.78,2.6l0.34,6.13l8.07,-1.35l1.38,-5.01l15.17,5.24l17.71,11.65l11.88,-2.64l20.37,15.82l22.81,2.11l5.58,-8.54l7.29,-1.69l9.82,3.95l1.49,6.42l17.57,1.24l4.07,20.68l-8.09,11.52l-1.37,8.31l9.87,13.93l1.4,11.08l7.35,6.42l0,0l-2.57,3.91l-4.84,-2.25l3.24,3.64l-2.8,6.04l-7.14,-6.08l-4.95,5.61l-9.71,1.77l-0.68,5.57l-9.54,9.41l-4.84,-6.69l-5.66,1.03l-2.85,6.35l-4.98,1.82l3.01,2.74l-5.03,6.42l-3.13,-2.63l-1.4,2.15l2.8,0.98l0.34,11.48l5.23,5.13l-4.71,9.25l2.01,5.91l-4.68,7.07l-8.89,3.85l0,0L528,179.49l-4.41,3.99l-11.04,-5.32l-2.23,-4.73l-3.31,0.54l0.9,-6.22l-9.21,-1.28l-1.94,-9.17l-3.37,1.25l-5.22,-6.13l-7.93,0.5l-8.12,-4.49l-0.5,-3.51l-8.41,-0.56l-0.9,3.47l-5.7,-0.55l-1.44,4.88l-18.09,1.01l-3.56,3.88l-15.1,-4.38l0,0l-2.29,-12.18l4.35,-2.67l-0.36,-3.43l-2.56,-3.89l-18.07,-6.94l-3.62,-5.62l2.58,-21.21l6.97,-0.96l9.29,8.26l11.3,-4.06l3.94,-5.04l7.49,3.85l0.97,-6.84l-0.43,-7.36l-3.42,-0.54l1.75,-7.91l-3.79,0.11l1.9,-4.01l-5.89,-7.13l4.45,-2.92l-18.4,-9.55l0,0l9.4,-23.38l13.6,0.86l-0.1,-25.22l1.39,-4.24L441.98,0.73zM545.7,88.94L545.7,88.94l-6.12,5.04l-4.05,-2.49l-1.78,10.9l3.1,5.61l17.12,0.52l6.22,-4.68l4.46,2.63l-1.84,-14.95l-11.62,-6.6l0,0l-0.61,-0.22l0,0l-3.16,4.24l0,0l-0.5,-0.74L545.7,88.94z"
        },
        {
          "-id": "AT-4",
          "-title": "Upper Austria",
          "-class": "land",
          "-d": "M347.18,30.34L364.11,41.91L360.18,46.13L364.69,51.44L381.06,53.12L386.11,56.63L390.27,51.68L393.72,52.25L396.9,45.07L399.5,48.82L404.19,50.35L408.18,47.41L414.61,52.64L414.61,52.64L433.01,62.19L428.56,65.11L434.45,72.25L432.55,76.25L436.34,76.15L434.59,84.06L438.01,84.6L438.45,91.96L437.47,98.8L429.98,94.95L426.04,99.99L414.73,104.05L405.44,95.8L398.47,96.75L395.89,117.96L399.5,123.58L417.58,130.52L420.13,134.4L420.49,137.84L416.14,140.51L418.42,152.69L418.42,152.69L414.81,150.75L401.46,157.18L400.9,160.69L384.35,169.41L379.82,168.73L377.93,164.21L363.41,168.14L363.74,162.13L360.08,158.2L341.64,156.34L334.97,166.8L334.84,172.12L339.79,175.76L337.22,186.46L326.23,185.16L326.23,185.16L317.55,177.76L321.19,168.08L318.1,165.3L322.96,156.7L314.44,153.94L316.54,148.13L320.76,149.77L321.59,146.7L310.65,147.15L302.31,142.94L301.74,128.71L307.96,129.98L308.89,125.59L303.22,121.47L291.2,126.35L283.29,117.51L274.11,118.49L268.29,123.02L268.29,123.02L260.12,113.46L260.22,107.56L281.14,91.96L306.14,83.81L314.5,71.25L314.5,55.9L320.09,52.11L337.88,61.16L345.19,49.17L346.33,39.03L342.91,36.99z"
        },
        {
          "-id": "AT-6",
          "-title": "Styria",
          "-class": "land",
          "-d": "M418.42,152.69L433.52,157.07L437.09,153.18L455.17,152.18L456.61,147.29L462.32,147.84L463.21,144.36L471.63,144.92L472.13,148.43L480.25,152.92L488.17,152.42L493.39,158.56L496.76,157.3L498.7,166.47L507.91,167.75L507.01,173.97L510.32,173.43L512.55,178.16L523.59,183.48L528,179.49L532.67,191.09L532.67,191.09L521.68,196.11L528.41,223.5L526.28,231.51L531.66,241.55L518.99,259.93L518.99,259.93L518.07,270.95L521.89,280.77L506.46,272.96L486.89,276.76L479.41,285.52L472.34,280.77L444.47,281.18L444.47,281.18L440.96,278.72L441.49,267.53L437.22,265.53L441.34,251.15L426.68,234.89L408.39,241.02L397.4,237.99L391.3,243.05L388.61,237.88L378,237.54L375.56,231.98L360.55,240.13L353.81,249.84L347.27,251.15L342,247.17L342,247.17L351.94,232.95L349.72,227.99L358.86,224.23L351.18,219.24L345.22,206.24L336.28,210.32L327.95,207.44L324.48,191.3L326.23,185.16L326.23,185.16L337.22,186.46L339.79,175.76L334.84,172.12L334.97,166.8L341.64,156.34L360.08,158.2L363.74,162.13L363.41,168.14L377.93,164.21L379.82,168.73L384.35,169.41L400.9,160.69L401.46,157.18L414.81,150.75z"
        },
        {
          "-id": "AT-5",
          "-title": "Salzburg",
          "-class": "land",
          "-d": "M268.29,123.02L274.11,118.49L283.29,117.51L291.2,126.35L303.22,121.47L308.89,125.59L307.96,129.98L301.74,128.71L302.31,142.94L310.65,147.15L321.59,146.7L320.76,149.77L316.54,148.13L314.44,153.94L322.96,156.7L318.1,165.3L321.19,168.08L317.55,177.76L326.23,185.16L326.23,185.16L324.48,191.3L327.95,207.44L336.28,210.32L345.22,206.24L351.18,219.24L358.86,224.23L349.72,227.99L351.94,232.95L342,247.17L342,247.17L334.76,236.03L311.23,232.55L308.4,229.25L301.36,229.6L299.2,235.05L283.96,238.56L272.88,231.14L258.33,228.57L257.91,225.28L251.29,228.77L251.29,228.77L238.98,222.35L231.41,222.86L218.45,232.62L218.45,232.62L210.32,231.19L210.32,231.19L206,217.85L208.27,205.08L237.06,202.74L239.91,194.55L245.65,194.69L250.15,185.95L255.36,184.62L249.93,176.44L252.75,172.94L245.48,169.92L245.48,166.34L245.48,166.34L247.91,161.56L262.14,161.47L260.24,164.11L265.47,168.68L261.97,172.34L263.26,175.38L279.69,186.25L283.23,182.84L285.9,159.88L280.82,155.56L272.11,155.56L279.75,140.64z"
        },
        {
          "-id": "AT-7",
          "-title": "Tyrol",
          "-class": "land",
          "-d": "M218.45,232.62l12.96,-9.76l7.57,-0.52l12.31,6.43l0,0l5.42,6.64l4.47,0.44l-3.86,5.29l1.78,4.06l17.58,19.59l-2.84,3.44l-17.22,2.91l-1.96,9.39l0,0l-10.09,0.59l-11.89,-5.58l-4.83,-8.74l-5.69,-1.05l1.86,-6.49l-2.44,-4.99l-4.91,0.99l-5.78,-4.29l2.03,-2.99l-3.8,-8.36L218.45,232.62zM219.22,157.07l1.02,3.89l14.4,-2l4.69,8.25l6.15,-0.87l0,0l-0.01,3.58l7.27,3.02l-2.81,3.49l5.42,8.18l-5.21,1.33l-4.5,8.74l-5.74,-0.14l-2.84,8.2l-28.79,2.34L206,217.85l4.32,13.35l0,0l-30.98,13.1l-10.26,-5.13l-6.55,3.48l-4.56,-3.3l-6.02,5.48l-6.73,-3.21l-12.38,3.1l-5.5,6.16l-1.04,10.68l-5.03,6.33l-23.28,-2.45l2.73,-4.12l-7.75,-6.04l-9.5,4.43l-6.03,-2.22l1.49,-9.78l-8.07,-7.22l-5.77,8.79l-5.95,-0.62l-0.81,7.55l-6.89,1.81l0,0l-3.99,-8.65l5.02,-6.69l-2.12,-11.54l6.26,-7.83l0.75,-12.24l-3.23,-2.14l0,0l12.05,-4.09l8.44,-8.94l3.17,-6.06l-2.99,-17.81l3.41,-0.09l-2.04,3.49l5.43,2.42l3.88,0.15l2.09,-4.13l14.28,6.43l8.72,-2.69l2.39,2.82l-3.87,3.63l5.29,0.18l3.31,9.7l11.11,0.21l7.2,-4.4l3.8,0.68l-2.33,3.87l3.56,-0.24l5.54,-6.15l6.73,0.54l-3,-3.27l3.94,-4.82l11.17,-0.2l5.12,-9.37l16.61,1.5l13.09,-5.1l15.46,2.26l-3.17,-11.08l7.41,-5.08L219.22,157.07z"
        },
        {
          "-id": "AT-8",
          "-title": "Vorarlberg",
          "-class": "land",
          "-d": "M24,170.85L25.05,176.35L29.72,178.61L37.37,176.47L39.79,183.94L46.95,186.8L47.64,198.97L54.4,194.54L58.73,195.79L54.14,208.91L54.14,208.91L57.36,211.04L56.61,223.28L50.35,231.11L52.47,242.66L47.45,249.34L51.44,257.99L51.44,257.99L48.23,259.11L41.33,252.11L30.06,248.21L30.2,238.16L8.52,233.45L10.39,225.33L5.37,220.3L2.36,208.86L13.74,194.76L12.54,187.31L7.54,186.05L0,174.81L18.42,178.01z"
        },
        {
          "-id": "AT-9",
          "-title": "Vienna",
          "-class": "land",
          "-d": "M550.84,84.85L551.18,84.93L551.18,84.93L562.81,91.53L564.65,106.48L560.19,103.85L553.97,108.53L536.85,108.01L533.75,102.39L535.53,91.49L539.58,93.98L545.7,88.94L545.7,88.94L547.42,88.95L547.42,88.95L550.58,84.72L550.58,84.72z"
        }
      ]
    }
  } 

Creating Map Component

From our previous steps when we registered svg plugin with vue now we can refer it within our components like  "this.$svg".

Adding template is quite straight forward just add following lines to AustriaMap.vue in components directory.

Now we will add our script associated with component.

<template>
    <div :id="svgId" class="svg-container"></div>
</template>
<script>

    import austriaMap from "../assets/austriaMap"
    export default {
        name: "AustriaMap",
        data: function () {
            return {
                svgId: "austriaMap",
                mapAttr: {
                    viewBoxWidth: 1106,
                    viewBoxHeight: 500,
                    imageWidth: 1106,
                    imageHeight: 500,
                },
                svgContainer: null
            }
        },
        mounted: function () {
            this.generateVenueMap()
        },
        methods: {
            generateVenueMap: function () {
                const vue = this;
                const mapData = austriaMap.g.path
                const svgContainer = vue.$svg("austriaMap").size('100%', '100%').viewbox(-200, 0, vue.mapAttr.viewBoxWidth, vue.mapAttr.viewBoxHeight);
                vue.svgContainer = svgContainer;
                mapData.forEach((pathObj) => {
                    vue.generatePath(svgContainer, pathObj);
                })
            },
            generatePath: function (svgCont, pathObj) {
                const vue = this;
                
                const attrs = {
                    'fill': "#8470ff",
                    'stroke': "white",
                    'stroke-width': 2,
                    'title': pathObj["-title"],
                    'map-id': pathObj["-id"],
                };
                const element = svgCont.path(pathObj["-d"]).attr(attrs);
                element.click(function () {
                    const mapId = this.node.attributes["map-id"].value;
                    const title = this.node.attributes["title"].value;
                    vue.$emit("map-clicked", {mapId, title});
                })
                
            }
        }

    }
</script>

There are some significant pieces in the above code which you need to understand we are rendering austria map on mounted hook present in vue components, also we have defined a svg container data property, we first initialize a svg and then save it to that container.

Next we are using a generate path method to render individual path components with right attributes and at the same time attaching a click handler with each path, the callback added to click handler extract important attributes from svg element and then triggers a vue event "map-clicked" which parent component can listen to for interactivity.

For better understanding of above process you can refer to following links

Generating Map in a parent component

We will refactor default HelloWorld component for simplicity and replace it with following contents.

<template>
  <div class="hello">
    <h1>Austria Map</h1>
    <p>
      Please click a state to view state properties
    </p>
    <div>
      <austria-map v-on:map-clicked="onMapClick"></austria-map>
    </div>
  </div>
</template>

<script>
import AustriaMap from './AustriaMap.vue'
export default {
  name: 'HelloWorld',
  components: {
    AustriaMap
  },
  props: {
    msg: String
  },
  methods: {
    onMapClick: function(attr){
      this.$notify({
        group: 'map',
        title: 'State clicked',
        text: `You clicked on state with id: ${attr.mapId} and title: ${attr.title}`
      });
    }
  }
}
</script>

Here we have included AustriaMap component and registered besides that we have attached a event listener for map-clicked component and within that handler we are simply generating a notification with contents referring to important attributes of state we have clicked on .

Preparing App.js File

As a final step we prepare App.js file which will look something like this.

<template>
  <div id="app">
    <notifications group="map" />
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Now run your app and enjoy

npm run serve

If you faced any problems facing this tutorial please refer to code uploaded on gitlab don't forget to fork and improve.