18

i am writing a Gantt Chart using d3

i have xScale with Time Scale(Time)

this.xScale = d3.scaleTime()
            .domain([this.startDate,this.endDate])
            .range([0,this.getWidth()]);

and yScale as Band Scale (Resources)

this.yScale = d3.scaleBand()
        .domain(resources.map(function(res){
            return res.res_num;
        }))
        .rangeRound([0,this.getHeight()])
        .paddingInner(.3)

Problem is i need to drag and drop the task( SVG Rect) from one resource to another resources

When i drag i am using the transfrom so its moving on the SVG

_onDrag : function(d)
    {
        var x = d3.event.dx+d3.event.x;
        var y = d3.event.dy+d3.event.y;
        d3.select(this).raise().attr("transform","translate(" + [x,y] + ")");
    },

But on drop i have to handle the logic as below:

  1. the rect should not be in between the resources, so need to transform to any band based on d3.event.y
    1. in xAxis the time scale which has the invert but the yScale not have this. How to do this?
chiranjeevigk
  • 1,636
  • 1
  • 23
  • 43

4 Answers4

32

Solved this as below but i don't know this is standard way, if any other way please answer to this.

var eachBand = self.yScale.step();
var index = Math.round((d3.event.y / eachBand));
var val = self.yScale.domain()[index];

eachBand gives the size of the band in pixels if i divide the y value (drag event) by the eachBand i will get the index which gives the yScale value

chiranjeevigk
  • 1,636
  • 1
  • 23
  • 43
18

When the graph has padding on the left and right the answers give undefined as result if the value is outside the bars of the graph. The following function can be used to take this padding into consideration and clamp the index to the valid values of the domain.

function scaleBandInvert(scale) {
  var domain = scale.domain();
  var paddingOuter = scale(domain[0]);
  var eachBand = scale.step();
  return function (value) {
    var index = Math.floor(((value - paddingOuter) / eachBand));
    return domain[Math.max(0,Math.min(index, domain.length-1))];
  }
}

It can be used as follows:

var xScale = d3.scaleBand().domain(...).rangeRound([a,b]);
....
var dvalX = scaleBandInvert(xScale)(value);  // single invert
....
var dRangeX = d3.event.selection.map(scaleBandInvert(xScale));  // in a brush event handler
rioV8
  • 24,506
  • 3
  • 32
  • 49
  • 1
    Thank you for this: `var paddingOuter = scale(domain[0]);` – Llorenç Pujol Ferriol Dec 02 '19 at 11:30
  • I found that in order for the bisect to switch between bars, I had to use `Math.round` and `paddingInner`: `var paddingInner = scale.step() * scale.paddingInner();` and then `var index = Math.round((x - scaleStart - paddingInner / 2) / eachBand);` – tgordon18 Dec 10 '19 at 13:24
  • Nice, but just a small addition. To me it worked more precisely if I used `const paddingOuter = scale.paddingOuter();` instead of `const paddingOuter = scale(domain[0]);` – Omar Omeiri Dec 12 '22 at 10:32
12

One tweak to the accepted answer - you may want to use Math.floor rather than Math.round as the former will return the next level lower if you happen to select below the center point of the item. Modified solution is thus:

var eachBand = self.yScale.step();
var index = Math.floor((d3.event.y / eachBand));
var val = self.yScale.domain()[index];
Samantha Branham
  • 7,350
  • 2
  • 32
  • 44
Matthew Cox
  • 1,047
  • 10
  • 23
0

@rioV8's answer did work for me.

But In case you are changing the scale's range for zooming or panning, it will break.

For those that need zoom:

import type { ScaleBand } from 'd3';

function invertBand(scale: ScaleBand<string>) {
  const domain = scale.domain();
  const step = scale.step();
  const firstVisibleBand = Math.abs(scale.range()[0]) / step;

  return function (v: number) {
    const index = Math.floor(
      (
        (v / WIDTH)
        / (step / WIDTH)
      )
      + firstVisibleBand,
    );
    return domain[Math.max(0, Math.min(index, domain.length - 1))];
  };
}

Omar Omeiri
  • 1,506
  • 1
  • 17
  • 33