15 June 2023
statistics   web   devlog  

Interactive Linear Regression with Vue + d3

We can break down the functionality of the interactive graphic above into the coordinate display, the dragging interface and structural division of labor between d3 and Vue.

Vue + d3

Both d3 and Vue are frameworks for manipulating the DOM, and are relatively opinionated about how code is rendered to be displayed in order to be reactive, though in effect, for the end user, the result is quite similar. The “wheelhouse” of both frameworks is very different though, I see Vue as more of a general purpose reactivity framework targeted toward UI elements, while d3 is more data motivated in its design. For basic designs like the graphic above, both are certainly viable options.

I’ve played around with some d3 in the past, but the syntax and style of coding was messy and somewhat unintuitive. I got by mostly by piecing together bits and pieces of code from example online. Now I feel like I have a better handle on the underlying elements that d3 is manipulating, and some of the logic behind how the library is organized. Also the newest versions of d3 (v5+) greatly simplify the syntax with the function .join.

Folks have written and talked about dividing the DOM between d3 and React, and this post isn’t all that different conceptually, but you’ll see more Vue related code here.

Since both libraries are trying to work with the DOM, but in decidedly different ways, there become quite a few decisions to make about getting both to play well together. Let’s start looking at some basic code manipulating circles in d3 and Vue.

Binding data to circles

We’ll build a little demo you see in nearly every intro to d3 tutorial, but with two different methods, the d3 way and the Vue way to understand some of the fundamental differences between the two libraries.

The d3 way

Starting with an empty svg element, we can add circles directly to the DOM using d3 select statements and the selection.append()

html
<svg class="svgBox"
     viewBox="0 0 400 100">
</svg>
js
import * as d3 from "d3"

const svg = d3.select(".svgBox") // get svg element

svg.append("circle") // add circle to svg
   .attr("cx", 200) // set attributes of circle element
   .attr("cy", 50)
   .attr("r", 20)
   .style("fill", "red") // also specify style

If we look at the resulting html, this is what we’d see:

html
<!-- after d3 -->
<svg class="svgBox"
     viewBox="0 0 400 100">
   <circle cx="200" cy="50" r="40" style="fill:red">
</svg>

Adding multiple circles from data will make use of the join statement,

js
import * as d3 from "d3"

const xpos = [50, 150, 250, 350]

const svg = d3.select(".svgBox")
svg.selectAll("circle")
   .data(xpos)  // d
   .join("circle")
   .attr("cx", d => d) // d represents the datum for each selected element
   .attr("cy", 50)
   .attr("r", 20)
   .attr("fill", "red")

If this is the first time you’re seeing a data join statement in d3, it’s a little magical, but roughly the pattern is:

  1. selection.selectAll() - Define the DOM elements you want to attach data to. Even if the node is empty, you need to specify where the data should be bound. 1

  2. selection.data() - Associate the data with the DOM elements. This is the function of the data statement. It might be weird that the data is chained to the selection command, but consider the 3 possibilities:

    1. number of data elements is greater than number of selected nodes, then new elements need to be created. For now the data is bound to empty nodes. In d3 terminology, these new nodes are called “enter” nodes.
    2. there are already existing nodes that were selected that should be associated with the data. In the case the order of the elements does not match the data, you can specify a key argument to specify how the data should be matched to the nodes. These are “update” nodes.
    3. number of data elements is less than the number of selected nodes, then the extra nodes need to be removed. These are “exit” nodes.

    The result of the data statement is a selection object referencing the “update” nodes, but also containing references to the “enter” and “exit” nodes.

  3. selection.join() - Specify what to do with the three sets from the data statement. By default, the first argument specifies what to do with the newly created “enter” nodes. The join statement can also take callback functions for what to with the update and exit nodes though. The result of this statement is simply another selection statement, of the “update” and “enter” nodes, (since that’s what’s still visible on the dom) and you can chain other commands to it as you would any other selection.

Now we can create a bunch of circles with random sizes and colors, by passing in functions into attr() and style().

js
import * as d3 from "d3"

const xpos = [50, 150, 250, 350]

const svg = d3.select(".example3")
svg.selectAll("circle")
   .data(xpos)
   .join("circle")
   .attr("cx", d => d)
   .attr("cy", 50)
   .attr("r", () => Math.random() * 38 + 2) // range 2-40
   .style("fill", () => "#"+ Math.floor(Math.random() * 16777215).toString(16))

