Ramona Bîscoveanu 

🤝

Hello 👋

Ramona Bîscoveanu

👩🏼‍💻  Developer @ SAP

 @CodesOfRa

 

🧀 🍷

A tale of true friendship

D3

Vue

😎

🤨

🤝

  1. D3 renders
  2. Vue renders

}

DOM elements

1. D3 renders

<template>
  <svg id="myChart" ></svg>
</template>

import * as d3 from "d3";
export default {
  props: ["data"],
  mounted() {
    this.drawChart();
  },
  methods: {
    drawChart() {
     ....
      var xScale = d3
        .scaleLinear()
        .domain([0, n - 1])
        .range([0, width]); 
      var yScale = d3
        .scaleLinear()
        .domain([105, 123]) 
        .range([height, 0]); 
      var line = d3
        .line()
        .x(function(d, i) {
          return xScale(i);
        })
        .y(function(d) {
          return yScale(d.y);
        });
      var dataset = this.data;
      var svg = d3
        .select("#myChart")
        .append("svg")
        .attr("width", width )
        .attr("height", height)
        .append("g")
        .attr("transform", 
              "translate(" + margin.left +
                         "," + margin.top + ")");
   svg
        .append("path")
        .datum(dataset)
        .attr("class", "line")
        .attr("d", line);

      svg
        .selectAll(".dot")
        .data(dataset)
        .enter()
        .append("circle")
        .attr("class", "dot")
        .attr("cx", function(d, i) {
          return xScale(i);
        })
        .attr("cy", function(d) {
          return yScale(d.y);
        })
        .attr("r", 3);
    }
  }
};

import * as d3 from "d3";
export default {
  props: ["data"],
  mounted() {
    this.drawChart();
  },
  methods: {
    drawChart() {
     ....
      var xScale = d3
        .scaleLinear()
        .domain([0, n - 1])
        .range([0, width]); 
      var yScale = d3
        .scaleLinear()
        .domain([105, 123]) 
        .range([height, 0]); 
      var line = d3
        .line()
        .x(function(d, i) {
          return xScale(i);
        })
        .y(function(d) {
          return yScale(d.y);
        });
      var dataset = this.data;
      var svg = d3
        .select("#myChart")
        .append("svg")
        .attr("width", width )
        .attr("height", height)
        .append("g")
        .attr("transform", 
              "translate(" + margin.left +
                         "," + margin.top + ")");
   svg
        .append("path")
        .datum(dataset)
        .attr("class", "line")
        .attr("d", line);

      svg
        .selectAll(".dot")
        .data(dataset)
        .enter()
        .append("circle")
        .attr("class", "dot")
        .attr("cx", function(d, i) {
          return xScale(i);
        })
        .attr("cy", function(d) {
          return yScale(d.y);
        })
        .attr("r", 3);
    }
  }
};
</script>

import * as d3 from "d3";
export default {
  props: ["data"],
  mounted() {
    this.drawChart();
  },
  methods: {
    drawChart() {
     ....
      var xScale = d3
        .scaleLinear()
        .domain([0, n - 1])
        .range([0, width]); 
      var yScale = d3
        .scaleLinear()
        .domain([105, 123]) 
        .range([height, 0]); 
      var line = d3
        .line()
        .x(function(d, i) {
          return xScale(i);
        })
        .y(function(d) {
          return yScale(d.y);
        });
      var dataset = this.data;
      var svg = d3
        .select("#myLineChart")
        .append("svg")
        .attr("width", width )
        .attr("height", height)
        .append("g")
        .attr("transform", 
              "translate(" + margin.left +
                         "," + margin.top + ")");
   svg
        .append("path")
        .datum(dataset)
        .attr("class", "line")
        .attr("d", line);

      svg
        .selectAll(".dot")
        .data(dataset)
        .enter()
        .append("circle")
        .attr("class", "dot")
        .attr("cx", function(d, i) {
          return xScale(i);
        })
        .attr("cy", function(d) {
          return yScale(d.y);
        })
        .attr("r", 3);
    }
  }
};
</script>

😎

  • working

  • reuse old code

🤨

  • not readable

  • not reactive ✨

Watchers to the rescue

🔎

2. Vue renders and D3 🛠

<template>
  <svg :width="width" :height="heigth">
    <path
      v-for="(d,index) in data"
      :d="generateLine(d.values,index)"
      :key="d.country"
      :stroke="colours(index)"
      :stroke-width="selected[index]"
      fill="none"
      @mouseover="onSelected(d,index)"
      @click="onClick(d)"
    ></path>
  </svg>
