block by nbremer 9ea9967adfe7849f9fef22eea1b884de

Canvas tutorial - Medium level - Solution

Full Screen

The solution to the exercise (medium level) from my canvas tutorial where an SVG based scatterplot had to be turned into a canvas based one.

Built with blockbuilder.org

index.html

<!DOCTYPE html>

<head>
    <meta charset="utf-8">

    <style>
        body {
            font-family: 'Roboto', sans-serif;
        }

        svg, canvas {
            position: absolute;
            top: 0;
            left: 0;
        }

        .chart-title {
            font-size: 16px;
            text-anchor: middle;
            letter-spacing: 1.3px;
        }

        .axis .title {
            font-size: 12px;
            font-weight: 500;
            text-anchor: middle;
            text-transform: uppercase;
            letter-spacing: 1.3px;
            fill: #6b6b6b;
        }

        .axis text {
            font-family: 'Roboto', sans-serif;
            font-size: 11px;
            fill: #919191;
        }

        .axis path {
            display: none;
        }

        .axis line {
            stroke: #a5a5a5;
        }
    </style>
    <!-- Google fonts -->
    <link href="https://fonts.googleapis.com/css?family=Roboto:400,500" rel="stylesheet">

    <script src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>
    <script>
        ////////////// Create the SVG and canvas //////////////

        let margin = {
            top: 40,
            right: 10,
            bottom: 60,
            left: 100
        }
        let width = 1000 - margin.left - margin.right,
            height = 800 - margin.top - margin.bottom

        //Create the canvas
        let canvas = d3.select("body")
            .append("canvas")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)

        //Get the 2D context from the canvas element
        let ctx = canvas.node().getContext("2d")
        ctx.translate(margin.left, margin.top)

        //Create the SVG container
        let svg = d3.select("body")
            .append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

        ////////////// Create scales //////////////

        const x_scale = d3.scaleLinear() //rating
            .range([0, width])

        const y_scale = d3.scaleLog() //budget
            .domain([500, 500e6]) //in dollars
            .range([height, 0])

        const r_scale = d3.scaleSqrt() //profit ratio = revenue / budget
            .domain([0, 6])
            .range([0, 10])

        //Make the movies that have the highest profit ratio less visible
        //to balance their increased size
        const opacity_scale = d3.scaleLinear() //profit ratio = revenue / budget
            .domain([0, 100])
            .range([0.2, 0.01])
            .clamp(true)

        //A linear color scale to give an idea about when a movie was made
        const color_scale = d3.scaleSequential(d3.interpolateViridis) //release year

        ////////////// Read in the data //////////////

        //Data based on dataset from 
        //https://data.world/popculture/imdb-5000-movie-dataset
        d3.csv("imdb-movies.csv", function (error, data) {
            if (error) throw error

            ////////////// Final data prep //////////////

            //Make the number columns actually numeric
            data.forEach(d => {
                d.release_year = +d.release_year
                d.budget = +d.budget
                d.revenue = +d.revenue
                d.rating = +d.rating
                d.num_voted_users = +d.num_voted_users
                d.profit_ratio = +d.profit_ratio
            })

            ////////////// Adjust scales //////////////

            //Set the range for the scales now that the data is read in
            x_scale.domain(d3.extent(data, d => d.rating)).nice()
            color_scale.domain(d3.extent(data, d => d.release_year))

            ////////////// Create circles //////////////

            data.forEach(d => {
                //Set the color and opacity
                ctx.fillStyle = color_scale(d.release_year)
                ctx.globalAlpha = opacity_scale(d.profit_ratio)

                //Draw the circle
                ctx.beginPath()
                ctx.arc(x_scale(d.rating), y_scale(d.budget), r_scale(d.profit_ratio), 0, 2*Math.PI)
                ctx.closePath()
                ctx.fill()
            })

            ////////////// Create axes //////////////

            let x_axis = svg.append("g") //x scale - rating
                .attr("class", "axis x")
                .attr("transform", "translate(0 " + height + ")")
                .call(d3.axisBottom(x_scale))

            let y_axis = svg.append("g") //y scale - budget
                .attr("class", "axis y")
                .call(d3.axisLeft(y_scale)
                    .ticks(5)
                    .tickFormat(d3.format("$,.0s"))
                )

            ////////////// Create titles //////////////

            //Add chart title
            svg.append("text")
                .attr("class", "chart-title")
                .attr("x", width / 2)
                .attr("y", -10)
                .text("comparing budgets versus ratings for nearly 5000 movies across the last 90 years")

            //Add x title
            x_axis.append("text")
                .attr("class", "title")
                .attr("x", width / 2)
                .attr("y", 40)
                .text("Rating")

            //Add y title
            y_axis.append("text")
                .attr("class", "title")
                .attr("x", 0)
                .attr("y", 50)
                .text("Budget")

        })//d3.csv

    </script>
</body>