If we wrap the code up in a function and add a button,

html
<svg class="svgBox"
     viewBox="0 0 400 100">
</svg>
<button class="randomize-button">Randomize</button>
js
import * as d3 from "d3"

const xpos = [50, 150, 250, 350]

const svg = d3.select(".svgBox")

function update() {
   svg.selectAll("circle")
      .data(xpos)
      .join("circle")
      .transition() 
      .duration(1000) // add 1s transition between states
      .attr("cx", d => d)
      .attr("cy", 50)
      .attr("r", () => Math.random() * 38 + 2) // range 2-40
      .style("fill", () => "#"+ Math.floor(Math.random() * 16777215).toString(16))
}
update() // initialize the circles

document.querySelector(".randomize-button")
        .addEventListener("click", update)
Circles with d3

Awesome, I think this is already quite mesmerizing. Now we’ll implement the same demo with state variables in Vue.

The Vue way

I’ll be using the options API in Vue 3 as I think very well suited to creating one off components like these demos. Let’s first set up some state variables of what we want Vue to track, I think it makes sense to track the circles as a list of objects, each with their own attributes.

vue
<script>
export default {
  data() {
    return {
        circles: [
          {cx: 50,  cy: 50, r: 20, fill: "red"},
          {cx: 150, cy: 50, r: 20, fill: "red"},
          {cx: 250, cy: 50, r: 20, fill: "red"},
          {cx: 350, cy: 50, r: 20, fill: "red"}
       ],
    }
  },
  methods : {
    newCircles() {
      this.circles.forEach((circle) => {
        circle.r = Math.floor(Math.random() * 40 + 2)
        circle.fill = "#"+ Math.floor(Math.random() * 16777215).toString(16)
     })
    },
  }
}
</script>
vue-html
<template>
  <svg viewBox="0 0 400 100">
    <circle v-for="(circle, index) in circles"
      :key="index"
      :cx="circle.cx"
      cy="50"
      :r="circle.r"
      :fill="circle.fill"
      style="transition: all 1s ease-out;"/>
  </svg>
  <button @click="newCircles">New Circles</button>
  <!-- Show the state variables dynamically -->
  <p style="font-family: monospace;font-size:1rem"> Colors: {{ circles.map(d => d.fill) }}</p>
  <p style="font-family: monospace;font-size:1rem"> Radii: {{  circles.map(d => Math.round(d.r)) }}</p>
</template>

The v-for directive allows us to iterate through an object and create html elements on the fly. Furthermore, we can bind the attributes of each html element to the object properties. :cx is also a Vue directive that binds the value of the attribute to the reactive state variable. Similarly, we can define click functionality with the @click directive in vue, which allows us to specify a function defined in the methods section of the component. The logic is similar to the d3 example, but instead of modifying the DOM directly, we modify the state variables and let Vue handle the DOM updates.

Circles with Vue

It’s quite easy to bind reactive states to various elements in Vue, whereas in d3, I believe you’d still have to manually manipulate the DOM with update functions. Personally I think Vue is more readable — even if you are not familiar with Vue, but have some background in programming, I think you’ll have an easier time understanding the Vue style compared to the d3 approach.

In conflict or harmony?

These examples are illustrative of each respective style of approach, but let’s start mixing the libraries together. In this demo, I’ll make a d3 function that modifies the fill of the circles directly, stealing control away from Vue’s state tracking. What will break? We’ll make it so if you click on the bubbles, they’ll change to orange. The d3 way of adding event listeners is to use selection.on() with a callback function dealing with the event (first argument) and the data (second argument). We’ll set up the d3 style event listener in the mounted() lifecycle method of the Vue component to ensure that the selection is able to find the DOM elements.

vue
<script>
export default {
   /* data and methods same as before */
  mounted() {
    d3.selectAll(".svgBox circle")
      .on("click", function(e, d) {
         // `e` - event is a "PointerEvent"
         // `d` - datum is undefined (because we didn't bind any data to the circles via d3)
         // `this` - refers to the clicked DOM element
        d3.select(this).attr("fill", "orange")
      })
  }
}
</script>
d3 steals from Vue!

