DEV Community

Cover image for Make a scatter plot with Svelte and D3
AnupJoseph for Learners Just that

Posted on • Updated on

Make a scatter plot with Svelte and D3

This blog is second in a series of (unofficial) course notes for the Data Visualization with React and D3 series by Curran Kelleher. Read the introductory blog post here.

The next chart in the series is a scatter plot based on the Iris Flowers Dataset. I figured we could reuse a lot of the code from earlier examples than rewiriting everything from scratch. A properly cleaned version of the dataset by Curran is available here.The dataset has four numerical columns namely sepal_length,sepal_width,petal_length,petal_width which we need to convert to numbers. Let's change the row and onMount functions to reflect this:

const  row  =  function  (data)  {
    data.sepal_length  =  +data.sepal_length;
    data.sepal_width  =  +data.sepal_width;
    data.petal_length  =  +data.petal_length;
    data.petal_width  =  +data.petal_width;

    return data;
};

onMount(async  ()  => {

    dataset  =  await  csv(
"https://gist.githubusercontent.com/curran/9e04ccfebeb84bcdc76c/raw/3d0667367fce04e8ca204117c290c42cece7fde0/iris.csv",
    row
    ).then((data)  => {
        return  data;
    });
});
Enter fullscreen mode Exit fullscreen mode

in case you are wondering where this code came from, look up this gist

The scaleBand logic we used before doesn't make much sense in a scatter plot so we need to change that to scaleLinear. I am going to plot petal_width on X-axis and petal_length on Y-axis and so let's change the domain of xScale and yScale respectively. Again doesn't matter too much, so feel free to change the X and Y axes to your liking

$: xScale  =  scaleLinear()
    .domain(extent(dataset, (d)  =>  d.petal_width))
    .range([0, width]); 

$: yScale  =  scaleLinear()
    .domain(extent(dataset, (d)  =>  d.petal_length))
    .range([0, height]);
Enter fullscreen mode Exit fullscreen mode

To make the dots for the scatter plot we can make use of the <circle> SVG tag. in the plotting logic let's replace the <rect> tag by circle and specify its attributes appropriately.

<circle
    cx={xScale(data.petal_width)}
    cy={yScale(data.petal_length)}
    r="5"
/>
Enter fullscreen mode Exit fullscreen mode

scatter-basic

Something you probably noticed here is that some dots appear to be cutoff from the SVG. The solution I can think of is to shift all the circles to the left. So I am going to wrap all the circles in a <g> apply the transform directive on it. Lets use the margins that we initialized way back before to translate it across:

