How to use x and width in a bar chart with scaleTime?
Asked Answered
F

2

5

I have a codepen here - https://codepen.io/anon/pen/xpaYYw?editors=0010

Its a simple test graph but the date will be formatted like this.

I have dates on the x axis and amounts on the y

How can I use the x scale to set the width and x position of the bars.

    layers.selectAll('rect')
      .data(data)
      .enter()
      .append('rect')

      .attr('height', function(d, i) {
        return height - y(d.one);
      })

      .attr('y', function(d, i) {
        return y(d.one);
      })

      .attr('width', function(d, i) {
        return 50;
      })

      .attr('x', function(d, i) {
        return 80*i;
      })

      .style('fill', (d, i) => {
        return colors[i];
      });
Foresight answered 16/1, 2018 at 10:22 Comment(0)
B
8

The problem with your question has nothing to do with programming, or JavaScript, or D3... the problem is a basic dataviz concept (that's why I added the tag in your question):

What you're trying to do is not correct! You should not use bars with a time scale. Time scales are for time series (in which we use dots, or dots connected by lines).

If you use bars with time in the x axis you'll face problems:

  1. Positioning the bar: the left margin of the bar will be always at the date you set. The whole bar will lie after that date;
  2. Setting the width of the bar: in a real bar chart, which uses categorical variables for the x axis, the width has no meaning. But in a time scale the width represents time.

However, just for the sake of explanation, let's create this bar chart with a time scale (despite the fact that this is a wrong choice)... Here is how to do it:

First, set the "width" of the bars in time. Let's say, each bar will have 10 days of width:

.attr("width", function(d){
    return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})

Then, set the x position of the bar to the current date less half its width (that is, less 5 days in our example):

.attr('x', function(d, i) {
    return x(d3.timeDay.offset(d.date, -5));
})

Finally, don't forget to create a "padding" in the time scale:

var x = d3.scaleTime()
  .domain([d3.min(data, function(d) {
    return d3.timeDay.offset(d.date, -10);
  }), d3.max(data, function(d) {
    return d3.timeDay.offset(d.date, 10);
  })])
  .range([0, width]);

Here is your code with those changes:

var keys = [];
var legendKeys = [];

var maxVal = [];

var w = 800;
var h = 450;

var margin = {
  top: 30,
  bottom: 40,
  left: 50,
  right: 20,
};

var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;

var colors = ['#FF9A00', '#FFEBB6', '#FFC400', '#B4EDA0', '#FF4436'];

var data = [{
    "one": 4306,
    "two": 2465,
    "three": 2299,
    "four": 988,
    "five": 554,
    "six": 1841,
    "date": "2015-05-31T00:00:00"
  }, {
    "one": 4378,
    "two": 2457,
    "three": 2348,
    "four": 1021,
    "five": 498,
    "six": 1921,
    "date": "2015-06-30T00:00:00"
  }, {
    "one": 3404,
    "two": 2348,
    "three": 1655,
    "four": 809,
    "five": 473,
    "six": 1056,
    "date": "2015-07-31T00:00:00"
  },

];

data.forEach(function(d) {
  d.date = new Date(d.date)
})

for (var i = 0; i < data.length; i++) {
  for (var key in data[i]) {
    if (!data.hasOwnProperty(key) && key !== "date")
      maxVal.push(data[i][key]);
  }
}

var x = d3.scaleTime()
  .domain([d3.min(data, function(d) {
    return d3.timeDay.offset(d.date, -10);
  }), d3.max(data, function(d) {
    return d3.timeDay.offset(d.date, 10);
  })])
  .range([0, width]);

var y = d3.scaleLinear()
  .domain([0, d3.max(maxVal, function(d) {
    return d;
  })])
  .range([height, 0]);

var svg = d3.select('body').append('svg')
  .attr('class', 'chart')
  .attr('width', w)
  .attr('height', h);

var chart = svg.append('g')
  .classed('graph', true)
  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

var layersArea = chart.append('g')
  .attr('class', 'layers');


var layers = layersArea.append('g')
  .attr('class', 'layer');

layers.selectAll('rect')
  .data(data)
  .enter()
  .append('rect')

.attr('height', function(d, i) {
  return height - y(d.one);
})

.attr('y', function(d, i) {
  return y(d.one);
})

// .attr('width', function(d, i) {
//   return 50;
// })

.attr("width", function(d) {
  return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})

.attr('x', function(d, i) {
  return x(d3.timeDay.offset(d.date, -5));
})

.style('fill', (d, i) => {
  return colors[i];
});

chart.append('g')
  .classed('x axis', true)
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x)
    .tickFormat(d3.timeFormat("%Y-%m-%d")).tickValues(data.map(function(d) {
      return new Date(d.date)
    })));

chart.append('g')
  .classed('y axis', true)
  .call(d3.axisLeft(y)
    .ticks(10));
<script src="https://d3js.org/d3.v4.min.js"></script>
Breann answered 16/1, 2018 at 10:53 Comment(3)
Gerardo, thanks for your help. I see what you mean now about not using scaleTime. I don't care about the time or data in between those dates. It will just show data for that month. Would I be better using a scaleLinear for the dates on the x axis as they are just a moment in time, they could just months. I just need to convert the time stamp in the data to a readable dateForesight
In that case, you could use a band scale, just passing the strings.Breann
While I understand the concerns about using scaleTime with a bar chart and the challenges that introduces I don't think it's necessarily "wrong" as ttmt stated. In fact, sometimes it's very appropriate. For example, the NYTimes has several good examples of this in their COVID graphs in which they have bars as the raw data and add a line on top of the bars for the 7 day average. It is nice to have a scaleTime scale to allow zooming in and out to give the x ticks an appropriate date label for this type of situation. But, yes, for most bar charts a scaleLinear or scaleBand is better.Sleety
S
1

Regardless of what Gerardo is saying, I partially agree, because sometimes you want to compare monthly data which can be categorical.

For this purposes 2 scales need to be defined, 1 is scaleTime() and then scaleBand which has range dependant on scaleTime() output.

xTime = d3.scaleTime().range([0, WIDTH]).domain([minDate, maxDate]);
xScale = d3
  .scaleBand()
  .range([0, WIDTH])
  .domain(d3.timeMonth.range(...xTime.domain()));

in this example I used minDate, maxDate in case you want to display 6 months of data regardless if data actually has said data or not, otherwise extent() can be used to get min and max values.

Good thing about this approach is that is dynamic and missing values will display no data, however space will be dedicated to it.

Scent answered 27/6, 2023 at 10:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.