LOADING...

Preview

Pen ID
Unlock Campus Themeforest adv

 

Code

  
    d3 Line Chart
    
    
  
  
  

Calgary Flames 2

vs.

Chicago Blackhawks 3

Wednesday, November 27, 2013
CSS
body {
  font: 300 13px/18px 'Helvetica Neue', sans-serif;
  color: #333333;
  margin: 0px auto;
  width: 900px;
  background-color: #222;
}

header {
  margin: 0;
  width: 100%;
  padding: 0px 20px;
  height: 30px;
  background-color: #fff;
}

.date {
  display: block;
  font-size: 10px;
  padding-left: 5px;
  text-transform: uppercase;
  font-weight: 700;
}

h2 {
  font-family: 'Roboto Slab', serif;
  display: inline-block;
  font-size: 15px;
  padding-top: 15px;
  line-height: 15px;
  font-weight: 100;
  margin: 0 5px;
}

.score {
  font-weight: 700;
  padding: 0 0 0 5px;
}

#content {
  width: 860px;
  padding: 0px 40px;
  background-color: #fcfcfc;
  overflow: auto;
  margin: 0px auto;
}

#chart-controls {
  position: relative;
  top: 46px;
  left: 20px;
}

input { margin-left: 0px; }
#shot-details {
  width: 155px;
  float: right;
  margin-top: 70px;
}

#shot-details h5 {
  font-size: 13px;
  font-weight: 200;
  padding: 5px 10px;
  font-family: 'Roboto Slab', serif;
  border: 1px solid #eee;
  border-radius: 2px;
  margin: 15px 0px;
}

#shot-details p {
  margin: 0px 5px;
  font-size: 12px;
  color: #666;
}

