限制d3中Y轴刻度数量,在柱形图上保留顶部刻度。

3

在我的d3柱状图中,如果最高的柱子超过了最后一个刻度线,我应该在顶部Y轴刻度线(带有水平网格线)。这是通过计算最后一个刻度线,然后使用tickValues()应用实现的。

另外,包括x轴域(0)在内,应该最多有5个刻度线和网格线。我尝试过使用ticks(),但与tickValues()不兼容。有解决方法吗?

// container size
var margin = {top: 10, right: 10, bottom: 30, left: 30},
width = 400,
height = 300;

var data = [
{"month":"DEC","setup":{"count":26,"id":1,"label":"Set Up","year":"2016","graphType":"setup"}},
{"month":"JAN","setup":{"count":30,"id":1,"label":"Set Up","year":"2017","graphType":"setup"}},
{"month":"FEB","setup":{"count":30,"id":1,"label":"Set Up","year":"2017","graphType":"setup"}}];

var name = 'dashboard';

// x scale
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.2);

// set x and y scales
xScale.domain(data.map(function(d) { return d.month; }));

// x axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.outerTickSize(0);

var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) {  
    return d.setup.count;
})])
.range([height, 0]);

var ticks = yScale.ticks(),
lastTick = ticks[ticks.length-1];    
var newLastTick = lastTick + (ticks[1] - ticks[0]);  
if (lastTick < yScale.domain()[1]){
    ticks.push(lastTick + (ticks[1] - ticks[0]));
}

// adjust domain for further value
yScale.domain([yScale.domain()[0], newLastTick]);

// y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(-width, 0, 0) 
.tickFormat(d3.format('d'))
.tickValues(ticks);


// create svg container
var svg = d3.select('#chart')
.append('svg')
.attr('class','d3-setup-barchart')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//.on('mouseout', tip.hide);        

// apply tooltip
//svg.call(tip);

// Horizontal grid (y axis gridline)
svg.append('g')         
.attr('class', 'grid horizontal')
.call(d3.svg.axis()
      .scale(yScale)
      .orient('left')
      .tickSize(-width, 0, 0) 
      .tickFormat('')
      .tickValues(ticks)
      );

// create bars
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('g');

bars.append('rect')
.attr('class', function(d,i) {
    return 'bar';
})
.attr('id', function(d, i) {
    return name+'-bar-'+i;
})
.attr('x', function(d) { return xScale(d.month); })
.attr('width', xScale.rangeBand())
.attr('y', function(d) { return yScale(d.setup.count); })
.attr('height', function(d) { return height - yScale(d.setup.count); })
.on('click', function(d, i) {
    d3.select(this.nextSibling)
    .classed('label-text selected', true);
    d3.select(this)
    .classed('bar selected', true);  
    d3.select('#'+name+'-axis-text-'+i)
    .classed('axis-text selected', true);
});
//.on('mouseover', tip.show)
//.on('mouseout', tip.hide);

// apply text at the top
bars.append('text')
.attr('class',function(d,i) {
    return 'label-text';
})
.attr('x', function(d) { return xScale(d.month) + (xScale.rangeBand()/2) - 10; })
.attr('y', function(d) { return yScale(d.setup.count) + 2 ; })
.attr('transform', function() { return 'translate(10, -10)'; })
.text(function(d) { return d.setup.count; });

// draw x axis
svg.append('g')
.attr('id', name+'-x-axis')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);

// apply class & id to x-axis texts
d3.select('#'+name+'-x-axis')
.selectAll('text')
.attr('class', function(d,i) {
    return 'axis-text';
})
.attr('id', function(d,i) { return name+'-axis-text-' + i; });

// draw y axis
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');

// remove 0 in y axis
svg.select('.y')
.selectAll('.tick')
.filter(function (d) { 
    return d === 0 || d % 1 !== 0;     
}).remove();

svg
.select('.horizontal')
.selectAll('.tick')
.filter(function (d) { 
    return d === 0 || d % 1 !== 0;     
}).remove();

JSFiddle


就像你的另一个问题一样,在D3 v4中这将非常容易...您可以使用d3.ticks设置仅包含5个值(包括修改后的最后一个刻度线)的数组,并将该数组传递给tickValues。我知道这是很多工作,但是将您的代码移动到v4,您会发现事情会更容易! - Gerardo Furtado
谢谢@GerardoFurtado,我会把所有的图表都迁移到d3 v4。 - Anshad Vattapoyil
@GerardoFurtado 为了快速修复,有没有可能通过更改lastTick计算步骤来解决这个问题? - Anshad Vattapoyil
1个回答

2

如我在评论中所说的那样,如果您使用的是D3 v4.x,那么这将非常容易:您可以使用d3.ticksd3.range来设置tickValues