</template>
<template>
  <svg :width="width" :height="heigth">
    <path
      v-for="(d,index) in data"
      :d="generateLine(d.values,index)"
      :key="d.country"
      :stroke="colours(index)"
      :stroke-width="selected[index]"
      fill="none"
      @mouseover="onSelected(d,index)"
      @click="onClick(d)"
    ></path>
  </svg>
</template>
<template>
  <svg :width="width" :height="heigth">
    <path
      v-for="(d,index) in data"
      :d="generateLine(d.values,index)"
      :key="d.country"
      :stroke="colours(index)"
      :stroke-width="selected[index]"
      fill="none"
      @mouseover="onSelected(d,index)"
      @click="onClick(d)"
    ></path>
  </svg>
</template>
<template>
  <svg :width="width" :height="heigth">
    <path
      v-for="(d,index) in data"
      :d="generateLine(d.values,index)"
      :key="d.country"
      :stroke="colours(index)"
      :stroke-width="selected[index]"
      fill="none"
      @mouseover="onSelected(d,index)"
      @click="onClick(d)"
    ></path>
  </svg>
</template>
 computed: {
    path() {
     return d3.line();
    }
 }
  computed: {
...
    colours: function() {
      return d3.scaleSequential(d3.interpolateViridis).domain([0, 265]);
    },
    xScale: function() {
      const { year, margin, width } = this.defaultData;
      var xScale = d3
        .scaleLinear()
        .domain([year.min, year.max])
        .range([margin.bottom, width]);
      return xScale;
    },
    yScale: function() {
      const { margin, heigth, value } = this.defaultData;
      var yScale = d3
        .scaleLinear()
        .domain([0, value.max])
        .range([heigth - margin.top, margin.top]);
      return yScale;
    }
  }
 
generateLine: function(data) {
      this.path.x(d => this.xScale(d.year))
               .y(d => this.yScale(d.value));
      return this.path(data);
    }

Watch out for perfomance!

 <line-chart :data="data" 
             :selected="selected" 
             @onHover="handleHover" />
<template>
  ...
   <path v-for="(data,index) in data"
         :id="'arc'+index"
         :key="'arc'+index"
         :d="calculateArc(data,index)"
         stroke="#fff"
         :fill="returnColor(data.Consumption_Emissions)"
         @mouseenter="(event) => hovered = hovered = {data:data,event:event}"
         @mouseleave="() => hovered = null"
         />
  ...
</template>   
<template>
  ...
   <path v-for="(data,index) in data"
         :id="'arc'+index"
         :key="'arc'+index"
         :d="calculateArc(data,index)"
         stroke="#fff"
         :fill="returnColor(data.Consumption_Emissions)"
         @mouseenter="(event) => hovered = hovered = {data:data,event:event}"
         @mouseleave="() => hovered = null"
         />
  ...
</template>   
computed: {
  arc() {
    return d3.arc();
   }
  ...
  }

...

methods: {
    calculateArc(data) {
      this.arc
        .innerRadius(80) 
        .outerRadius(() =>
          this.yScale(
            data["Consumption_Emissions"]
          )
        )
        .startAngle(() => this.xScale(data["Code"]))
        .endAngle(() => {
          return this.xScale(data["Code"]) + this.xScale.bandwidth();
        })
        .padAngle(0.01)
        .padRadius(80);
      return this.arc(data);
    }
    ...
  }
computed: {
  arc() {
    return d3.arc();
   }
  ...
  }

...

methods: {
    calculateArc(data) {
      this.arc
        .innerRadius(80) 
        .outerRadius(() =>
          this.yScale(
            data["Consumption_Emissions"]
          )
        )
        .startAngle(() => this.xScale(data["Code"]))
        .endAngle(() => {
          return this.xScale(data["Code"]) + this.xScale.bandwidth();
        })
        .padAngle(0.01)
        .padRadius(80);
      return this.arc(data);
    }
    ...
  }

😎

  • Easy to read for Vue developers

  • Reactive

🤨

  • Animations 

When D3 needs to control the DOM ?

  • Axes

  • Brushes

  • Transitions

Axes

<g ref="xAxis" :transform="styleX"></g>
<g ref="yAxis" :transform="styleY"></g>
  mounted() {
    this.$nextTick(function() {
      d3.select(this.$refs.xAxis)
        .call(d3.axisBottom(this.xScale)
        .ticks(7));
      d3.select(this.$refs.yAxis)
        .call(d3.axisLeft(this.yScale)
        .ticks(7));
    });
  }
