block by pstuffa bc14012b64c12112867e4daaa7af4f6b

Reusable Line Chart

Full Screen

index.html

<!DOCTYPE html>
<meta charset="utf-8">

<!-- Vendor Modules -->
<script src="./d3.js"></script>
<script src="./moment.js"></script>

<!-- PB Modules -->
<script src="./helpers.js"></script>
<script src="./line-chart.js"></script>

<!-- CSS Styling  -->
<link href="./line-chart.css" rel="stylesheet" />

<body>

<div id="chart"></div>


<script>


var datasets = ["Any_Bedrooms", "One_Bedroom", "Studios", "Two_Bedrooms", "Three_Bedrooms_Or_More"];
// sales_discount_share_time_series_All_Types_
// 
var selection = d3.select("#select-data")
    .on("input", function(d) {
      d3.csv("./sales_inventory_time_series_counts_All_Types_" + this.value + ".csv", ready);
    })

selection.selectAll("option")
  .data(datasets)
.enter().append("option")
  .attr("value", function(d) { return d })
  .text(function(d) { return d });


d3.csv("./sales_inventory_time_series_counts_All_Types_Any_Bedrooms.csv", ready);

function ready(error, data) {

  console.log(data);
  var columns = data.columns.filter(function(d) { return d != "Area" || d != "Boro" || d != "AreaType" })
    , flatData = []
    , excludeAreas = ["Area","NYC","Manhattan","All Downtown","All Midtown","All Upper West Side","All Upper East Side","All Upper Manhattan","Bronx","Brooklyn","North Brooklyn","Northwest Brooklyn","East Brooklyn","Prospect Park","South Brooklyn","Queens","Staten Island"];

  data = data.filter(function(d) { return excludeAreas.indexOf(d["Area"]) == -1 })
      .filter(function(d) { return d["Boro"] == "Brooklyn" })

  data.forEach(function(d) {
    columns.forEach(function(col) {
      d["date"] = moment(col, "YYYY-MM-DD");
      if(+d[col] > 0) { flatData.push({"date": d["date"], "val": +d[col], "area": d["Area"] });  }
    })
  });

  var lines = lineChart()
      .x("date")
      .y("val")
      .margin({top: 20, right: 20, bottom: 120, left: 60})
      .width(parseInt(d3.select("#chart").style("width"),10))
      .height(650 - 50 - 100)
      .nest("area");

  d3.select("#chart")
    .datum(flatData)
    .call(lines);

}



</script>

helpers.js

// https://github.com/wbkd/d3-extended
d3.selection.prototype.moveToFront = function() {  
  return this.each(function(){
    this.parentNode.appendChild(this);
  });
};



line-chart.css

body {
  font-family: 'Source Sans Pro', sans-serif !important;
  font-color:  #004C75;
  font-weight: 300;
}

select {
  border-radius: 0 0 0 0 !important;
  background-color: #fff !important;
  border: 1px solid #ccc !important;
  color: #1A1F2C !important;
  font-family: Source Sans Pro, Helvetica, Arial, Geneva, sans-serif !important;
  font-size: 12px !important;
  height: 32px !important;
  margin: 0 !important;
  outline: 0 !important;
  padding: 7px !important;
  text-align: left !important;
  vertical-align: top !important;
  -webkit-appearance: none !important;
}

select, select[size="0"], select[size="1"] {
    background-image: url(data:image/png;base64,R0lGODlhDQAEAIAAAAAAAP8A/yH5BAEHAAEALAAAAAANAAQAAAILhA+hG5jMDpxvhgIAOw==);
    background-repeat: no-repeat;
    background-position: right center;
    padding-right: 20px !important;
}

h2 {
  margin-top: 0px !important;
  padding-top: 0px !important;
}

#sidebar {
  padding-left: 50px !important;
}

.input select {
  padding-left: 0px;
  margin-left: 0px;
}

.no-padding {
  padding: 0px !important;
  margin: 0px !important;
}