As you can test, clicking the circles will turn them orange now, but the state variables in the array do not update. Vue only triggers a rerender when you change the state variables through the reference, and will not know if we modify the DOM element directly through d3 as we have, which causes the momentary de-sync. Once we press the “New Circles” button again though, the visualization and array of state variables become back in sync because our function sets the variables through the Vue references. So to avoid this, we should generally decide if this state variable needs to be managed by Vue, and if so, do the courteous thing and let the Vue references know about the changes.

My general opinion is that if the state variables are used by multiple subscribers, it’s likely easier to let Vue manage the complexity. The approach Vue takes is more centralized, and thus it’s easier to scale up, as well as keep track of what is going on. If there are only a few subscribers, then d3’s update pattern is fine for reactivity, and you’ll be working more closely with the DOM for updates. Watchers and lifecycle methods make reactivity so much easier than manually coodinating updates. d3 however has the advantage of a data join model, so we can precisely control transitions on entering the screen or exiting the screen in a manner that is more difficult to do in Vue. The even greater advantage of d3 though, is the visualization utilities that are built in to the library. For example, in the next section, we’ll see that d3 has already defined a convenient utility for drag events, that can save us a lot of work if we tried implementing it ourselves through Vue.

The Dragging interface

Dragging in d3

We’ll start with the easier d3.drag() utility and then I’ll show how to implement a basic version of it in Vue more manually, and another version in which we mix them. For the minimal example, we’ll start with an empty svg again,

html
<svg class="svgBox" viewBox="0 0 400 200">
</svg>
js
import * as d3 from "d3"

// define drag behavior
function dragging(event) { // event is "DragEvent"
    d3.select(this) // this refers to the DOM element in callback
      .attr("cx", () => event.x) // .x coordinate is relative to SVG
      .attr("cy", () => event.y) // .y coordinate is relative to SVG
}
const drag = d3.drag()
               .on("drag", dragging)

const svg = d3.select(".svgBox")

svg.append("circle") // add circle
   .attr("cx", 200) // styles
   .attr("cy", 100)
   .attr("r", 20)
   .attr("fill", "red")
   .call(drag) // use drag instance on circle

This may seem like an odd separation at first — it seems like it would be easier to call .drag() directly on the selection chain, instead of defining it in pieces as shown. However, “dragging” behavior can be more complicated than it first appears. Furthermore, using a .call() statement allows modularity in the functionality we add to the selection, as well as promoting method chaining2 by passing along the selected object.

Dragging an object has at least 3 events we can define, when the user starts dragging (pointer down), the actual movement (pointer move), and when the user stops (pointer up). The d3 interface allows us to hook into every single one of these. For example, to indicate to the user the selected circle, we can add a stroke border for the duration of the drag event. Here’s the same dragging functionality with a step up in complexity:

Drag Implementation in d3
js
import * as d3 from "d3"

// define drag behavior in 3 parts: start, drag, end
function dragStart(event) {
    d3.select(this).attr("stroke", "black") // add the border to circle when dragging
}
function dragging(event) {
    d3.select(this).attr("cx", () => event.x)
                   .attr("cy", () => event.y)
}
function dragEnd(event) {
    d3.select(this).attr("stroke", "none")
}

// define all 3 parts of the drag behavior
const drag = d3.drag().on("start", dragStart)
                      .on("drag", dragging)
                      .on("end", dragEnd)

const svg = d3.select(".example7")
svg.append("circle")
   .attr("cx", 200)
   .attr("cy", 100)
   .attr("r", 20)
   .attr("fill", "red")
   .attr("stroke-width", 2)
   .style("cursor", "pointer")
   .call(drag)

Dragging in Vue

In order to implement the dragging behavior in Vue, we first need to have an element that listens to the three main parts of the dragging event. Similar to the d3 example, we need to define a method for the start, during, and end of the dragging motion. In Vue, this takes the form of having multiple directives, @pointerdown, @pointermove, @pointerup:

vue-html
<template>
    <svg viewBox="0 0 400 200" ref="dragBox" preserveAspectRatio="xMinYMin meet">
      <circle ref="dragCircle"
      :cx="circle.x" :cy="circle.y" :r="circle.r"
      fill="red"
      :stroke="circle.stroke" stroke-width="2"
      @pointerdown="startDrag" @pointermove="dragging" @pointerup="stopDrag"
      @touchstart.prevent="" @dragstart.prevent="" />
    </svg>
</template>