<g  transform={`translate(${margin.left},${margin.right})`}>
    {#each  dataset  as data, i}
        <circle
        cx={xScale(data.petal_width)}
        cy={yScale(data.petal_length)}
        r="5"
        />
    {/each}
</g>
Enter fullscreen mode Exit fullscreen mode

scatter-cropped

I am also going to reconfigure the scales so that we have more space to work with at the bottom of the page and left.

const  innerHeight  =  height  -  margin.top  -  margin.bottom,
innerWidth  =  width  -  margin.left  -  margin.right;

$: xScale  =  scaleLinear()
    .domain(extent(dataset, (d)  =>  d.petal_width))
    .range([0, innerWidth]);

$: yScale  =  scaleLinear()
    .domain(extent(dataset, (d)  =>  d.petal_length))
    .range([0, innerHeight]);
Enter fullscreen mode Exit fullscreen mode

The Iris flowers in this dataset are of three different species. I think it makes sense to represent them with different colors. I am going to map an array of colors to the species using the scaleOrdinal function in D3.

const classSet = new Set(dataset.map((d) => d.class));
$: colorScale = scaleOrdinal()
    .domain(classSet)
    .range(["#003049", "#d62828", "#f77f00"]);
Enter fullscreen mode Exit fullscreen mode

And then change the <circle> element as follows:

<circle
    cx={xScale(data.petal_width)}
    cy={yScale(data.petal_length)}  
    r="5"
    style={`fill:${colorScale(data.class)}`}
/>
Enter fullscreen mode Exit fullscreen mode

scatter-colored

I think I'll make this a (slightly) more fully fleshed out chart and add labels and axes. First lets add x and y-axis labels. Labels are ofcourse just <text> elements.
We add the Y-axis label as follows:

<text  transform={`translate(${-25},${innerHeight  /  2}) rotate(-90)`}
>Petal Length</text>
Enter fullscreen mode Exit fullscreen mode

That cryptic transform essentially just shifts to the left of all the circles and then rotate it. The Y-axis label is added as follows:

<text  x={innerWidth  /  2  }  y={innerHeight  +  30}>Petal Width</text>
Enter fullscreen mode Exit fullscreen mode

scatter-colored-axes
Let's add an X-axis and Y-axis. We could write our own Axis component but I saw a nice reusable axis component that I quite liked here. I am going to make a few changes there and use it.

<script>

    import { select, selectAll } from  "d3-selection";
    import { axisBottom, axisLeft } from  "d3-axis";


    export let  innerHeight;
    export let  margin;
    export let  position;
    export let  scale;



    let  transform;
    let  g;

    $: {

        select(g).selectAll("*").remove();

        let  axis;
        switch (position) {
            case  "bottom":
                axis  =  axisBottom(scale).tickSizeOuter(0);
                transform  =  `translate(0, ${innerHeight})`;
                break;

            case  "left":

                axis  =  axisLeft(scale).tickSizeOuter(0);
                transform  =  `translate(${margin}, 0)`;
    }
    select(g).call(axis);
}
</script>

<g  class="axis"  bind:this={g}  {transform} />
Enter fullscreen mode Exit fullscreen mode

Finally lets import the axis component and add it in the <g> element like so:

<Axis  {innerHeight}  {margin}  scale={xScale}  position="bottom" />
<Axis  {innerHeight}  {margin}  scale={yScale}  position="left" />
Enter fullscreen mode Exit fullscreen mode

Scatter plot with axes

Yeah the Y-axis is inverted 😬. Turns out I have doing this a bit wrong. For the record, I did wonder how such thin petals were so long. But then again what do I know about Iris flowers. Fixing this is easy enough. Let's change yScale as follows:

$: yScale  =  scaleLinear()
    .domain(extent(dataset, (d)  =>  d.petal_length))
    .range([innerHeight, 0]);
Enter fullscreen mode Exit fullscreen mode

Image description



If you want a simple scatter plot then this is probably all you need. I actually went on to add some more (completely unecessary) styling to it. I wanted to see if for each species of the flower we could have different shaped-petals. Not Iris petals shapes of course but petals nonetheless.

So I gathered some petal shapes from a FrontendMasters workshop by Shirley Wu here, modified them ever so slightly and saved as paths.js

export const  petalPaths  =  [
    'M0 0 C5 5 5 10 0 10 C-5 10 -5 5 0 0',
    'M-3.5 0 C-2.5 2.5 2.5 2.5 3.5 0 C5 2.5 2.5 7.5 0 10 C-2.5 7.5 -5.0 2.5 -3.5 0',
    'M0 0 C5 2.5 5 7.5 0 10 C-5 7.5 -5 2.5 0 0'
]
Enter fullscreen mode Exit fullscreen mode

Let's import the petalpaths and map them to species using D3 scaleOrdinal.

import { petalPaths } from  "./paths";
$: shapeScale  =  scaleOrdinal().domain(classSet).range(petalPaths);
Enter fullscreen mode Exit fullscreen mode

Finally instead of plotting circles, we plot a <path> element and set the d attribute to shapeScale.

<path
    d={shapeScale(data.class)}
    fill={`${colorScale(data.class)}`}
/>
Enter fullscreen mode Exit fullscreen mode

We wrap it in a <g> element and translate it to their respective position so that they dont overlap each other.

<g

    transform={`translate(${xScale(data.petal_width)},${
    yScale(data.petal_length)  -  5
    })`}
    >
    <path
    d={shapeScale(data.class)}
    fill={`${colorScale(data.class)}`}
    />
</g>
Enter fullscreen mode Exit fullscreen mode

Scatter plot with Shapes
I love this plot!
One thing to notice here however is that the plot does loose some accuracy on adding the shapes. So if that's an important concern then best stay away from it. Anyway, I think I'll end here.
Here's the full code -

So that's it for today. Have a nice day!

Discussion (0)