.HomeCampaignBanner {
  width: 100%;
  height: 112px;
  clear: left;
  display: block;
  position: relative;
  background: url(//cdn-assets-s3.streeteasy.com/assets/home/2017_skyline_mobileweb@2x-b34dcf6b8c1cd7f972641e0d85c0d6238364d87e1806778ba5df3c7d04c159cb.png) left bottom/auto 100% repeat;
}

.brush-text {
  font-size: 12px;
  font-weight: 100;
}

.navbar-logotype {
  margin: 10px;
}

.navbar-nav  {
    padding-top:0px !important; 
    padding-bottom:0 !important;
    height: 18px;
}

.navbar {
  min-height:12px !important;
  background-color:  #0280C5 !important
}

.parent {
  margin-top: 70px;
}

.zero-line {
  stroke: darkgray;
  stroke-width: 2px;
}

.subtext {
  align: center;
}

.y .tick line {
  stroke: #000;
  stroke-width: .125px;
  stroke-opacity: .125px;
}

.x .tick line {
  stroke: #fff;
  stroke-width: 20px;
}

.axis--x .domain,
.axis--grid .tick line {
  stroke: #fff;
}

.axis--grid .tick--minor line {
  stroke-opacity: .5;
}

.axis line {
  stroke: #000;
}

.domain {
  stroke: none;
}

.above {
  fill: #ED422F;
}

.below {
  fill: #8C8C8C;
}

.line {
  stroke: #000;
  stroke-width: 1px;
  fill: none;
}

text {
  font-size: 12px;
}

.zoom {
  cursor: move;
  fill-opacity: 0;
  pointer-events: all;
}

button {
    display: inline-block;
    padding: 0 20px;
    margin-bottom: 0;
    border: none;
    cursor: pointer;
    line-height: 40px;
    outline: 0;
    color: white !important;
    border: solid 1px #FFF;
    background: none;
    background-color: #0080C6;
    -webkit-appearance: none;
    border-radius: 0 0 0 0;
    -webkit-user-select: none;
    user-select: none;
    text-transform: uppercase;
    font-family: Source Sans Pro, Helvetica, Arial, Geneva, sans-serif;
    font-size: 14px;
    font-weight: normal;
    text-align: center;
}

button:hover {
    background-color: #afafaf;
    color: white;
}

.page-num {
    padding: 4px 4px;
}

.active {
  color: steelblue;
  font-weight: bolder;
}

.emphasis {
  font-size: 24px;
}

.background-rect {
  fill: #A6A6A6;
  fill-opacity: .25;
}

.hover-text {
  font-size: 16px;
}
.y-axis-text {
  font-weight: 100;
}

.voronoi {
  fill-opacity: 0;
  /*stroke: #000;*/
}

line-chart.js

 function lineChart() {

  var margin = {top: 20, right: 20, bottom: 20, left: 20}
    , width = 760
    , height = 120
    , xValue = "date"
    , yValue = "val"
    , yText = "Number of Listings"
    , nestValue = "area"
    , colorScale = d3.scaleSequential(d3.interpolateRainbow)
    , xScale = d3.scaleTime()
    , yScale = d3.scaleLinear()
    , xAxis = d3.axisBottom(xScale).tickSize(6, 0)
    , yAxis = d3.axisLeft(yScale) 
    , line = d3.line().curve(d3.curveMonotoneX).x(X).y(Y)
    , voronoi = d3.voronoi().x(X).y(Y)
    , hoverText;

  function chart(selection) {
    selection.each(function(data) {

      // Set Voronoi to User Inputed Margins and Height
      voronoi.extent([[-margin.left, -margin.top], [width + margin.right, height-margin.bottom]]);

      // Update the x-scale.
      xScale
          .domain(d3.extent(data, function(d) { return d[xValue]; }))
          .range([0, width - margin.left - margin.right]);

      // Update the y-scale.
      yScale
          .domain([0, d3.max(data, function(d) { return d[yValue]; })])
          .range([height - margin.top - margin.bottom, 0]);

      if(yScale.domain()[1] <= 1) { 
        yAxis.tickFormat(d3.format(".0%"))
      }
      // The margins are changed by accessors 
      yAxis.tickSize(-(width-margin.left-margin.right));
      xAxis.tickSize(-(height));

      // Update the color-scale
      colorScale
          .domain([0, d3.set(data.map(function(d) { return d[nestValue] })).values().length])

      // Nest data for lines 
      var nestedData = d3.nest()
          .key(function(d) { return d[nestValue] })
          .entries(data);

      // Select the svg element, if it exists.
      var svg = d3.select(this).selectAll("svg").data([data]);

      // Otherwise, create the skeletal chart.
      var gEnter = svg.enter().append("svg")
          .attr("width", width)
          .attr("height", height)
        .append("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      gEnter.append("g")
          .attr("class", "y axis")
          .transition()
          .call(yAxis);

      gEnter.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + yScale.range()[0] + ")")
          .transition()
          .call(xAxis);

      gEnter.append("defs").append("clipPath")
          .attr("id", "clip")
        .append("rect")
          .attr("width", width)
          .attr("height", height);

      gEnter.append("text")
          .attr("class", "y-axis-text")
          .attr("text-anchor", "middle")  // this makes it easy to centre the text as the transform is applied to the anchor
          .attr("transform", "translate("+ (-margin.left/2) +","+((height-margin.bottom)/2)+")rotate(-90)")  // text is drawn off the screen top left, move down and out and rotate
          .text(yText);

      hoverText = gEnter.append("text")
          .attr("class", "hover-text")
          .attr("text-anchor", "end")
          .attr("x", width - margin.left - margin.right);

      // Update the outer dimensions.
      svg.attr("width", width)
          .attr("height", height);

      // Update the inner dimensions.
      var g = svg.select("g")
          .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

      // Create the lines
      gEnter.append("g")
          .attr("class", "lines")
          .selectAll(".line")
          .data(nestedData, function(d) { return d.key })
        .enter().append("path")
          .attr("class", "line")
          .style("clip-path", "url(#clip)")
          .style("stroke", function(d, i) { d.color = colorScale(i); return d.color })
          .attr("d", function(d) { return line(d.values) })      

      var lines = svg.select("g").select(".lines").selectAll(".line")
          .data(nestedData, function(d) { return d.key });

      // Enter new lines 
      lines.enter().append("path")
          .attr("class", "line")
          .style("clip-path", "url(#clip)")
          .style("stroke", function(d, i) { d.color = colorScale(i); return d.color })
          .attr("d", function(d) { return line(d.values) });

      // Remove any old lines 
      lines.exit().remove();

      // Update the line path.
      lines
          .style("stroke", function(d, i) { d.color = colorScale(i); return d.color })
          .transition()
          .attr("d", function(d) { return line(d.values) });

      // Update the x-axis.
      g.select(".x")
          .attr("transform", "translate(0," + yScale.range()[0] + ")")
          .transition()
          .call(xAxis);

      // Update the x-axis.
      g.select(".y")
          .transition()
          .call(yAxis);

      // Brushing section 
      var  margin2 = {top: height-margin.bottom + 10, right: margin.right, bottom: 80, left: margin.left}
         , height2 = height - margin2.top - margin2.bottom
         , width2 = width - margin.left - margin.right
         , x2 = d3.scaleTime().range([0, width - margin.left - margin.right]).domain(xScale.domain())
         , y2 = d3.scaleLinear().range([height2, 0]).domain(yScale.domain())
         , xAxis2 = d3.axisBottom(x2)
         , contextLine = d3.line().curve(d3.curveMonotoneX).x(X2).y(Y2);

      var brush = d3.brushX()
          .extent([[0, 0], [width2, height2]])
          .on("brush end", brushed);

      var context = gEnter.append("g")
          .attr("class", "context")
          .attr("transform", "translate(0," + margin2.top + ")");

      context.append("rect")
          .attr("class","background-rect")
          .attr("width", width2)
          .attr("height", height2)

      context.append("text")
        .attr("class", "brush-text")
        .attr("transform","translate(0," + (height2 + 35) + ")")
        .text("Click and drag this section to select a timeframe.");

      context.append("g")
          .attr("class", "axis axis--grid")
          .attr("transform", "translate(0," + height2 + ")")
          .call(d3.axisBottom(x2)
              .ticks(d3.timeMonth, 12)
              .tickSize(-height2)
              .tickFormat(function() { return null; }))
        .selectAll(".tick")
          .classed("tick--minor", function(d) { return d.getMonth(); });

      context.append("g")
          .attr("class", "context-x axis")
          .attr("transform", "translate(0," + height2 + ")");

      context.append("g")
          .attr("class", "context-brush brush");

      d3.select(".context-x").call(xAxis2);
      d3.select(".context-brush").call(brush);

      function brushed() {
        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
        var s = d3.event.selection || x2.range();
        xScale.domain(s.map(x2.invert, x2));

        // Update Lines
        d3.selectAll(".line")
          .attr("d", function(d) { return line(d.values) });
        // Update Voronoi
        d3.selectAll(".voronoi-path")
          .data(voronoi.polygons(d3.merge(nestedData.map(function(d) { return d.values; }))))
          .attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; });
        // Update Axis
        d3.select(".x").call(xAxis.ticks(6));
      }
      // Ends Brushing Section

      // Voronoi Section
      // Create the Voronoi 
      gEnter.append("g")
          .attr("class", "voronoi")
          .selectAll("path")
          .data(voronoi.polygons(d3.merge(nestedData.map(function(d) { return d.values; }))))
        .enter().append("path")
          .attr("class", "voronoi-path")
          .attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; })
          .on("mouseenter", mouseover)
          .on("mouseleave", mouseout);

      // Voronoi Selection 
      var voronoiSelection = svg.select("g").select(".voronoi").selectAll(".voronoi-path")
          .data(voronoi.polygons(d3.merge(nestedData.map(function(d) { return d.values; }))));

      // Enter new voroni 
      voronoiSelection.enter().append("path")
          .attr("class", "voronoi-path")
          .attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; })
          .on("mouseenter", mouseover)
          .on("mouseleave", mouseout);

      // Remove any old lines 
      voronoiSelection.exit().remove();

      // Update the line path.
      voronoiSelection
          .attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; });

      // Ends Voronoi Section 

    });
  }

  function mouseover(d) {

    console.log(d)

    d3.selectAll(".line")
      .filter(function(e) { return d.data[nestValue] == e.key })
      .moveToFront()
      .transition()
      .style("stroke-width", 4)
      .style("stroke", "#000");

    hoverText.text(d.data[nestValue]);

  }

  function mouseout(d) {

    d3.selectAll(".line")
      .filter(function(e) { return d.data[nestValue] == e.key })
      .transition()
      .style("stroke-width", 1)
      .style("stroke", function(d, i) { return d.color });

    hoverText.text("");

  }

  // The x-accessor for the path generator; xScale ∘ xValue.
  function X(d) {
    return xScale(d[xValue]);
  }

  // The y-accessor for the path generator; yScale ∘ yValue.
  function Y(d) {
    return yScale(d[yValue]);
  }

  // The x-accessor for the path generator; x2 ∘ xValue.
  function X2(d) {
    return x2(d[xValue]);
  }

  // The y-accessor for the path generator; y2 ∘ yValue.
  function Y2(d) {
    return y2(d[yValue]);
  }

  chart.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return chart;
  };

  chart.width = function(_) {
    if (!arguments.length) return width;
    width = _;
    return chart;
  };

  chart.height = function(_) {
    if (!arguments.length) return height;
    height = _;
    return chart;
  };

  chart.x = function(_) {
    if (!arguments.length) return xValue;
    xValue = _;
    return chart;
  };

  chart.y = function(_) {
    if (!arguments.length) return yValue;
    yValue = _;
    return chart;
  };

  chart.yText = function(_) {
    if (!arguments.length) return yText;
    yText = _;
    return chart;
  };

  chart.nest = function(_) {
    if (!arguments.length) return nestValue;
    nestValue = _;
    return chart;
  };


  return chart;
}

queue.min.js

!function(){function n(n){function e(){for(;i=a<c.length&&n>p;){var u=a++,e=c[u],o=t.call(e,1);o.push(l(u)),++p,e[0].apply(null,o)}}function l(n){return function(u,t){--p,null==s&&(null!=u?(s=u,a=d=0/0,o()):(c[n]=t,--d?i||e():o()))}}function o(){null!=s?m(s):f?m(s,c):m.apply(null,[s].concat(c))}var r,i,f,c=[],a=0,p=0,d=0,s=null,m=u;return n||(n=1/0),r={defer:function(){return s||(c.push(arguments),++d,e()),r},await:function(n){return m=n,f=!1,d||o(),r},awaitAll:function(n){return m=n,f=!0,d||o(),r}}}function u(){}var t=[].slice;n.version="1.0.7","function"==typeof define&&define.amd?define(function(){return n}):"object"==typeof module&&module.exports?module.exports=n:this.queue=n}();