We’re referencing the methods startDrag, dragging, and stopDrag for the three events. The other directives are to prevent the default behavior when dragging on a touchscreen and scrolling. The attribute binds to the reactive variables should look familiar. In this case, we are tracking the properties of the circle element in a single object, along with a boolean variable for whether or not we’re currently dragging the circle.

vue
<script>
export default {
  data() {
    return {
        circle : {
          x: 200, 
          y: 100,
          r: 20,
          stroke: "none"
        },
        isDragging: false
    }
  },
  // `methods` split out below
}
</script>

The methods for dragging behavior are thus defined in three parts as follows:

vue
<script>
// utility functions
function clamp(value, lo, hi) {
  return value < lo ? lo : 
         value > hi ? hi : 
         value;
}
// constants for setting drag boundaries
const 
  svgXmin = 0,
  svgXmax = 400,
  svgYmin = 0,
  svgYmax = 200

export default {
  // `data` split out above
  methods: {
    startDrag(event) {
      this.isDragging = true;
      this.circle.stroke = "black"
      // continue to drag circle even if cursor leaves svg
      event.target.setPointerCapture(event.pointerId) 
    },
    dragging(event) {
      if (this.isDragging) { 
        const svg = this.$refs.dragBox;

        // convert screen coordinates to svg coordinates
        const pt = new DOMPoint(event.clientX, event.clientY) // gives the same coordinates as pt
        const svgP = pt.matrixTransform(svg.getScreenCTM().inverse())

        // set boundaries of dragging
        this.circle.x = clamp(svgP.x, svgXmin + this.circle.r, svgXmax - this.circle.r)
        this.circle.y = clamp(svgP.y, svgYmin + this.circle.r, svgYmax - this.circle.r)
      }
    },
    stopDrag() {
      this.circle.stroke = "none"
      this.isDragging = false
    },
  }
}
</script>

There may be some unfamiliar code in the dragging method that we didn’t see in the d3 method of handling drag.

  1. .getScreenCTM() - First we create a new point at the coordinates that event.clientX and event.clientY provide. These positions give the coordinate of the cursor relative to the screen. But when we’re moving the circle svg around, we need to know what the coordinates for the center of the circle in our SVG coordinates. Luckily there is a method to translate between the two. From the documentation, .getScreenCTM() returns the matrix that transforms the DOM coordinates into the SVG coordinates of the element specified. Thankfully the transformation also takes into account any stretching or squishing the SVG element goes through with the viewBox and preserveAspectRatio.

  2. .setPointerCapture() - Without this line, if your cursor moves too quickly, the cursor will leave the circle and we will not detect the dragging behavior anymore. This sets the circle as the target for all future PointerEvent, until the next pointerup or pointercancel event. Why didn’t we need this code with d3? Well, it’s because d3.drag() handled some of those details for us!

  3. Here, we’re setting the boundaries (with a utility function “clamp”) of that with the constants of the SVG (inset by the radius of the circle).

Drag Implementation in Vue

The working demo is quite similar to the d3 version in functionality, but now we have a better understanding of the details that d3 had abstracted away from us. I think this gives us hints on how best to have these two powerful libraries complement one another, if we can leverage the utilities that d3 provides for calculations pass control back to Vue managing the state variables, we can get the best of both worlds.

Here’s our strategy now:

  1. d3.drag() attaches the event listeners we need to the DOM element.
  2. In the call back functions for d3.drag() instead of directly updating the DOM element position, we modify the Vue state variable and trigger a rerender through Vue.

Here’s the code for the minimal functionality:

vue
<script>
import * as d3 from "d3"
export default {
  data() {
    return {
        circle : {
          x: 200, 
          y: 100,
          r: 20,
        },
    }
  },
  methods: {
    createDrag() {
        // capture vue component context `this`
        const vm  = this
        return d3.drag()
                 .on("drag", function(e, d) {
                   // `this` inside the call back is the circle element
                   vm.circle.x = e.x;
                   vm.circle.y = e.y;
                 })
    }
  },
  mounted() {
    d3.select(this.$refs.dragCircle)
      .call(this.createDrag())
  }
}
</script>

<template>
    <svg viewBox="0 0 400 200" ref="dragBox" preserveAspectRatio="xMinYMin meet">
      <circle ref="dragCircle"
      :cx="circle.x" :cy="circle.y" :r="circle.r"
      fill="red"
      :stroke="circle.stroke" stroke-width="2" />
    </svg>
    <p style="font-family:monospace">x: {{ Math.round(circle.x) }}, y: {{ Math.round(circle.y) }}</p>
