block by curran 55adf7e46f4dd549d42873977d97c00f

Responsive Scatter Plot I

Full Screen

An example of a Scatter Plot component using a React-inspired pattern for stateless components.

forked from curran‘s block: Scatter Plot with Color Legend

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Solution</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js"></script>
    <style>
      body {
        margin: 0px;
      }
      .domain {
        display: none;
      }
      .tick line {
        stroke: #C0C0BB;
      }
      .tick text, .legendCells text {
        fill: #8E8883;
        font-size: 28pt;
        font-family: sans-serif;
      }
      .axis-label, .legend-label {
        fill: #635F5D;
        font-size: 50pt;
        font-family: sans-serif;
      }

      /* Make the chart container fill the page using CSS. */
      #visualization {
        position: fixed;
        left: 0px;
        right: 0px;
        top: 0px;
        bottom: 0px;
      }
    </style>
  </head>
  <body>
    
    <div id="visualization"></div>
    
    <script>
      
      // A Scatter Plot component, using the revealing module pattern.
      const ScatterPlot = (() => {
        const xScale = d3.scaleLinear();
        const yScale = d3.scaleLinear();
        const colorScale = d3.scaleOrdinal()
          .range(d3.schemeCategory10);

        const xAxis = d3.axisBottom()
          .scale(xScale)
          .tickPadding(15);

        const yAxis = d3.axisLeft()
          .scale(yScale)
          .ticks(5)
          .tickPadding(15);

        const colorLegend = d3.legendColor()
          .scale(colorScale)
          .shape('circle');
        
        const defaults = {
          margin: { left: 120, right: 300, top: 20, bottom: 120 },
          colorLegendX: 60,
          colorLegendY: 150,
          colorLegendLabelX: -30,
          colorLegendLabelY: -40,
          xAxisLabelOffset: 100,
          yAxisLabelOffset: -60,
          circleOpacity: 1,
          circleRadius: 10
        };

        return (svg, props) => {
          const { 
            data,
            width,
            height,
            xValue,
            xLabel,
            yValue,
            yLabel,
            colorValue,
            colorLabel,
            margin,
            colorLegendX,
            colorLegendY,
            colorLegendLabelX,
            colorLegendLabelY,
            xAxisLabelOffset,
            yAxisLabelOffset,
            circleOpacity,
            circleRadius,
          } = Object.assign({}, defaults, props);

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

          xAxis.tickSize(-innerHeight);
          yAxis.tickSize(-innerWidth);
          
          svg
            .attr('width', width)
            .attr('height', height);

          let g = svg.selectAll('.container').data([null]);
          const gEnter = g.enter().append('g').attr('class', 'container');
          g = gEnter
            .merge(g)
              .attr('transform', `translate(${margin.left},${margin.top})`);

          const xAxisGEnter = gEnter.append('g').attr('class', 'x-axis');
          const xAxisG = xAxisGEnter
            .merge(g.select('.x-axis'))
              .attr('transform', `translate(0, ${innerHeight})`);
          xAxisGEnter
            .append('text')
              .attr('class', 'axis-label')
              .attr('y', xAxisLabelOffset)
            .merge(xAxisG.select('.axis-label'))
              .attr('x', innerWidth / 2)
              .text(xLabel);

          const yAxisGEnter = gEnter.append('g').attr('class', 'y-axis');
          const yAxisG = yAxisGEnter.merge(g.select('.y-axis'));
          yAxisGEnter
            .append('text')
              .attr('class', 'axis-label')
              .attr('y', yAxisLabelOffset)
              .style('text-anchor', 'middle')
            .merge(yAxisG.select('.axis-label'))
              .attr('x', -innerHeight / 2)
              .attr('transform', `rotate(-90)`)
              .text(yLabel);
  
          const colorLegendGEnter = gEnter.append('g').attr('class', 'legend');
          const colorLegendG = colorLegendGEnter
            .merge(g.select('.legend'))
              .attr('transform', `translate(${innerWidth + colorLegendX},${colorLegendY})`);
          colorLegendGEnter
            .append('text')
              .attr('class', 'legend-label')
              .attr('x', colorLegendLabelX)
              .attr('y', colorLegendLabelY)
            .merge(colorLegendG.select('legend-label'))
              .text(colorLabel);

          xScale
            .domain(d3.extent(data, xValue))
            .range([0, innerWidth])
            .nice();

          yScale
            .domain(d3.extent(data, yValue))
            .range([innerHeight, 0])
            .nice();

          const circles = g.selectAll('.mark').data(data);
          circles
            .enter().append('circle')
              .attr('class', 'mark')
              .attr('fill-opacity', circleOpacity)
              .attr('r', circleRadius)
            .merge(circles)
              .attr('cx', d => xScale(xValue(d)))
              .attr('cy', d => yScale(yValue(d)))
              .attr('fill', d => colorScale(colorValue(d)));

          xAxisG.call(xAxis);
          yAxisG.call(yAxis);
          colorLegendG.call(colorLegend)
            .selectAll('.cell text')
              .attr('dy', '0.1em');
        }
      })();
      
      // The main entry point, which uses the Scatter Plot component.
      function main(){
        const visualization = d3.select('#visualization');
        const visualizationDiv = visualization.node();
        const svg = visualization.append('svg');

        const row = d => {
          d.petalLength = +d.petalLength;
          d.petalWidth = +d.petalWidth;
          d.sepalLength = +d.sepalLength;
          d.sepalWidth = +d.sepalWidth;
          return d;
        };

        d3.csv('iris.csv', row, data => {

          // Render the scatter plot with updated width and height.
          const render = () => {
            ScatterPlot(svg, {
              data,
              width: visualizationDiv.clientWidth,
              height: visualizationDiv.clientHeight,
              xValue: d => d.sepalLength,
              xLabel: 'Sepal Length',
              yValue: d => d.petalLength,
              yLabel: 'Petal Length',
              colorValue: d => d.species,
              colorLabel: 'Species',
              circleOpacity: 0.6,
              circleRadius: 8
            });
          }

          // Draw for the first time to initialize.
          render();

          // Redraw based on the new size whenever the browser window is resized.
          window.addEventListener('resize', render);
        });
      }
      
      main();
      
    </script>
  </body>
