user6144864
user6144864

Reputation:

Append rectangle to line graph to show weekends

I'm learing how to plot graphs with D3 and I want to plot on the graph an area ,or similar, to indicate that the dates of the weekend are different than the rest (I have attach and image to explain myself better). What I have right now is the Fiddle I show later.

As you can see, on the data, there are days that don't have values and their respective days are missing.

JSFiddle of what I have accoplish right now:

JSFiddle

var data = [
  {"date":"1-May-13","close":58.13},
  {"date":"30-Apr-13","close":53.98},
  {"date":"27-Apr-13","close":67.00},
  {"date":"26-Apr-13","close":89.70},
  {"date":"25-Apr-13","close":99.00},
  {"date":"24-Apr-13","close":130.28},
  {"date":"23-Apr-13","close":166.70},
  {"date":"20-Apr-13","close":234.98},
  {"date":"19-Apr-13","close":345.44},
  {"date":"18-Apr-13","close":443.34},
];

var margin = {top: 20, right: 50, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var parseDate = d3.time.format("%d-%b-%y").parse,
    bisectDate = d3.bisector(function(d) { return d.date; }).left,
    formatValue = d3.format(",.2f"),
    formatCurrency = function(d) { return "$" + formatValue(d); };

var x = d3.time.scale()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var line = d3.svg.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });

var 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 + ")");

  data.forEach(function(d) {
    d.date = parseDate(d.date);
    d.close = +d.close;

  data.sort(function(a, b) {
    return a.date - b.date;
  });

  x.domain([data[0].date, data[data.length - 1].date]);
  y.domain(d3.extent(data, function(d) { return d.close; }));

  svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

  svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
    .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Price ($)");

  svg.append("path")
      .datum(data)
      .attr("class", "line")
      .attr("d", line);

  var focus = svg.append("g")
      .attr("class", "focus")
      .style("display", "none");

  focus.append("circle")
      .attr("r", 4.5);

  focus.append("text")
      .attr("x", 9)
      .attr("dy", ".35em");

  svg.append("rect")
      .attr("class", "overlay")
      .attr("width", width)
      .attr("height", height)
      .on("mouseover", function() { focus.style("display", null); })
      .on("mouseout", function() { focus.style("display", "none"); })
      .on("mousemove", mousemove);

  function mousemove() {
    var x0 = x.invert(d3.mouse(this)[0]),
        i = bisectDate(data, x0, 1),
        d0 = data[i - 1],
        d1 = data[i],
        d = x0 - d0.date > d1.date - x0 ? d1 : d0;
    focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
    focus.select("text").text(formatCurrency(d.close));
  }
});

CSS:

