CSNW / d3.compose

Compose complex, data-driven visualizations from reusable charts and components with d3
http://CSNW.github.io/d3.compose/
MIT License
697 stars 24 forks source link

Time scales do not have width or rangeBand #45

Closed tpitale closed 8 years ago

tpitale commented 8 years ago

The itemWidth function returns either a scale's width or uses the rangeBand to calculate one. It appears that d3.scale.time has neither function.

I've not come up with a workaround for this, as most suggestions are to simply calculate width/data.length which seems unlikely to be what's intended.

Open to suggestions!

timhall commented 8 years ago

This is similar to #34 and I haven't figured out a clean way to make this work. It's tricky because the width value for the chart depends on how it's laid out so it's difficult to know ahead of time. Some possibilities:

  1. (from #34) specify an interval for the chart and calculate the width from that
  2. From your suggestion, specify the data length and calculate the width from this (automatically determining is difficult since the data could be sparse)
  3. Use the linear/time scale to specify the domain for an ordinal scale

I think an interval solution will work and I've been investigating how best to add it, but I think the best solution is to create an ordinal scale from the given linear/time scale:

// Use linear/time scale to define ticks for ordinal
var xScale = d3.scale.linear().domain([0, 50]);
var xScaleOrdinal = d3.scale.ordinal().domain(xScale.ticks(10));
// => pass ordinal to bars

Closing this issue in favor of #34

tpitale commented 8 years ago

I tried:

xScale = d3c.helpers.createScale({type: 'time', data: options.data, key: 'x', adjacent: true})
xScaleOrdinal = d3.scale.ordinal().domain(xScale.ticks(10));

scales = {
  x: xScaleOrdinal,
  y: {domain: [-5, 5]}
}

And got some very strange results on the axis. And no width.

I'm probably missing something.

I also tried passing the domain from time xscale to the ordinal domain.

apologies for the coffeescript, not my project

tpitale commented 8 years ago

Can I just set the bar width myself? I see there's a function, but unclear how it's used. I tried call barWidth on a chart and passing a function. That just errors. Again, probably doing it wrong.

tpitale commented 8 years ago

Well, I'm getting somewhere. Somewhere weird.

  height = 350
  width = 960

  formatDate = d3.time.format('%H:%M')
  parseDate = d3.time.format("%Y-%m-%d %H:%M").parse

  chart = ->
    d3.select('#response_container .d3-graph').chart('Compose', (options) ->
      # Options function:
      # Gets evaluated on each draw and takes in data and returns options

      # xScale = d3c.helpers.createScale({type: 'time', data: options.data, key: 'x', adjacent: true})
      # xScaleOrdinal = d3.scale.ordinal().domain(xScale.domain());

      domain = d3.extent(options.data, (d) -> d.x)
      # timeScale = d3c.helpers.createScale({type: 'time', domain: domain, range: [0, width], adjacent: true})

      scales = {
        # x: d3.scale.ordinal().domain(domain).range([0, width])
        x: {type: 'ordinal', domain: domain, range: [0, width], adjacent: true},
        y: {domain: [-5, 5], range: [height, 0]}
      }

      charts = [
        # Create results chart:
        # d3c.lines -> type: 'Lines' -> d3.chart('Lines')
        # 'results' -> chart key for transitions and legend
        # {data: data} -> pass given data to Lines chart
        d3c.bars('results', {data: options.data, xScale: scales.x, yScale: scales.y})
      ]

      title = d3c.title('Responses')
      legend = d3c.legend({charts: ['results']}) # match key to charts key/name
      x_axis = d3c.axis('xAxis', {scale: scales.x, tickFormat: formatDate})
      y_axis = d3c.axis('yAxis', {scale: scales.y, ticks: 5})

      [
        # Add charts to chart layer (d3c.layered)
        title,
        [y_axis, d3c.layered(charts), legend],
        x_axis
      ]
    )
    # .margins({
    #   top: 35, right: 20, bottom: 20, left: 40
    # }).height(350).responsive(true)

  bar_chart = chart()
  bar_chart.height(height)
  bar_chart.width(width)

  data = [
    {x: parseDate('2016-02-11 11:01'), y: -5},
    {x: parseDate('2016-02-11 11:02'), y: -2},
    {x: parseDate('2016-02-11 11:03'), y: 1},
    {x: parseDate('2016-02-11 11:04'), y: -5},
    {x: parseDate('2016-02-11 11:05'), y: -2},
    {x: parseDate('2016-02-11 11:06'), y: 1},
    {x: parseDate('2016-02-11 11:07'), y: -5},
    {x: parseDate('2016-02-11 11:08'), y: -2},
    {x: parseDate('2016-02-11 11:09'), y: 1}
  ]

  bar_chart.draw({
    data: data
  })

Get's me:

screen shot 2016-02-11 at 10 49 44 pm
tpitale commented 8 years ago

I can get the xAxis … or I can get bars with width. But not both.

tpitale commented 8 years ago

This is the BEST!

  height = 350
  width = 960

  formatDate = d3.time.format('%H:%M')
  parseDate = d3.time.format("%Y-%m-%d %H:%M").parse

  chart = ->
    d3.select('#response_container .d3-graph').chart('Compose', (options) ->
      # Options function:
      # Gets evaluated on each draw and takes in data and returns options

      # xScale = d3c.helpers.createScale({type: 'time', data: options.data, key: 'x', adjacent: true})
      # xScaleOrdinal = d3.scale.ordinal().domain(xScale.domain());

      domain = d3.extent(options.data, (d) -> d.x)
      # timeScale = d3c.helpers.createScale({type: 'time', domain: domain, range: [0, width], adjacent: true})

      scales = {
        # x: d3.scale.ordinal().domain(domain).range([0, width])
        x: d3c.helpers.createScale({type: 'ordinal', domain: domain, range: [0, width], adjacent: true}),
        y: {domain: [-5, 5], range: [height, 0]}
      }

      charts = [
        # Create results chart:
        # d3c.lines -> type: 'Lines' -> d3.chart('Lines')
        # 'results' -> chart key for transitions and legend
        # {data: data} -> pass given data to Lines chart
        d3c.bars('results', {data: options.data, xScale: scales.x, yScale: scales.y})
      ]

      console.log(scales.x(options.data[0].x))
      console.log(scales.x(options.data[1].x))
      console.log(scales.x(options.data[2].x))
      console.log(scales.x(options.data[3].x))
      console.log(scales.x(options.data[4].x))
      console.log(scales.x(options.data[5].x))
      console.log(scales.x(options.data[6].x))
      console.log(scales.x(options.data[7].x))
      console.log(scales.x(options.data[8].x))

      title = d3c.title('Responses')
      legend = d3c.legend({charts: ['results']}) # match key to charts key/name
      x_axis = d3c.axis('xAxis', {scale: scales.x, tickFormat: formatDate})
      y_axis = d3c.axis('yAxis', {scale: scales.y, ticks: 5})

      [
        # Add charts to chart layer (d3c.layered)
        title,
        [y_axis, d3c.layered(charts), legend],
        x_axis
      ]
    )
    # .margins({
    #   top: 35, right: 20, bottom: 20, left: 40
    # }).height(350).responsive(true)

  bar_chart = chart()
  bar_chart.height(height)
  bar_chart.width(width)

  data = [
    {x: parseDate('2016-02-11 11:01'), y: -5},
    {x: parseDate('2016-02-11 11:02'), y: -2},
    {x: parseDate('2016-02-11 11:03'), y: 1},
    {x: parseDate('2016-02-11 11:04'), y: -5},
    {x: parseDate('2016-02-11 11:05'), y: -2},
    {x: parseDate('2016-02-11 11:06'), y: 1},
    {x: parseDate('2016-02-11 11:07'), y: -5},
    {x: parseDate('2016-02-11 11:08'), y: -2},
    {x: parseDate('2016-02-11 11:09'), y: 1}
  ]

  bar_chart.draw({
    data: data
  })

Yields the correct graph! If I remove all the console.log bits, it no longer works!

tpitale commented 8 years ago

Obviously, nothing to do with the console.log itself. Just the usage of the x scale.

tpitale commented 8 years ago

Turned it into options.data.forEach((value, key) -> scales.x(value.x)).

tpitale commented 8 years ago

The scale has to be time to get the right output from calling the scale.

Thu Feb 11 2016 11:01:00 GMT-0600 (CST) 0
Thu Feb 11 2016 11:02:00 GMT-0600 (CST) 120
Thu Feb 11 2016 11:03:00 GMT-0600 (CST) 240
Thu Feb 11 2016 11:04:00 GMT-0600 (CST) 360
Thu Feb 11 2016 11:05:00 GMT-0600 (CST) 480
Thu Feb 11 2016 11:06:00 GMT-0600 (CST) 600
Thu Feb 11 2016 11:07:00 GMT-0600 (CST) 720
Thu Feb 11 2016 11:08:00 GMT-0600 (CST) 840
Thu Feb 11 2016 11:09:00 GMT-0600 (CST) 960

This output is using a time type rather than ordinal. Ordinal gives the right bars (sometimes out of order, though). Time gives the right scale, order/position. But no width, obviously.

tpitale commented 8 years ago

Ordinal gives this output:

Thu Feb 11 2016 11:01:00 GMT-0600 (CST) 240
Thu Feb 11 2016 11:02:00 GMT-0600 (CST) 240
Thu Feb 11 2016 11:03:00 GMT-0600 (CST) 720
Thu Feb 11 2016 11:04:00 GMT-0600 (CST) 240
Thu Feb 11 2016 11:05:00 GMT-0600 (CST) 720
Thu Feb 11 2016 11:06:00 GMT-0600 (CST) 240
Thu Feb 11 2016 11:07:00 GMT-0600 (CST) 720
Thu Feb 11 2016 11:08:00 GMT-0600 (CST) 240
Thu Feb 11 2016 11:09:00 GMT-0600 (CST) 720
tpitale commented 8 years ago

Based on: https://github.com/mbostock/d3/wiki/Ordinal-Scales, ordinal is not what I want. I want linear (or it's extension, time). But linear gives me an error about n.getHour. Not sure what that is yet.

timhall commented 8 years ago

I think the following line is your issue:

domain = d3.extent(options.data, (d) -> d.x)

This sets the domain to [Feb 11 11:01, Feb 11 11:09] and ignores all of the times in between. For ordinal every x value needs to be in the domain so the domain should be something like the following:

domain = data.map(d => d.x)
// domain = [Feb 11 11:01, Feb 11 11:02, Feb 1111:03, ...]

scales = {
  x: {type: 'ordinal', domain: domain, adjacent: true},
  y: {domain: [-5, 5]}
}

Let me know if that works for you.