</html>

iris.csv

sepalLength,sepalWidth,petalLength,petalWidth,species
5.1,3.5,1.4,0.2,setosa
4.9,3.0,1.4,0.2,setosa
4.7,3.2,1.3,0.2,setosa
4.6,3.1,1.5,0.2,setosa
5.0,3.6,1.4,0.2,setosa
5.4,3.9,1.7,0.4,setosa
4.6,3.4,1.4,0.3,setosa
5.0,3.4,1.5,0.2,setosa
4.4,2.9,1.4,0.2,setosa
4.9,3.1,1.5,0.1,setosa
5.4,3.7,1.5,0.2,setosa
4.8,3.4,1.6,0.2,setosa
4.8,3.0,1.4,0.1,setosa
4.3,3.0,1.1,0.1,setosa
5.8,4.0,1.2,0.2,setosa
5.7,4.4,1.5,0.4,setosa
5.4,3.9,1.3,0.4,setosa
5.1,3.5,1.4,0.3,setosa
5.7,3.8,1.7,0.3,setosa
5.1,3.8,1.5,0.3,setosa
5.4,3.4,1.7,0.2,setosa
5.1,3.7,1.5,0.4,setosa
4.6,3.6,1.0,0.2,setosa
5.1,3.3,1.7,0.5,setosa
4.8,3.4,1.9,0.2,setosa
5.0,3.0,1.6,0.2,setosa
5.0,3.4,1.6,0.4,setosa
5.2,3.5,1.5,0.2,setosa
5.2,3.4,1.4,0.2,setosa
4.7,3.2,1.6,0.2,setosa
4.8,3.1,1.6,0.2,setosa
5.4,3.4,1.5,0.4,setosa
5.2,4.1,1.5,0.1,setosa
5.5,4.2,1.4,0.2,setosa
4.9,3.1,1.5,0.1,setosa
5.0,3.2,1.2,0.2,setosa
5.5,3.5,1.3,0.2,setosa
4.9,3.1,1.5,0.1,setosa
4.4,3.0,1.3,0.2,setosa
5.1,3.4,1.5,0.2,setosa
5.0,3.5,1.3,0.3,setosa
4.5,2.3,1.3,0.3,setosa
4.4,3.2,1.3,0.2,setosa
5.0,3.5,1.6,0.6,setosa
5.1,3.8,1.9,0.4,setosa
4.8,3.0,1.4,0.3,setosa
5.1,3.8,1.6,0.2,setosa
4.6,3.2,1.4,0.2,setosa
5.3,3.7,1.5,0.2,setosa
5.0,3.3,1.4,0.2,setosa
7.0,3.2,4.7,1.4,versicolor
6.4,3.2,4.5,1.5,versicolor
6.9,3.1,4.9,1.5,versicolor
5.5,2.3,4.0,1.3,versicolor
6.5,2.8,4.6,1.5,versicolor
5.7,2.8,4.5,1.3,versicolor
6.3,3.3,4.7,1.6,versicolor
4.9,2.4,3.3,1.0,versicolor
6.6,2.9,4.6,1.3,versicolor
5.2,2.7,3.9,1.4,versicolor
5.0,2.0,3.5,1.0,versicolor
5.9,3.0,4.2,1.5,versicolor
6.0,2.2,4.0,1.0,versicolor
6.1,2.9,4.7,1.4,versicolor
5.6,2.9,3.6,1.3,versicolor
6.7,3.1,4.4,1.4,versicolor
5.6,3.0,4.5,1.5,versicolor
5.8,2.7,4.1,1.0,versicolor
6.2,2.2,4.5,1.5,versicolor
5.6,2.5,3.9,1.1,versicolor
5.9,3.2,4.8,1.8,versicolor
6.1,2.8,4.0,1.3,versicolor
6.3,2.5,4.9,1.5,versicolor
6.1,2.8,4.7,1.2,versicolor
6.4,2.9,4.3,1.3,versicolor
6.6,3.0,4.4,1.4,versicolor
6.8,2.8,4.8,1.4,versicolor
6.7,3.0,5.0,1.7,versicolor
6.0,2.9,4.5,1.5,versicolor
5.7,2.6,3.5,1.0,versicolor
5.5,2.4,3.8,1.1,versicolor
5.5,2.4,3.7,1.0,versicolor
5.8,2.7,3.9,1.2,versicolor
6.0,2.7,5.1,1.6,versicolor
5.4,3.0,4.5,1.5,versicolor
6.0,3.4,4.5,1.6,versicolor
6.7,3.1,4.7,1.5,versicolor
6.3,2.3,4.4,1.3,versicolor
5.6,3.0,4.1,1.3,versicolor
5.5,2.5,4.0,1.3,versicolor
5.5,2.6,4.4,1.2,versicolor
6.1,3.0,4.6,1.4,versicolor
5.8,2.6,4.0,1.2,versicolor
5.0,2.3,3.3,1.0,versicolor
5.6,2.7,4.2,1.3,versicolor
5.7,3.0,4.2,1.2,versicolor
5.7,2.9,4.2,1.3,versicolor
6.2,2.9,4.3,1.3,versicolor
5.1,2.5,3.0,1.1,versicolor
5.7,2.8,4.1,1.3,versicolor
6.3,3.3,6.0,2.5,virginica
5.8,2.7,5.1,1.9,virginica
7.1,3.0,5.9,2.1,virginica
6.3,2.9,5.6,1.8,virginica
6.5,3.0,5.8,2.2,virginica
7.6,3.0,6.6,2.1,virginica
4.9,2.5,4.5,1.7,virginica
7.3,2.9,6.3,1.8,virginica
6.7,2.5,5.8,1.8,virginica
7.2,3.6,6.1,2.5,virginica
6.5,3.2,5.1,2.0,virginica
6.4,2.7,5.3,1.9,virginica
6.8,3.0,5.5,2.1,virginica
5.7,2.5,5.0,2.0,virginica
5.8,2.8,5.1,2.4,virginica
6.4,3.2,5.3,2.3,virginica
6.5,3.0,5.5,1.8,virginica
7.7,3.8,6.7,2.2,virginica
7.7,2.6,6.9,2.3,virginica
6.0,2.2,5.0,1.5,virginica
6.9,3.2,5.7,2.3,virginica
5.6,2.8,4.9,2.0,virginica
7.7,2.8,6.7,2.0,virginica
6.3,2.7,4.9,1.8,virginica
6.7,3.3,5.7,2.1,virginica
7.2,3.2,6.0,1.8,virginica
6.2,2.8,4.8,1.8,virginica
6.1,3.0,4.9,1.8,virginica
6.4,2.8,5.6,2.1,virginica
7.2,3.0,5.8,1.6,virginica
7.4,2.8,6.1,1.9,virginica
7.9,3.8,6.4,2.0,virginica
6.4,2.8,5.6,2.2,virginica
6.3,2.8,5.1,1.5,virginica
6.1,2.6,5.6,1.4,virginica
7.7,3.0,6.1,2.3,virginica
6.3,3.4,5.6,2.4,virginica
6.4,3.1,5.5,1.8,virginica
6.0,3.0,4.8,1.8,virginica
6.9,3.1,5.4,2.1,virginica
6.7,3.1,5.6,2.4,virginica
6.9,3.1,5.1,2.3,virginica
5.8,2.7,5.1,1.9,virginica
6.8,3.2,5.9,2.3,virginica
6.7,3.3,5.7,2.5,virginica
6.7,3.0,5.2,2.3,virginica
6.3,2.5,5.0,1.9,virginica
6.5,3.0,5.2,2.0,virginica
6.2,3.4,5.4,2.3,virginica
5.9,3.0,5.1,1.8,virginica