但是,如果您想坚持使用D3 v3,则有一个解决方案。

在您的情况下,默认方法是使用scale.ticks设置刻度线的数量。然而,正如API所说的那样:

如果count是一个数字,那么大约会返回count个刻度线。如果未指定count,则默认为10。指定的计数仅是提示;根据输入域,比例尺可能会返回更多或更少的值。(强调是我的)

因此,您不能在这里使用scale.ticks来设置固定数量的5个刻度线。

因此,我的解决方案涉及创建自己的函数来计算刻度线。这并不复杂。这就是它:

function createTicks(start, stop, count) {
    var difference = stop - start;
    var steps = difference / (count - 1);
    var arr = [start];
    for (var i = 1; i < count; i++) {
        arr.push(~~(start + steps * i))
    }
    return arr;
}

该函数需要三个参数:第一个值(start),最后一个值(stop)和刻度数量(count)。我使用双NOT,因为不知何故,您正在过滤非整数值。所以,我们只需要在yScale域中设置最大刻度。例如,将最大刻度设为最大值的10%:
var yScale = d3.scale.linear()
    .domain([0, d3.max(data, function(d) {
        return d.setup.count;
    }) * 1.1])
    //    ^----- 10% increase
    .range([height, 0]);

(如果需要,您可以保留原有的数学计算来获取新的最后一个刻度点,我只是展示了一种与数据中最大值不同的设置域的最大值的不同方法)
接下来,我们定义y轴的刻度。
var axisTicks = createTicks(yScale.domain()[0], yScale.domain()[1], 5);

使用我们自定义的函数与您的域名配合使用,它将返回以下数组:
[0, 8, 16, 24, 33]

接下来,只需要在axis.tickValues中使用该数组即可。

这是您更新后的 fiddle 示例:https://jsfiddle.net/7ktzpnno/

下面是相同代码的 Stack 片段:

// container size
var margin = {
    top: 10,
    right: 10,
    bottom: 30,
    left: 30
  },
  width = 400,
  height = 300;

var data = [{
  "month": "DEC",
  "setup": {
    "count": 26,
    "id": 1,
    "label": "Set Up",
    "year": "2016",
    "graphType": "setup"
  }
}, {
  "month": "JAN",
  "setup": {
    "count": 30,
    "id": 1,
    "label": "Set Up",
    "year": "2017",
    "graphType": "setup"
  }
}, {
  "month": "FEB",
  "setup": {
    "count": 30,
    "id": 1,
    "label": "Set Up",
    "year": "2017",
    "graphType": "setup"
  }
}];

var name = 'dashboard';

// x scale
var xScale = d3.scale.ordinal()
  .rangeRoundBands([0, width], 0.2);

// set x and y scales
xScale.domain(data.map(function(d) {
  return d.month;
}));

// x axis
var xAxis = d3.svg.axis()
  .scale(xScale)
  .orient('bottom')
  .outerTickSize(0);

var yScale = d3.scale.linear()
  .domain([0, d3.max(data, function(d) {
    return d.setup.count;
  }) * 1.1])
  .range([height, 0]);

var axisTicks = createTicks(yScale.domain()[0], yScale.domain()[1], 5);

function createTicks(start, stop, count) {
  var difference = stop - start;
  var steps = difference / (count - 1);
  var arr = [start];
  for (var i = 1; i < count; i++) {
    arr.push(~~(start + steps * i))
  }
  return arr;
}

// y axis
var yAxis = d3.svg.axis()
  .scale(yScale)
  .orient('left')
  .tickSize(-width, 0, 0)
  .tickValues(axisTicks);

// tooltip
// var tip = d3.tip()
// .attr('class', 'd3-tip')
// .offset([-10, 0])
// .html(function(d) {
//  return '<span class="tooltip-line">'+d.patientSetup.label+': '+
//  d.patientSetup.count + '</span><span>'+d.patientNotSetup.label+': '+
//  d.patientNotSetup.count + '</span>';
// });

// create svg container
var svg = d3.select('#chart')
  .append('svg')
  .attr('class', 'd3-setup-barchart')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  .append('g')
  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//.on('mouseout', tip.hide);        

// apply tooltip
//svg.call(tip);

// Horizontal grid (y axis gridline)
svg.append('g')
  .attr('class', 'grid horizontal')
  .call(d3.svg.axis()
    .scale(yScale)
    .orient('left')
    .tickSize(-width, 0, 0)
    .tickValues(axisTicks)
  );

// create bars
var bars = svg.selectAll('.bar')
  .data(data)
  .enter()
  .append('g');