body {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.overlay {
  fill: none;
  pointer-events: all;
}

.focus circle {
  fill: none;
  stroke: steelblue;
}

And an image of what I want to accomplish:

Graph with weekend

Upvotes: 2

Views: 565

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102194

Given your x scale domain, which can be better defined with:

x.domain(d3.extent(data, function(d) {
    return d.date;
}))

We can populate an array with all the Saturdays between the first date (which is x.domain()[0]) and the last date (which is x.domain()[1]). We only need to find the Saturdays: the bars' width will be Saturday + 2 days (which will cover the whole Saturday and the whole Sunday).

There are several functions to do this, here is one:

function findSat(date1, date2) {
    if (date1 > date2) return false;
    var saturdays = [];
    while (date1 < date2) {
        if (date1.getDay() === 6) saturdays.push(new Date(date1));
        date1.setDate(date1.getDate() + 1);
    }
    return saturdays;
}

Using that function, we can populate an array with all the Saturdays between your first and last date:

var rectDate = findSat(x.domain()[0], x.domain()[1]);

Now, we use that rectDate array to create the rectangles:

var rects = svg.selectAll(".rects")
    .data(rectDate)
    .enter()
    .append("rect")
    .attr("y", margin.top)
    .attr("height", height - margin.bottom)
    .attr("x", d => x(d))
    .attr("width", d => x(d3.time.day.offset(d, +2)) - x(d))
    .attr("fill", "yellow");

Notice that, for the width, we subtract "Saturday + 2 days" from Saturday:

.attr("width", d => x(d3.time.day.offset(d, +2)) - x(d))

Here is your updated fiddle: http://jsfiddle.net/36yaot6t/

And here the same code, in a Stack snippet:

var data = [{
    "date": "1-May-13",
    "close": 58.13
}, {
    "date": "30-Apr-13",
    "close": 53.98
}, {
    "date": "27-Apr-13",
    "close": 67.00
}, {
    "date": "26-Apr-13",
    "close": 89.70
}, {
    "date": "25-Apr-13",
    "close": 99.00
}, {
    "date": "24-Apr-13",
    "close": 130.28
}, {
    "date": "23-Apr-13",
    "close": 166.70
}, {
    "date": "20-Apr-13",
    "close": 234.98
}, {
    "date": "19-Apr-13",
    "close": 345.44
}, {
    "date": "18-Apr-13",
    "close": 443.34
}, ];

var margin = {
        top: 20,
        right: 50,
        bottom: 30,
        left: 50
    },
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var parseDate = d3.time.format("%d-%b-%y").parse,
    bisectDate = d3.bisector(function(d) {
        return d.date;
    }).left,
    formatValue = d3.format(",.2f"),
    formatCurrency = function(d) {
        return "$" + formatValue(d);
    };

var x = d3.time.scale()
    .range([0, width]);

var y = d3.scale.linear()
    .range([height, 0]);

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");

var line = d3.svg.line()
    .x(function(d) {
        return x(d.date);
    })
    .y(function(d) {
        return y(d.close);
    });

var 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 + ")");

data.forEach(function(d) {
    d.date = parseDate(d.date);
    d.close = +d.close;
});

data.sort(function(a, b) {
    return a.date - b.date;
});

x.domain(d3.extent(data, function(d) {
    return d.date;
}))
y.domain(d3.extent(data, function(d) {
    return d.close;
}));

function findSat(date1, date2) {
    if (date1 > date2) return false;
    var saturdays = [];
    while (date1 < date2) {
        if (date1.getDay() === 6) saturdays.push(new Date(date1));
        date1.setDate(date1.getDate() + 1);
    }
    return saturdays;
}

var rectDate = findSat(x.domain()[0], x.domain()[1]);

var rects = svg.selectAll(".rects")
    .data(rectDate)
    .enter()
    .append("rect")
    .attr("y", margin.top)
    .attr("height", height - margin.bottom)
    .attr("x", d => x(d))
    .attr("width", d => x(d3.time.day.offset(d, +2)) - x(d))
    .attr("fill", "yellow");

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis)
    .append("text")
    .attr("transform", "rotate(-90)")
    .attr("y", 6)
    .attr("dy", ".71em")
    .style("text-anchor", "end")
    .text("Price ($)");

svg.append("path")
    .datum(data)
    .attr("class", "line")
    .attr("d", line);

var focus = svg.append("g")
    .attr("class", "focus")
    .style("display", "none");

focus.append("circle")
    .attr("r", 4.5);

focus.append("text")
    .attr("x", 9)
    .attr("dy", ".35em");

svg.append("rect")
    .attr("class", "overlay")
    .attr("width", width)
    .attr("height", height)
    .on("mouseover", function() {
        focus.style("display", null);
    })
    .on("mouseout", function() {
        focus.style("display", "none");
    })
    .on("mousemove", mousemove);

function mousemove() {
    var x0 = x.invert(d3.mouse(this)[0]),
        i = bisectDate(data, x0, 1),
        d0 = data[i - 1],
        d1 = data[i],
        d = x0 - d0.date > d1.date - x0 ? d1 : d0;
    focus.attr("transform", "translate(" + x(d.date) + "," + y(d.close) + ")");
    focus.select("text").text(formatCurrency(d.close));
}
body {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

.overlay {
  fill: none;
  pointer-events: all;
}

.focus circle {
  fill: none;
  stroke: steelblue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 1

Related Questions