Skip to content

Commit f3a45af

Browse files
committed
bar: draw bar texts
1 parent 2b404c6 commit f3a45af

File tree

2 files changed

+291
-9
lines changed

2 files changed

+291
-9
lines changed

src/traces/bar/plot.js

Lines changed: 272 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
1414

1515
var Lib = require('../../lib');
16+
var svgTextUtils = require('../../lib/svg_text_utils');
17+
1618
var Color = require('../../components/color');
19+
var Drawing = require('../../components/drawing');
1720
var ErrorBars = require('../../components/errorbars');
1821

1922
var arraysToCalcdata = require('./arrays_to_calcdata');
@@ -42,9 +45,9 @@ module.exports = function plot(gd, plotinfo, cdbar) {
4245

4346
arraysToCalcdata(d);
4447

45-
d3.select(this).selectAll('path')
48+
d3.select(this).selectAll('g.point')
4649
.data(Lib.identity)
47-
.enter().append('path')
50+
.enter().append('g').classed('point', true)
4851
.each(function(di, i) {
4952
// now display the bar
5053
// clipped xf/yf (2nd arg true): non-positive
@@ -75,15 +78,18 @@ module.exports = function plot(gd, plotinfo, cdbar) {
7578
d3.select(this).remove();
7679
return;
7780
}
81+
7882
var lw = (di.mlw + 1 || trace.marker.line.width + 1 ||
7983
(di.trace ? di.trace.marker.line.width : 0) + 1) - 1,
8084
offset = d3.round((lw / 2) % 1, 2);
85+
8186
function roundWithLine(v) {
8287
// if there are explicit gaps, don't round,
8388
// it can make the gaps look crappy
8489
return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ?
8590
d3.round(Math.round(v) - offset, 2) : v;
8691
}
92+
8793
function expandToVisible(v, vc) {
8894
// if it's not in danger of disappearing entirely,
8995
// round more precisely
@@ -93,6 +99,7 @@ module.exports = function plot(gd, plotinfo, cdbar) {
9399
// its neighbor
94100
(v > vc ? Math.ceil(v) : Math.floor(v));
95101
}
102+
96103
if(!gd._context.staticPlot) {
97104
// if bars are not fully opaque or they have a line
98105
// around them, round to integer pixels, mainly for
@@ -108,12 +115,274 @@ module.exports = function plot(gd, plotinfo, cdbar) {
108115
y0 = fixpx(y0, y1);
109116
y1 = fixpx(y1, y0);
110117
}
111-
d3.select(this).attr('d',
118+
119+
// append bar path and text
120+
var bar = d3.select(this);
121+
122+
bar.append('path').attr('d',
112123
'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z');
124+
125+
appendBarText(gd, bar, d, i, x0, x1, y0, y1);
113126
});
114127
});
115128

116129
// error bars are on the top
117130
bartraces.call(ErrorBars.plot, plotinfo);
118131

119132
};
133+
134+
function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
135+
var trace = calcTrace[0].trace;
136+
137+
// get bar text
138+
var traceText = trace.text;
139+
if(!traceText) return;
140+
141+
var text;
142+
if(Array.isArray(traceText)) {
143+
if(i >= traceText.length) return;
144+
text = traceText[i];
145+
}
146+
else {
147+
text = traceText;
148+
}
149+
150+
// get text position
151+
var traceTextPosition = trace.textposition,
152+
textPosition;
153+
if(Array.isArray(traceTextPosition)) {
154+
if(i >= traceTextPosition.length) return;
155+
textPosition = traceTextPosition[i];
156+
}
157+
else {
158+
textPosition = traceTextPosition;
159+
}
160+
161+
if(textPosition === 'none') return;
162+
163+
var barWidth = Math.abs(x1 - x0),
164+
barHeight = Math.abs(y1 - y0),
165+
barIsTooSmall = (barWidth < 8 || barHeight < 8),
166+
167+
barmode = gd._fullLayout.barmode,
168+
inStackMode = (barmode === 'stack'),
169+
inRelativeMode = (barmode === 'relative'),
170+
inStackOrRelativeMode = inStackMode || inRelativeMode,
171+
172+
calcBar = calcTrace[i],
173+
isOutmostBar = !inStackOrRelativeMode || calcBar._outmost;
174+
175+
if(textPosition === 'auto') {
176+
textPosition = (barIsTooSmall && isOutmostBar) ? 'outside' : 'inside';
177+
}
178+
179+
if(textPosition === 'outside') {
180+
if(!isOutmostBar) textPosition = 'inside';
181+
}
182+
183+
if(textPosition === 'inside') {
184+
if(barIsTooSmall) return;
185+
}
186+
187+
188+
// get text font
189+
var textFont;
190+
191+
if(textPosition === 'outside') {
192+
var traceOutsideTextFont = trace.outsidetextfont;
193+
if(Array.isArray(traceOutsideTextFont)) {
194+
if(i >= traceOutsideTextFont.length) return;
195+
textFont = traceOutsideTextFont[i];
196+
}
197+
else {
198+
textFont = traceOutsideTextFont;
199+
}
200+
}
201+
else {
202+
var traceInsideTextFont = trace.insidetextfont;
203+
if(Array.isArray(traceInsideTextFont)) {
204+
if(i >= traceInsideTextFont.length) return;
205+
textFont = traceInsideTextFont[i];
206+
}
207+
else {
208+
textFont = traceInsideTextFont;
209+
}
210+
}
211+
212+
if(!textFont) {
213+
var traceTextFont = trace.textfont;
214+
if(Array.isArray(traceTextFont)) {
215+
if(i >= traceTextFont.length) return;
216+
textFont = traceTextFont[i];
217+
}
218+
else {
219+
textFont = traceTextFont;
220+
}
221+
}
222+
223+
if(!textFont) {
224+
textFont = gd._fullLayout.font;
225+
}
226+
227+
// append bar text
228+
var textSelection = bar.append('text')
229+
// prohibit tex interpretation until we can handle
230+
// tex and regular text together
231+
.attr('data-notex', 1)
232+
.text(text)
233+
.attr({
234+
'class': 'bartext',
235+
transform: '',
236+
'data-bb': '',
237+
'text-anchor': 'middle',
238+
x: 0,
239+
y: 0
240+
})
241+
.call(Drawing.font, textFont);
242+
243+
textSelection.call(svgTextUtils.convertToTspans);
244+
textSelection.selectAll('tspan.line').attr({x: 0, y: 0});
245+
246+
// position bar text
247+
var textBB = Drawing.bBox(textSelection.node()),
248+
textWidth = textBB.width,
249+
textHeight = textBB.height;
250+
if(!textWidth || !textHeight) {
251+
textSelection.remove();
252+
return;
253+
}
254+
255+
// compute translate transform
256+
var transform;
257+
if(textPosition === 'outside') {
258+
transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB,
259+
trace.orientation);
260+
}
261+
else {
262+
transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB);
263+
}
264+
265+
textSelection.attr('transform', transform);
266+
}
267+
268+
function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) {
269+
// compute text and target positions
270+
var barWidth = Math.abs(x1 - x0),
271+
barHeight = Math.abs(y1 - y0),
272+
textWidth = textBB.width,
273+
textHeight = textBB.height,
274+
barX = (x0 + x1) / 2,
275+
barY = (y0 + y1) / 2,
276+
textX = (textBB.left + textBB.right) / 2,
277+
textY = (textBB.top + textBB.bottom) / 2;
278+
279+
// apply target padding
280+
var targetWidth = 0.95 * barWidth,
281+
targetHeight = 0.95 * barHeight;
282+
283+
return getTransform(
284+
textX, textY, textWidth, textHeight,
285+
barX, barY, targetWidth, targetHeight);
286+
}
287+
288+
function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) {
289+
290+
// compute text and target positions
291+
var textWidth = textBB.width,
292+
textHeight = textBB.height,
293+
textX = (textBB.left + textBB.right) / 2,
294+
textY = (textBB.top + textBB.bottom) / 2;
295+
296+
var targetWidth, targetHeight,
297+
targetX, targetY;
298+
if(orientation === 'h') {
299+
if(x1 < x0) {
300+
// bar end is on the left hand side
301+
targetWidth = 2 + textWidth; // padding included
302+
targetHeight = Math.abs(y1 - y0);
303+
targetX = x1 - targetWidth / 2;
304+
targetY = (y0 + y1) / 2;
305+
}
306+
else {
307+
targetWidth = 2 + textWidth; // padding included
308+
targetHeight = Math.abs(y1 - y0);
309+
targetX = x1 + targetWidth / 2;
310+
targetY = (y0 + y1) / 2;
311+
}
312+
}
313+
else {
314+
if(y1 > y0) {
315+
// bar end is on the bottom
316+
targetWidth = Math.abs(x1 - x0);
317+
targetHeight = 2 + textHeight; // padding included
318+
targetX = (x0 + x1) / 2;
319+
targetY = y1 + targetHeight / 2;
320+
}
321+
else {
322+
targetWidth = Math.abs(x1 - x0);
323+
targetHeight = 2 + textHeight; // padding included
324+
targetX = (x0 + x1) / 2;
325+
targetY = y1 - targetHeight / 2;
326+
}
327+
}
328+
329+
return getTransform(
330+
textX, textY, textWidth, textHeight,
331+
targetX, targetY, targetWidth, targetHeight);
332+
}
333+
334+
/**
335+
* Compute SVG transform to move a text box into a target box
336+
*
337+
* @param {number} textX X pixel coord of the text box center
338+
* @param {number} textY Y pixel coord of the text box center
339+
* @param {number} textWidth text box width
340+
* @param {number} textHeight text box height
341+
* @param {number} targetX X pixel coord of the target box center
342+
* @param {number} targetY Y pixel coord of the target box center
343+
* @param {number} targetWidth target box width
344+
* @param {number} targetHeight target box height
345+
*
346+
* @returns {string} SVG transform
347+
*/
348+
function getTransform(
349+
textX, textY, textWidth, textHeight,
350+
targetX, targetY, targetWidth, targetHeight) {
351+
352+
// compute translate transform
353+
var translateX = targetX - textX,
354+
translateY = targetY - textY,
355+
translate = 'translate(' + translateX + ' ' + translateY + ')';
356+
357+
// if bar text doesn't fit, compute rotate and scale transforms
358+
var doesntFit = (textWidth > targetWidth || textHeight > targetHeight),
359+
rotate, scale, scaleX, scaleY;
360+
361+
if(doesntFit) {
362+
var textIsHorizontal = (textWidth > textHeight),
363+
targetIsHorizontal = (targetWidth > targetHeight);
364+
if(textIsHorizontal !== targetIsHorizontal) {
365+
rotate = 'rotate(-90 ' + textX + ' ' + textY + ')';
366+
scaleX = targetWidth / textHeight;
367+
scaleY = targetHeight / textWidth;
368+
}
369+
else {
370+
scaleX = targetWidth / textWidth;
371+
scaleY = targetHeight / textHeight;
372+
}
373+
374+
if(scaleX > 1) scaleX = 1;
375+
if(scaleY > 1) scaleY = 1;
376+
377+
if(scaleX !== 1 || scaleY !== 1) {
378+
scale = 'scale(' + scaleX + ' ' + scaleY + ')';
379+
}
380+
}
381+
382+
// compute transform
383+
var transform = translate;
384+
if(scale) transform += ' ' + scale;
385+
if(rotate) transform += ' ' + rotate;
386+
387+
return transform;
388+
}