bars.append('rect')
  .attr('class', function(d, i) {
    return 'bar';
  })
  .attr('id', function(d, i) {
    return name + '-bar-' + i;
  })
  .attr('x', function(d) {
    return xScale(d.month);
  })
  .attr('width', xScale.rangeBand())
  .attr('y', function(d) {
    return yScale(d.setup.count);
  })
  .attr('height', function(d) {
    return height - yScale(d.setup.count);
  })
  .on('click', function(d, i) {
    d3.select(this.nextSibling)
      .classed('label-text selected', true);
    d3.select(this)
      .classed('bar selected', true);
    d3.select('#' + name + '-axis-text-' + i)
      .classed('axis-text selected', true);
  });
//.on('mouseover', tip.show)
//.on('mouseout', tip.hide);

// apply text at the top
bars.append('text')
  .attr('class', function(d, i) {
    return 'label-text';
  })
  .attr('x', function(d) {
    return xScale(d.month) + (xScale.rangeBand() / 2) - 10;
  })
  .attr('y', function(d) {
    return yScale(d.setup.count) + 2;
  })
  .attr('transform', function() {
    return 'translate(10, -10)';
  })
  .text(function(d) {
    return d.setup.count;
  });

// draw x axis
svg.append('g')
  .attr('id', name + '-x-axis')
  .attr('class', 'x axis')
  .attr('transform', 'translate(0,' + height + ')')
  .call(xAxis);

// apply class & id to x-axis texts
d3.select('#' + name + '-x-axis')
  .selectAll('text')
  .attr('class', function(d, i) {
    return 'axis-text';
  })
  .attr('id', function(d, i) {
    return name + '-axis-text-' + i;
  });

// draw y axis
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');

// remove 0 in y axis
svg.select('.y')
  .selectAll('.tick')
  .filter(function(d) {
    return d === 0 || d % 1 !== 0;
  }).remove();

svg
  .select('.horizontal')
  .selectAll('.tick')
  .filter(function(d) {
    return d === 0 || d % 1 !== 0;
  }).remove();
.d3-setup-barchart {
 background-color: #666666;
}

.d3-setup-barchart .axis path {
 fill: none;
 stroke: #000;
}

.d3-setup-barchart .bar {
 fill: #ccc;
}

.d3-setup-barchart .bar:hover {
 fill: orange;
 cursor: pointer;
}

.d3-setup-barchart .bar.selected {
 fill: orange;
 stroke: #fff;
 stroke-width: 2;
}

.d3-setup-barchart .label-text {
 text-anchor: middle;
 font-size: 12px;
 font-weight: bold;
 fill: orange;
 opacity: 0;
}

.d3-setup-barchart .label-text.selected {
 opacity: 1;
}

.d3-setup-barchart .axis text {
 fill: rgba(255, 255, 255, 0.6);
 font-size: 9px;
}

.d3-setup-barchart .axis-text.selected {
 fill: orange;
}

.d3-setup-barchart .y.axis path {
 display: none;
}

.d3-setup-barchart .y.axis text {
 font-size: 6px;
}

.d3-setup-barchart .x.axis path {
 fill: none;
 stroke: #353C41;
}

.d3-setup-barchart .grid .tick {
 stroke: #fff;
 opacity: .18 !important;
 stroke-width: 0;
}

.d3-setup-barchart .grid .tick line {
 stroke-width: .5 !important;
}

.d3-setup-barchart .grid path {
 stroke-width: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart"></div>

提示: 在你的问题中,你说“包括x轴域(0)应该最多有5个刻度和网格线。”。然而,在你的代码中,你故意删除了0刻度。如果你想在y轴上看到0刻度,请删除那个块:https://jsfiddle.net/jz0q547u/

谢谢,我已经开始将所有的图表迁移到v4。但是需要立即修复这个问题,所以请求v3的解决方案。在这个fiddle中,我可以看到刻度数值反映了两次(你可以看到重复的刻度值,字体大小稍微大一些)。这是因为水平网格线方法造成的吗? - Anshad Vattapoyil
是的,我注意到了...我以为那是有意为之的。所以,不是吗? - Gerardo Furtado
不,我会通过应用CSS选择器来隐藏它。如果您有其他更好的解决方案,请告诉我。 - Anshad Vattapoyil
我在这里有一个疑问,我们不能以相等的间距获取刻度数吗?另外一个问题是,我们不能传递最高柱形图计数而不是将域作为结束值传递吗?在这个例子中,0,33 - 8, 16, 24, 33,它给出了相等的总和,但在这个例子中0,66 - 16, 33, 49, 66,却没有。 - Anshad Vattapoyil
我在数学方面有点弱,所以想要澄清一下。第二组以16开始,所以下一个数字应该是32对吗?在16和33之间的差为17,但是在33和49之间又变成了16。 - Anshad Vattapoyil
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接