<g ref="xAxis" :transform="styleX"></g>
<g ref="yAxis" :transform="styleY"></g>
  mounted() {
    this.$nextTick(function() {
      d3.select(this.$refs.xAxis)
        .call(d3.axisBottom(this.xScale)
        .ticks(7));
      d3.select(this.$refs.yAxis)
        .call(d3.axisLeft(this.yScale)
        .ticks(7));
    });
  }

Transitions

  watch: {
    data(newValue) {
      newValue.map((data, index) => {
        var id = "#arc" + index;
        var d = this.calculateArc(data, index);
        TweenMax.to(id, 2, {
          attr: {
            d: d
          },
          ease: Power1.easeInOut,
          repeat: 0,
          yoyo: true
        });
      });
    }
  }
  <transition-group tag="g" css="false" appear="true"  
                    @enter="enter" @leave="leave">
    ...
  </transition-group>

What about layouts?

Links

Nodes

  <svg>
    <g transform="translate(460,430)">
      <g class="links">
        <line
          v-for="d in links"
          :key="d.index+'link'"
          :id="d.index"
          :x1="d.source.x"
          :y1="d.source.y"
          :x2="d.target.x"
          :y2="d.target.y"
          :stroke="calculateLinkColour(d)"
        ></line>
      </g>
      <g class="nodes">
        <circle
          v-for="(d,index) in nodes"
          :key="index+'node'"
          :r="calculateR(d.name)"
          :cx="d.x"
          :cy="d.y"
          :fill="calculateColour(d)"
          @click="selectedNode(d)"
        ></circle>
      </g>
    </g>
  </svg>
  <svg>
    <g transform="translate(460,430)">
      <g class="links">
        <line
          v-for="d in links"
          :key="d.index+'link'"
          :id="d.index"
          :x1="d.source.x"
          :y1="d.source.y"
          :x2="d.target.x"
          :y2="d.target.y"
          :stroke="calculateLinkColour(d)"
        ></line>
      </g>
      <g class="nodes">
        <circle
          v-for="(d,index) in nodes"
          :key="index+'node'"
          :r="calculateR(d.name)"
          :cx="d.x"
          :cy="d.y"
          :fill="calculateColour(d)"
          @click="selectedNode(d)"
        ></circle>
      </g>
    </g>
  </svg>

Links

Nodes

data() {
    return {
        ....
        layout: null
    }
},
mounted() {
    this.layout = d3
        .forceSimulation()
        .force(
            "link",
            d3.forceLink().id(function(d) {
                return d.index;
            })
        )
        .force(
            "charge",
            d3
            .forceManyBody()
            .strength(-75)
            .distanceMax([230])
        )
        .force("center", d3.forceCenter(15, 15));
},
  watch: {
    data(newData) {
      var relations = [];
      var data = newData;
      this.filterChildren(data);
      this.createLinks(data, relations);
      relations.map((d, index) => {
        this.links.push({
          index: index,
          source: this.nodes[d[0]],
          target: this.nodes[d[1]]
        });
      });
      this.layout
        .nodes(this.nodes)
        .on("tick", () => {
          this.$forceUpdate();
        })
        .force("link")
        .links(this.links)
        .distance(40)
        .strength(0.3);
    }
  },
  watch: {
    data(newData) {
      var relations = [];
      var data = newData;
      this.filterChildren(data);
      this.createLinks(data, relations);
      relations.map((d, index) => {
        this.links.push({
          index: index,
          source: this.nodes[d[0]],
          target: this.nodes[d[1]]
        });
      });
      this.layout
        .nodes(this.nodes)
        .on("tick", () => {
          this.$forceUpdate();
        })
        .force("link")
        .links(this.links)
        .distance(40)
        .strength(0.3);
    }
  },
  watch: {
    data(newData) {
      var relations = [];
      var data = newData;
      this.filterChildren(data);
      this.createLinks(data, relations);
      relations.map((d, index) => {
        this.links.push({
          index: index,
          source: this.nodes[d[0]],
          target: this.nodes[d[1]]
        });
      });
      this.layout
        .nodes(this.nodes)
        .on("tick", () => {
          this.$forceUpdate();
        })
        .force("link")
        .links(this.links)
        .distance(40)
        .strength(0.3);
    }
  },

😎

  • Clean 

  • Easier to read

🤨

  • re-rendering on every 'tick'
  • will it scale ?

Large datasets

<canvas ref="myBigChart"></canvas>

v-show

🤝

Thank you!  

Data Visualization in Vue

By Ramona Biscoveanu

Data Visualization in Vue

D3 and Vue - Talk from Vue London 2019

  • 4,502