src/traces/bar/set_positions.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ function setGroupPositionsInStackOrRelativeMode(gd, pa, sa, calcTraces) {
179179

180180
// set bar bases and sizes, and update size axis
181181
stackBars(gd, sa, sieve);
182+
183+
// flag the outmost bar (for text display purposes)
184+
for(var i = 0; i < calcTraces.length; i++) {
185+
var calcTrace = calcTraces[i];
186+
187+
for(var j = 0; j < calcTrace.length; j++) {
188+
var bar = calcTrace[j];
189+
190+
if(!isNumeric(bar.s)) continue;
191+
192+
var isOutmostBar = ((bar.b + bar.s) === sieve.get(bar.p, bar.s));
193+
if(isOutmostBar) bar._outmost = true;
194+
}
195+
}
196+
197+
// Note that marking the outmost bars has to be done
198+
// before `normalizeBars` changes `bar.b` and `bar.s`.
199+
if(barnorm) normalizeBars(gd, sa, sieve);
182200
}
183201

184202

@@ -506,12 +524,7 @@ function stackBars(gd, sa, sieve) {
506524
}
507525

508526
// if barnorm is set, let normalizeBars update the axis range
509-
if(barnorm) {
510-
normalizeBars(gd, sa, sieve);
511-
}
512-
else {
513-
Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true});
514-
}
527+
if(!barnorm) Axes.expand(sa, [sMin, sMax], {tozero: true, padded: true});
515528
}
516529

517530

0 commit comments

Comments
 (0)