</template>
Drag Implementation in Vue + d3

If we reflect on these 3 implementations, the d3 implementation handles most of the work for us, and also works for canvas and html elements out of the box. If we had to extend our Vue implementation, we’d have to add more code to handle the left and top styles, calculate the bounding boxes of the draggable object and the like. d3 abstracts the details so we can focus on the behavior of the dragging. The benefits of Vue can be seen here by providing a cleaner interface for updating state variables, and organizing the positions in a single object in a more intuitive manner. Adding multiple subscribers for reactivity is also much easier. Just to show the ease of extending the use of the state variables, I’m also binding the display of the position above the box.

There is certainly a lot of complexity to these drag events, and I’d highly recommend Red Blob Games, who has a post diving into some of the details of drag events. In short, since every device is different in interaction with a website, we have to account for touch events, pen movements, mouse movement. Chances are, a library that implements these events will be more robust than what we can come up with ourselves, and we’ll save ourselves some headache by using these libraries. I’ll also mention that Vue also has some reusable components for drag events, through the Composable API. The community has created VueUse for reusable reactive logic components if you’d like to stay in the Vue ecosystem.

Data Coordinates

We’ve got one more consideration before putting everything together into our linear regression component, and that’s the coordinates that our data points live. We’ve already seen how to convert between screen coordinates and the SVG coordinates, but if I display axes, I want the data point shown to be consistent with the calculations made at those coordinates. We need to make a mapping from the SVG coordinate to the axes shown. since we’re using the d3-axis library for our axes, we’ll use the d3-scales library to translate our SVG space to the axis space.

Using the same empty svg as before, we can add the d3 axes by first creating the linear scale, then creating the axis. The axis can be attached to the SVG element, and positioned appropriately.

js
import * as d3 from "d3"

let xScale = d3.scaleLinear()
    .domain([0, 60])
    .range([30, 500])
let yScale = d3.scaleLinear()
    .domain([0, 20])
    .range([370, 10])
let xAxis = d3.axisBottom(xScale)
let yAxis = d3.axisLeft(yScale)

d3.select(".svgBox")
   .append("g")
   .call(xAxis)
   .attr("transform", "translate(0, 370)")
d3.select(".svgBox")
   .append("g")
   .call(yAxis)
   .attr("transform", "translate(30, 0)")

d3 has a variety of scales that are meant to map one coordinate system into another. They are organized to be data-centric, where you can choose between diverging, sequential, continuous, or discrete scales. There are also special utilities for time scales as well.

d3.scaleLinear() is probably the most basic of them, and it takes a .domain() and .range(). In our application, the domain is the space our data lives, and the range is the space for our SVG elements. Thus, if we take the yScale as an example, our plot will show the values from [0, 20], but we want 0 to be shown at SVG coordinate 370. They go in opposite directions because the origin is at the top left for the svg element, but at the bottom left for the data coordinate system.

The scale that we’ve created may seem like just an ordinary object holding the information about the domain and range, but it’s actually a function. We can call the scale with an argument and also get the mapped value in the range. Furthermore, the scales come with an .invert() method that gives the reverse mapping.

js
xScale(30) // returns 265
yScale(0) // returns 370

yScale.invert(370) // returns 0

If we review all the coordinate transformations we’ve had to do… it’s a lot to keep track of!

PointerEventClient CoordSVG CoordData Coord\fbox{PointerEvent}\rightarrow \fbox{Client Coord} \rightarrow \fbox{SVG Coord} \rightarrow \fbox{Data Coord}

Since these coordinate transformations are somewhat common, we’ll wrap it up in a utility function for future use.

js
export function convertClientToSVGCoord(svg, clientX, clientY) {
    const pt = new DOMPoint(clientX, clientY) // gives the same coordinates as pt
    const svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
    return svgP
}

Now our main vue logic for keeping the state variables consistent with our pointer location can be written as follows. We have all the locations of the point in each coordinate system transformed through this function starting from the client coordinates.

vue
<script>
export default {
   /* data split out above */
    methods: {
        updateCoord(e) {
            let {x, y} = convertClientToSVGCoord(this.$refs.coordBox, e.clientX, e.clientY);
            this.coord.svgx = x;
            this.coord.svgy = y;
            this.coord.d3x = xScale.invert(x); // allows coordinates smaller than sVG
            this.coord.d3y = yScale.invert(y);
        }
    },
}
</script>

