Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

HTML

            
              <body>
  <div>
    <button onclick="weeklyData()">Weekly Dataset</button>
    <button onclick="hourlyData()">Hourly Dataset</button>
    <button onclick="resize()">Resize to 800,800</button>
  </div>

  <div class="container"></div>
</body>

            
          
!

CSS

            
              body {
      background-color: #fff;
    }

text {
   fill: black;
   font: 12px sans-serif;
   cursor: default;
}

.dot-label text {
   font-size: 12px;
}

.xLabel{
  color: red;
  transform: rotate(-45);
}
            
          
!

JS

            
              /**
 * @desc A grid with "punched" circles showing the frequency of a trip/event. The size and colour of the circle is representative of the absolute
 * value.
 * @author Robert O'Leary
 * @required D3.js
 */

d3.frequencyPunchcard = function module() {
   "use strict";
  var container, chart, mainG, border, rows, dots, maxRadius,
    csv, data, labelsX, allValues,
    margin = {
      top: 10,
      right: 10,
      bottom: 10,
      left: 10
    },
    width = 600 - margin.left - margin.right,
    height = 600 - margin.top - margin.bottom,
    padding = 3,
    xLabelHeight = 30,
    yLabelWidth = 150,
    borderWidth = 3,
    duration = 500;

  /**
   *@desc Entry point for component. Iniitialises component with selection and data
   * @param  {HTMLElement} _selection DOM element that will work as the container of the graph
   * @public
   */
  function exports(_selection) {
    _selection.each(function(_data) {
      csv = _data;
      container = this;

      parse();
      update();
    });
  }

  /**
   *@desc Parses the input CSV data to a usable format into a separate array. It extracts the X-axis labels into an array also.
   * @param  {string} csv string with the input data in csv format
   *@private
   */
  function parse() {
    var arr = [];
    labelsX = undefined;

    d3.csvParseRows(csv, function(d) {
      if (labelsX === undefined) return labelsX = d.slice(1); //first row is the X Labels, ignore first element

      var values = d.slice(1); //ignore first element
      var i = 0;

      for (; i < values.length; i++) {
        values[i] = parseInt(values[i], 10); //convert to decimal
      }

      arr.push({
        label: d[0],
        values: values
      })
    });

    data = arr;
  }

  /**
   *@desc Creates an array with the values of the entire dataset
   * @return  {Array} all the values from the data
   *@private
   */
  function getAllValues() {
    return Array.prototype.concat.apply([], data.map(function(d) {
      return d.values
    }));
  }

  /**
   *@desc Updates the chart. Call whenever there is a new dataset.
   *@private
   */
  function update() {
    var maxWidth = d3.max(data.map(function(d) {
      return d.values.length;
    }));

    allValues = getAllValues();
    maxRadius = d3.min([(width - yLabelWidth) / maxWidth, (height - xLabelHeight) / data.length]) / 2;

    updateSVG();
    updateBorder();

    updateRows();
    updateDots();
    updateDotLabels();

    updateXLabels();
    updateVerticalLines();

    updateYLabels();
    updateHorizontalLines();
  }

  /**
   *@desc Builds the svg element with appropriate attributes
   * @param  {HTMLElement} container DOM element that will work as the container of the graph
   * @private
   */
  function updateSVG() {
    if (!chart) {
      chart = d3.select(container)
        .append('svg')
        .classed("frequency-punchcard", true);

      mainG = chart.append('g');
    }

    chart.attr('width', width + margin.left + margin.right)
          .attr('height', height + margin.top + margin.bottom);

    mainG.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
  }

  /**
   *@desc Creates/updates the rows for the grid
   *@private
   */
  function updateRows() {
    rows = mainG.selectAll('.row')
      .data(data, function(d) {
        return d.label;
      });

    rows.exit()
      .transition()
      .duration(duration)
      .style('fill-opacity', 0)
      .remove();

    var rowsEnter = rows.enter().append("g")
      .attr('class', 'row');

      rowsEnter.transition()
        .duration(duration)
        .attr('transform', function(d, i) {
          return 'translate(' + yLabelWidth + ',' + (maxRadius * i * 2 + maxRadius + xLabelHeight) + ')'
        });

    rows = rowsEnter.merge(rows); //update

    rows.transition()
      .duration(duration)
      .attr('transform', function(d, i) {
        return 'translate(' + yLabelWidth + ',' + (maxRadius * i * 2 + maxRadius + xLabelHeight) + ')'
      });
  }

  /**
   *@desc Creates/updates the dots represnting the frequency
   *@private
   */
  function updateDots() {
    dots = rows.selectAll('circle')
      .data(function(d) {
        return d.values;
      });

    dots.exit()
      .transition()
      .duration(duration)
      .attr('r', 0)
      .remove();

    var dotsEnter = dots.enter().append("circle")
      .attr('cy', 0)
      .attr('r', 0)
      .style('fill', '#ffffff')
      .text(function(d) {
        return d
      });

    dots = dotsEnter.merge(dots); //update

    dots.transition()
      .duration(duration)
      .attr('r', function(d) {
        return radiusScale(d);
      })
      .attr('cx', function(d, i) {
        return i * maxRadius * 2 + maxRadius;
      })
      .style('fill', function(d) {
        return 'rgb(' + colourScale(d) + ',' + colourScale(d) + ',' + colourScale(d) + ')'
      });
  }

  /**
   *@desc Creates/updates the label of the dots represnting the frequency. The text is the value
   *@private
   */
  function updateDotLabels() {
    var dotLabels = rows.selectAll('.dot-label')
      .data(function(d) {
        return d.values;
      });

    dotLabels.exit().remove();

    var dotLabelsEnter = dotLabels.enter().append('g')
      .attr('class', 'dot-label');

    dotLabelsEnter.append('rect')
      .style('fill', '#000000')
      .style('opacity', 0)
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', maxRadius * 2)
      .attr('height', maxRadius * 2);

    dotLabelsEnter.append('text')
      .style('text-anchor', 'middle')
      .style('fill', '#ffffff')
      .style('opacity', 0)
      .attr('x', maxRadius)
      .attr('y', maxRadius + 4);

    dotLabelsEnter.on('mouseover', function(d) {
        var selection = d3.select(this);
        selection.select('rect').transition().duration(100).style('opacity', 1);
        selection.select("text").transition().duration(100).style('opacity', 1);
      })
      .on('mouseout', function(d) {
        var selection = d3.select(this)
        selection.select('rect').transition().style('opacity', 0)
        selection.select("text").transition().style('opacity', 0)
      });

    dotLabels = dotLabelsEnter.merge(dotLabels); //update

    dotLabels.attr('transform', function(d, i) {
        return 'translate(' + (i * maxRadius * 2) + ',' + (-maxRadius) + ')'
      })
      .select('text')
      .text(function(d) {
        return d
      })
      .attr('y', maxRadius + 4)
      .attr('x', maxRadius);

    dotLabels.select('rect')
      .attr('width', maxRadius * 2)
      .attr('height', maxRadius * 2);

      dotLabels.on('mouseover', function(d) {
          var selection = d3.select(this);
          selection.select('rect').transition().duration(100).style('opacity', 1);
          selection.select("text").transition().duration(100).style('opacity', 1);
        })
        .on('mouseout', function(d) {
          var selection = d3.select(this)
          selection.select('rect').transition().style('opacity', 0)
          selection.select("text").transition().style('opacity', 0)
        });
  }

  /**
   *@desc Creates/updates the x-axis labels
   *@private
   */
  function updateXLabels() {
    var xLabels = mainG.selectAll('.xLabel')
      .data(labelsX);

    xLabels.exit()
      .transition()
      .duration(duration)
      .style('fill-opacity', 0)
      .remove();

    var xLabelsEnter = xLabels.enter().append('text')
      .attr('y', xLabelHeight)
      .attr('transform', 'translate(0,-6)')
      .attr('class', 'xLabel')
      .style('text-anchor', 'middle')
      .style('fill-opacity', 0);

    xLabels = xLabelsEnter.merge(xLabels); //update

    xLabels.transition()
      .text(function(d) {
        return d
      })
      .duration(duration)
      .attr('x', function(d, i) {
        return maxRadius * i * 2 + maxRadius + yLabelWidth
      })
      .style('fill-opacity', 1)
  }

  /**
   *@desc Creates/updates the y-axis labels
   *@private
   */
  function updateYLabels() {
    var yLabels = mainG.selectAll('.yLabel')
      .data(data, function(d) {
        return d.label
      });

    yLabels.exit()
      .transition()
      .duration(duration)
      .style('fill-opacity', 0)
      .remove();

    var yLabelsEnter = yLabels.enter().append('text')
      .text(function(d) {
        return d.label
      })
      .attr('x', yLabelWidth)
      .attr('class', 'yLabel')
      .style('text-anchor', 'end')
      .style('fill-opacity', 0);

    yLabels = yLabelsEnter.merge(yLabels);

    yLabels.transition()
      .duration(duration)
      .attr('y', function(d, i) {
        return maxRadius * i * 2 + maxRadius + xLabelHeight
      })
      .attr('transform', 'translate(-6,' + maxRadius / 2.5 + ')')
      .style('fill-opacity', 1);
  }

  /**
   *@desc Creates/updates the vertical grid lines
   *@private
   */
  function updateVerticalLines() {
    var vert = mainG.selectAll('.vert')
      .data(labelsX);

    vert.exit()
      .transition()
      .duration(duration)
      .style('stroke-opacity', 0)
      .remove();

    var vertEnter = vert.enter().append('line')
      .attr('class', 'vert')
      .attr('y1', xLabelHeight + borderWidth / 2)
      .attr('stroke', '#888')
      .attr('stroke-width', 1)
      .style('shape-rendering', 'crispEdges')
      .style('stroke-opacity', 0);

    vert = vertEnter.merge(vert); //update

    vert.transition()
      .duration(duration)
      .attr('x1', function(d, i) {
        return maxRadius * i * 2 + yLabelWidth
      })
      .attr('x2', function(d, i) {
        return maxRadius * i * 2 + yLabelWidth
      })
      .attr('y2', maxRadius * 2 * rows.data().length + xLabelHeight - borderWidth / 2)
      .style('stroke-opacity', function(d, i) {
        return i ? 1 : 0
      });
  }

  /**
   *@desc Creates/updates the horizontal grid lines
   *@private
   */
  function updateHorizontalLines() {
    var horiz = mainG.selectAll('.horiz').
    data(data, function(d) {
      return d.label
    });

    horiz.exit()
      .transition()
      .duration(duration)
      .style('stroke-opacity', 0)
      .remove();

    var horizEnter = horiz.enter().append('line')
      .attr('class', 'horiz')
      .attr('x1', yLabelWidth + borderWidth / 2)
      .attr('stroke', '#888')
      .attr('stroke-width', 1)
      .style('shape-rendering', 'crispEdges')
      .style('stroke-opacity', 0);

    horiz = horizEnter.merge(horiz);

    horiz.transition()
      .duration(duration)
      .attr('x2', maxRadius * 2 * labelsX.length + yLabelWidth - borderWidth / 2)
      .attr('y1', function(d, i) {
        return i * maxRadius * 2 + xLabelHeight;
      })
      .attr('y2', function(d, i) {
        return i * maxRadius * 2 + xLabelHeight;
      })
      .style('stroke-opacity', function(d, i) {
        return i ? 1 : 0;
      });
  }

  /**
   *@desc Creates/updates the border, which is the outer rectangle
   *@private
   */
  function updateBorder() {
    if (!border) {
      border = mainG.append('rect');
    }

    border.attr('x', yLabelWidth)
      .attr('y', xLabelHeight)
      .style('fill-opacity', 0)
      .style('stroke', '#000')
      .style('stroke-width', borderWidth)
      .style('shape-rendering', 'crispEdges');

    border.transition()
      .duration(duration)
      .attr('width', maxRadius * 2 * labelsX.length)
      .attr('height', maxRadius * 2 * data.length);
  }

  function radiusScale(d) {
    if (d === 0) return 0;

    var f = d3.scaleSqrt()
      .domain([d3.min(allValues), d3.max(allValues)])
      .rangeRound([2, maxRadius - padding]);

    return f(d);
  }

  function colourScale(d) {
    var c = d3.scaleLinear()
      .domain([d3.min(allValues), d3.max(allValues)])
      .rangeRound([255 * 0.8, 0]);

    return c(d);
  }

  /**
   * Gets or Sets the margin of the chart
   * @param  {Object} _x Object with fields: top; bottom; right; left
   * @return { margin | module} Current margin or module to chain calls
   * @public
   */
  exports.margin = function(_x) {
    if (!arguments.length) return margin;
    margin = _x;
    update();
    return this;
  };


  /**
   * Gets or Sets the width of the chart
   * @param  {number} _x Desired width for the graph
   * @return { width | module} Current width or module to chain calls
   * @public
   */
  exports.width = function(_x) {
    if (!arguments.length) return width;
    width = parseInt(_x) - margin.left - margin.right;
    update();
    return this;
  };

  /**
   * Gets or Sets the height of the chart
   * @param  {number} _x Desired width for the graph
   * @return {height | module} Current height or module to chain calls
   * @public
   */
  exports.height = function(_x) {
    if (!arguments.length) return height;
    height = parseInt(_x) - margin.top - margin.bottom;
    update();
    return this;
  };

  /**
   * Gets or Sets the data of the chart
   * @param  {string} _x The data set
   * @return { data | module} Current data or module to chain calls
   * @public
   */
  exports.dataset = function(_x) {
    if (!arguments.length) return csv;
    csv = _x;
    parse();
    update();
    console.log(data);
    return this;
  };

  return exports;
}; //end module

//implement using data
var fp = d3.frequencyPunchcard();
var container = d3.select(".container");
var daily =  ",Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday\nCork->Dublin,18,18,18,18,18,18,18\nCork->Tralee,5,10,10,10,10,10,5\nCork->Limerick,0,10,10,10,10,10,10\nCork->Galway,15,15,15,15,15,15,15\nGalway->Roscommon,0,6,6,6,6,6,0\nRoscommon->Sligo,4,4,4,4,4,4,4";

var hourly = ",0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23\nCork-Dublin,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1\nCork-Tralee,0,0,0,0,0,0,0,2,3,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0\nCork-Limerick,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0\nCork-Galway,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0\nGalway-Roscommon,0,0,0,0,0,0,0,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0"

container.datum(daily).call(fp);

function resize(){
      fp.width(800).height(800);
};

function weeklyData() {
      fp.dataset(daily);
}

function hourlyData() {
     fp.dataset(hourly);
}

            
          
!
999px

Console