#shot-chart {
  display: inline-block;
  width: auto;
  float: left;
}
#strength-overlay:hover,
label:hover {
  cursor: pointer;
}
.area {
  fill: rgba(200,200,200,0.1);
}
.area.CHI {
  fill: rgba(62,133,162,0.58);
}
.area.CGY {
  fill: rgba(236,163,30,0.58);
}
.tooltip {
  position: absolute;
  padding: 4px 15px;
  font-size: 10px;
  color: #000;
  height: 0px;
  border: 1px solid;
  border-radius: 2px;
  height: auto;
  width: auto;
}
.tooltip.hidden {
  overflow: hidden;
  opacity: 0;
  padding: 0px;
  border: 0px;
  width: 0px;
  height: 0px;
}
.tooltip.away {
  background-color: #93BFD1;
  border-color: #3E85A2;
}
.tooltip.home {
  background-color: #FDEFD5;
  border-color: #ECA31E;
}
path.series {
  stroke-width: 4;
  fill: none;
}
path.away_team {
  stroke: #6DABC4;
}
path.home_team {
  stroke: #FFC967;
}
.linedot {
  stroke-width: 1;
  fill: #fff;
}
.linedot.active {
  stroke-width: 2;
}
.linedot.away_team {
  stroke: #3E85A2;
}
.linedot.home_team {
  stroke: #ECA31E;
}
.linedot.away_team.GOAL {
  stroke: #175670;
  fill: #3E85A2;
}
.linedot.home_team.GOAL {
  stroke: #AC7209;
  fill: #ECA31E;
}
.linedot.GOAL text {
  fill: #fff;
  stroke: #fff;
  stroke-width: 1px;
  font-size: 10px;
}
.line-helper {
  stroke-width: 1px;
  opacity: 0.4;
}
.line-helper.home {
  stroke: #ECA31E;
}
.line-helper.away {
  stroke: #3E85A2;
}
.axis {
  shape-rendering: crispEdges;
}
.x.axis line {
  stroke: rgba(150,150,150,0.15);
}
.x.axis .minor {
  stroke-opacity: .5;
}
.x.axis path {
  display: none;
}
.y.axis line {
  fill: none;
  stroke: #999;
}
.y.axis path {
  display: none;
}
.tick text {
  font-size: 11px;
  color: #333;
}
.legend text {
  font-size: 9px;
  fill: #999;
  font-weight: 600;
}
.legend .away {
  stroke: #3E85A2;
  fill: #6DABC4;
}
.legend .home {
  stroke: #ECA31E;
  fill: #FFC967;
}
.legend text.total_shots {
  fill: #333;
}
JS
var buildLegend = {
    legendItemCount: 0,
    base: function(chart, legendItemSpacing) {
      this.legendItemSpacing = legendItemSpacing;
      this.legend = chart.shotChart.append('g')
        .attr("class","legend")
        .attr("transform", "translate(" + (chart.width - 100) + "," + (-chart.margins[0] + 30) + ")");
    },
    addItem: function(team, xFactor, data) {
      this.legend.append("rect")
        .attr('class', team)
        .attr("height", 13)
        .attr("width", 25)
        .attr("x", (xFactor * this.legendItemCount)) // 0, 63
        .attr("y", 0)
        .attr("rx", 2)
        .attr("ry", 2)

      this.legend.append("text")
        .attr("class", "total_shots")
        .attr("x", (xFactor * (1 + this.legendItemCount))) // 7, 70
        .attr("y", 10).text(function(d, i) { return data["shot_count"]});

      this.legend.append("text")
        .attr("x", (xFactor * (4 + this.legendItemCount))) // 28, 91
        .attr("y", 10).text(function(d, i) { return data["abbr"]});
        this.legendItemCount = this.legendItemCount + this.legendItemSpacing;
    }
  }
  
    var buildStrengthAreas = function(chart, strength) {
    var area = d3.svg.area()
    .interpolate("strength")
    .x0(chart.width)
    .x1(0)
    .y0(chart.height)
    .y1(function(d) { return 0 });

    var areaData = [];
    for (var i = 0; i < strength.length; i++) {
      areaData.push({ "start_time": strength[i].start_time, "end_time": strength[i].end_time, "team": strength[i].advantage })
    }

    $.each(areaData, function(i, d) {
      d3.select('.strength-area').append("rect")
        .datum(d)
        .attr("d", area)
        .attr("height", chart.height)
        .attr("x", chart.x(d.start_time))
        .attr("width", chart.x((d.end_time || d3.max(data, function(d) { return parseGameTime(d.time_expired)})) - d.start_time))
        .attr("class", function(d) { return "area " + d.team });
    });
  }
    
  var parseGameTime = function(timeElapsed) {
    var time_expired = timeElapsed.split(':'),
    minutes = parseInt(time_expired[0] * 60),
    seconds = parseInt(time_expired[1]);
    time_expired = minutes + seconds;
    return time_expired;
  }
  
  var plotPoints = function(chart, team, data) {
      chart.shotChart.selectAll(".linedot." + team + "_team")
      .data(data.shot_events)
      .enter().append("circle")
        .attr("class", function(d) { return "linedot " + team + "_team " + d.shot_type })
        .attr("cy", function(d, i) { return chart["y"](i + 1) })
        .attr("cx", function(d) { return chart["x"](parseGameTime(d.time_expired)) })
        .attr("r", function(d) { return (d.shot_type === "GOAL") ? 6 : 4 })
        .on("mouseover", function(d, i) {
          var circle = d3.select(this);
          handleMouseEvents.over(circle, d, team, chart);
        })
        .on("mouseleave", function(d, i) {
          var circle = d3.select(this);
          handleMouseEvents.leave(circle, function(d) { return (d.shot_type === "GOAL") ? 6 : 4 });
        })
        .on("click", function(d, i) {
          var circle = d3.select(this);
          handleMouseEvents.showDetail(circle, d);
        })
   }
    
  
  var handleMouseEvents = {

    createToolTipDiv: function(duration) {
      this.transitionDuration = duration;
      var tooltip = d3.select("body").append("div")
      .attr("class", "tooltip")
      .style("opacity", 0);
    },


    /**
     * Handle the mouseover event for circles
     * @param {object} elem - The svg circle elem we are hovering on
     * @param {object} data - Data for the shot event
     * @param {string} team - away or home
     */
    over: function(elem, data, team, chart) {
      d3.select("body").style("cursor", "pointer");
      elem.transition().duration(200)
        .attr("r", (elem.attr("r") * 1.4));
      this.showToolTip(data, team);
      this.drawLinesToAxes(elem, "x", 0, team);
      this.drawLinesToAxes(elem, "y", chart.height, team);
    },


    /**
     * Handle the mouseleave event when exiting a circle
     * @param {object} elem - The svg circle elem we left
     * @param {int} r - The circle radius we want to revert to
     */
    leave: function(elem, r) {
      d3.select("body").style("cursor", "default");
      elem.transition().duration(200)
        .attr("r", r);
      d3.select('.tooltip').transition()
        .duration(this.transitionDuration)
        .style("opacity", 0);
      setTimeout(function() {
        d3.select(".tooltip").classed("hidden", true);
      }, this.transitionDuration)
      d3.selectAll(".line-helper").remove();
    },


    /**
     * Show tooltip when user is hovering on a circle
     * @param {object} data - Data for the shot event to populate the tooltip html
     * @param {string} team - away or home
     */
    showToolTip: function(data, team) {
      switch (data.shot_type) {
        case "SHOT":
          var shot_type = "Shot on goal";
          break;
        case "GOAL":
          var shot_type = "Goal";
          break;
        default:
          var shot_type = "Shot " + data.shot_type + "ed";
          break;
      }

      d3.select('.tooltip').classed("hidden", false).html("" + shot_type.toUpperCase() + "
" + data.period_time_expired + ", Period " + data.period) .style("left", (d3.event.pageX + 10) + "px") .style("top", (d3.event.pageY - 40) + "px"); d3.select('.tooltip').transition() .duration(this.transitionDuration) .style("opacity", 0.9) .attr("class", function() { return "tooltip " + team; }); }, /** * Show additional shot details on right-hand side when clicking a circle * @param {object} elem - The svg circle elem we clicked * @param {object} data - Data for the shot event to populate the html */ showDetail: function(elem, data) { d3.selectAll(".linedot").classed("active", false); elem.classed("active", true) var html = "
Shot Details
"; html += "

Player: " + data.player + "
Zone: " + data.zone + "

"; $('#shot-details').html(html); }, /** * Draw helper lines from circle user is hovering on to the x & y axes * @param {object} elem - The svg circle elem we are hovering on * @param {string} axisToDraw - Which axis the line should extend to * @param {int} finalPos - The final position the line should animate to * @param {string} team - away or home */ drawLinesToAxes: function(elem, axisToDraw, finalPos, team) { var offset = parseInt(elem.attr("r")) + 1; if (axisToDraw === "x") { var xPos = parseFloat(elem.attr("cx")) - offset, yPos = parseFloat(elem.attr("cy")); } else { var xPos = parseFloat(elem.attr("cx")), yPos = parseFloat(elem.attr("cy")) + offset; } d3.select("#shot-chart g").append("line") .attr("class", "line-helper " + axisToDraw + " " + team) .attr("x1", xPos) .attr("x2", xPos) .attr("y1", yPos) .attr("y2", yPos) .attr("stroke-dasharray", "3,3") .transition().duration(200).attr(axisToDraw + "1", finalPos) } } function Chart(margins, height, width, selector) { this.getMargins = function(margins) { console.log(typeof margins); if (typeof margins === "number") { return [margins, margins, margins, margins] } else if (margins instanceof Array) { return margins } else { console.log("Error: Chart margins should either be an integer or array of integers. You provided a " + typeof margins) } }, this.margins = this.getMargins(margins), this.height = height - this.margins[0] - this.margins[2], this.width = width - this.margins[1] - this.margins[3], this.drawChartBase = function() { var svg = d3.select(selector).append("svg:svg"); svg.attr("width", width) .attr("height", height) this.shotChart = svg.append("svg:g") this.shotChart.attr("transform", "translate(" + this.margins[3] + "," + this.margins[0] + ")"); }, this.config = function(opts) { if (opts.scales) { this.setupScales(opts.scales); } if (opts.xAxis) { this.setupHorizontalAxis(opts.xAxis.tickValues, opts.xAxis.tickFormat); } if (opts.yAxis) { this.setupVerticalAxis(opts.yAxis.ticks, opts.yAxis.orient); } if (opts.strengthAreas) { this.setupStrengthAreas(); } if (opts.legend) { buildLegend.base(this, opts.legend.itemSpacing) } if (opts.tooltip) { handleMouseEvents.createToolTipDiv(150); } }, /* Draw paths if this is a line chart */ this.drawPath = function(team, data) { var self = this, line = d3.svg.line() .x(function(d, i) { return self.x(parseGameTime(d.time_expired)); }) .y(function(d, i) { return self.y(i + 1); }), newLine = this.shotChart.append("svg:path"); newLine.attr("d", line(data.shot_events)).attr('class', team + '_team series'); return newLine; }, /* Handle Axis & Scales */ this.setupScales = function(axesScales) { var self = this; $.each(axesScales, function(i, axisData) { self[axisData.axis] = d3.scale.linear().domain([0, axisData.domainMax]).range(axisData.range); }); }, this.setupHorizontalAxis = function(customTickVals, formatTicks) { var xAxis = d3.svg.axis().scale(this["x"]).tickSize(-this.height).tickSubdivide(true) .tickValues(customTickVals) .tickFormat(formatTicks); this.shotChart.append("svg:g") .attr("class", "x axis") .attr("transform", "translate(0," + (this.height + 10) + ")") .call(xAxis); this.shotChart.selectAll(".x.axis .tick line") .attr("y1", -10) .attr("y2", -(this.height + 10)) }, this.setupVerticalAxis = function(tickCount, orientation) { var yAxis = d3.svg.axis().scale(this["y"]).ticks(tickCount).orient(orientation); this.shotChart.append("svg:g") .attr("class", "y axis") .attr("transform", "translate(0,0)") .call(yAxis); }, /* Handle Strength Areas */ this.setupStrengthAreas = function() { this.shotChart.append('g').attr('class', 'strength-area'); this.strengthAreas = d3.selectAll('.strength-area'); } } var chart = new Chart([80, 25, 80, 25], 500, 700, "#shot-chart"); chart.drawChartBase(); generateChart(data.teams, data.strength, data.game_end); /* shot data, strength data */ function generateChart(teams, strength, game_end) { chart.config({ "scales": [ { "axis": "x", "domainMax": 3600, "range": [0, chart.width]}, { "axis": "y", "domainMax": 75, "range": [chart.height, 0]} ], "xAxis": { "tickValues": [0, 1200, 2400, 3600], "tickFormat": function(d) { if (d === 0) { return "1st" } else if (d === 1200) { return "2nd" } else if (d === 2400) { return "3rd" } else if (d === 3600) { return "End" } } }, "yAxis": { "ticks": 8, "orient": "left" }, "legend": { "itemSpacing": 9 }, "strengthAreas": true, "tooltip": true }); /* For both home & away teams: add a legend item, SVG path, and points on each line */ $.each(teams, function(team) { buildLegend.addItem(team, 7, teams[team]); chart.drawPath(team, teams[team]); plotPoints(chart, team, teams[team]); }); /* DOM events */ $('#overlay-strength').change(function(e) { ($(e.currentTarget).is(":checked")) ? buildStrengthAreas(chart, strength) : chart.strengthAreas.selectAll("rect").remove(); }); };
Host Instantly Drag and Drop Website Builder

 

Description

D3 line chart plotting shot attempts for an NHL hockey game. Different point styling for goals vs. shots, tooltip on hover & additional details show on click. Optional strength chart toggle which underlays 5v4, 4v5 and 5v5 events during the game.
Term
Mon, 11/27/2017 - 22:03

Related Codes

Pen ID
Pen ID
Pen ID