<template>            
<svg class=".svgBox" ref="coordBox"
        viewBox="0 0 600 400"
        preserve-aspect-ratio="xMinYMin meet"
        @pointermove="updateCoord">            
</svg>
</template>

Putting it all together

Finally we have all the little elements to create our interactive graphic, and we can put them all together into a single component. The last step is to make the calculations for the line, and it can be calculated with the formulas:

slope=i=1n(xixˉ)(yiyˉ)i=1n(xixˉ)2intercept=yˉslopexˉ\begin{aligned} \text{slope} &= \frac{\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y})}{\sum_{i=1}^n (x_i - \bar{x})^2} \\[2em] \text{intercept} &= \bar{y} - \text{slope} \cdot \bar{x} \end{aligned}

These are implemented in the methods section of the Vue component by aggregating the data from the list of points we’re tracking, and calculating the numerator and denominator of the slope first.

vue
<script>
export default {
    data() {
        return {
            pointRadius: 7, // svg units
            points : [ // d3 coords
                {x: 10, y: 10}, 
                {x: 3, y: 5},
                {x: 22, y: 10},
                {x: 15, y: 15},
                {x: 5, y: 4}],
            isDragging: false,
        }
    },
    methods: {
        // d3 -> SVG coord
        getXY(point) {
            return {cx: xScale(point.x), cy: yScale(point.y)}
        },
        calcSLR() {
            const X = this.points.map(p => p.x)
            const Y = this.points.map(p => p.y)
            const Xbar = X.reduce((a,b) => a+b, 0) / X.length
            const Ybar = Y.reduce((a,b) => a+b, 0) / Y.length
            const SXY = X.map((x,i) => (x - Xbar) * (Y[i] - Ybar)).reduce((a,b) => a+b, 0)
            const SXX = X.map((x,i) => (x - Xbar) * (x - Xbar)).reduce((a,b) => a+b, 0)
            const b1 = SXY / SXX
            const b0 = Ybar - b1 * Xbar
            return { b0, b1 }
        },
        // methods related to dragging omitted, see above
    },
    // computed properties continued below
}
</script>

Calculating the slope and intercept is not enough if we want to display the fitted line on screen. The line SVG element takes endpoints as its attributes. I simply set one of the values to be the y-intercept since that value is already calculated, and simply set the other point to be longer than the size of of the SVG viewport. With this method, the line will run through the x-axis, which doesn’t look great, so I also added a clipping mask to the line so that the line doesn’t extend past the boundaries of our plot. An invisible stroke is also added to the outside of the circle to make the points easier to drag.

vue
<script>
export default {
   // data and methods split out above
    computed : {
        getLine() {
            const {b0, b1} = this.calcSLR()
            return {
                x1:xScale(0),
                y1:yScale(b0),
                x2:xScale(1000),  // currently runs through axes
                y2:yScale(b0 + b1 * 1000)
            }
        }
    }
}
</script>

<template>
    <svg ref="SLRBox" viewBox="0 0 600 400" xmlns:xlink="http://www.w3.org/1999/xlink">
        <defs>
            <clipPath id="cut-bottom">
                <rect x="30" y="0" width="570" height="370" />
            </clipPath>
        </defs>
        <line v-bind="getLine" stroke="red" stroke-width="3" clip-path="url(#cut-bottom)" />
        <circle
            v-for="point in points"
            v-bind="getXY(point)"
            :r="pointRadius"
            fill="red"
            @pointermove="doDrag(point, $event)"
            @touchstart.prevent=""
            @dragstart.prevent=""
            @pointerdown="startDragging"
            @pointerup="stopDragging"
        stroke-width="20"
        stroke="transparent">
        </circle>
    </svg>
</template>

In order to let the user know the circles are interactive, a little bubble pop animation is set through css, and the intercept and slope are displayed in the top right of the plot. We can also change the cursor when the user hovers over the circles to indicate that they are draggable. And that’s it! Now we have a basic interactive linear regression component that uses the best of d3 and Vue!

Footnotes

  1. selectAll is special, it creates a grouping structure for the data join. Non-Grouping Operations

  2. d3 call function promotes chaining