Skip to content

Commit 393ef2e

Browse files
committed
Fill in the margin completely with same-widths boxes
1 parent 15e0b2b commit 393ef2e

File tree

3 files changed

+208
-184
lines changed

3 files changed

+208
-184
lines changed

src/components/stack-chart/Canvas.js

Lines changed: 182 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
108108
// When the user checks the "use same widths for each stack" checkbox, some
109109
// expensive computation happens when the canvas is drawn. These computations
110110
// can be reused for hit testing, and therefore are saved in these variables.
111-
_sameWidthsIndexAtStart: null | number;
111+
_sameWidthsIndexAtViewportStart: null | number;
112112
_sameWidthsRangeLength: null | number;
113113

114114
componentDidUpdate(prevProps) {
@@ -254,20 +254,30 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
254254

255255
// Compute the start index as well as the length for the "same width"
256256
// drawing as well, if needed.
257-
this._sameWidthsRangeLength = this._sameWidthsIndexAtStart = null;
257+
this._sameWidthsRangeLength = this._sameWidthsIndexAtViewportStart = null;
258+
let sameWidthsIndexAtCanvasStart = null;
259+
let sameWidthsIndexAtCanvasEnd = null;
258260
if (useStackChartSameWidths) {
259-
const sameWidthsIndexAtStart = Math.max(
261+
const sameWidthsIndexAtViewportStart = Math.max(
260262
0,
261263
bisectionRight(sameWidthsIndexToTimestampMap, timeAtViewportStart) - 1
262264
);
263-
const sameWidthsIndexAtEnd = Math.min(
265+
const sameWidthsIndexAtViewportEnd = Math.min(
264266
sameWidthsIndexToTimestampMap.length - 1,
265267
bisectionLeft(sameWidthsIndexToTimestampMap, timeAtViewportEnd)
266268
);
267269

268-
this._sameWidthsIndexAtStart = sameWidthsIndexAtStart;
270+
this._sameWidthsIndexAtViewportStart = sameWidthsIndexAtViewportStart;
269271
this._sameWidthsRangeLength =
270-
sameWidthsIndexAtEnd - sameWidthsIndexAtStart;
272+
sameWidthsIndexAtViewportEnd - sameWidthsIndexAtViewportStart;
273+
274+
sameWidthsIndexAtCanvasStart =
275+
sameWidthsIndexAtViewportStart -
276+
(marginLeft / innerContainerWidth) * this._sameWidthsRangeLength;
277+
sameWidthsIndexAtCanvasEnd =
278+
sameWidthsIndexAtViewportEnd +
279+
(TIMELINE_MARGIN_RIGHT / innerContainerWidth) *
280+
this._sameWidthsRangeLength;
271281
}
272282

273283
const pixelAtViewportPosition = (
@@ -307,161 +317,175 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
307317
let lastDrawnPixelX = 0;
308318
for (let i = 0; i < stackTiming.length; i++) {
309319
// Only draw boxes that overlap with the canvas.
320+
const isTimingBoxBeforeCanvas =
321+
useStackChartSameWidths &&
322+
stackTiming.sameWidthsEnd &&
323+
sameWidthsIndexAtCanvasStart !== null
324+
? stackTiming.sameWidthsEnd[i] < sameWidthsIndexAtCanvasStart
325+
: stackTiming.end[i] < timeAtStart;
326+
if (isTimingBoxBeforeCanvas) {
327+
continue;
328+
}
329+
330+
const isTimingBoxAfterCanvas =
331+
useStackChartSameWidths &&
332+
stackTiming.sameWidthsStart &&
333+
sameWidthsIndexAtCanvasEnd !== null
334+
? stackTiming.sameWidthsStart[i] > sameWidthsIndexAtCanvasEnd
335+
: stackTiming.start[i] > timeAtEnd;
336+
if (isTimingBoxAfterCanvas) {
337+
break;
338+
}
339+
340+
// Draw a box, but increase the size by a small portion in order to draw
341+
// a single pixel at the end with a slight opacity.
342+
//
343+
// Legend:
344+
// |======| A stack frame's timing.
345+
// |O| A single fully opaque pixel.
346+
// |.| A slightly transparent pixel.
347+
// | | A fully transparent pixel.
348+
//
349+
// Drawing strategy:
350+
//
351+
// Frame timing |=====||========| |=====| |=| |=|=|=|=|
352+
// Device Pixels |O|O|.|O|O|O|O|.| | |O|O|O|.| | |O|.| | |O|.|O|.|
353+
// CSS Pixels | | | | | | | | | | | | |
354+
355+
// First compute the left and right sides of the box.
356+
let floatX: DevicePixels;
357+
let floatW: DevicePixels;
310358
if (
311-
stackTiming.end[i] > timeAtStart &&
312-
stackTiming.start[i] < timeAtEnd
359+
useStackChartSameWidths &&
360+
stackTiming.sameWidthsStart &&
361+
stackTiming.sameWidthsEnd &&
362+
this._sameWidthsRangeLength !== null &&
363+
this._sameWidthsIndexAtViewportStart !== null
313364
) {
314-
// Draw a box, but increase the size by a small portion in order to draw
315-
// a single pixel at the end with a slight opacity.
316-
//
317-
// Legend:
318-
// |======| A stack frame's timing.
319-
// |O| A single fully opaque pixel.
320-
// |.| A slightly transparent pixel.
321-
// | | A fully transparent pixel.
322-
//
323-
// Drawing strategy:
324-
//
325-
// Frame timing |=====||========| |=====| |=| |=|=|=|=|
326-
// Device Pixels |O|O|.|O|O|O|O|.| | |O|O|O|.| | |O|.| | |O|.|O|.|
327-
// CSS Pixels | | | | | | | | | | | | |
328-
329-
// First compute the left and right sides of the box.
330-
let floatX: DevicePixels;
331-
let floatW: DevicePixels;
332-
if (
333-
useStackChartSameWidths &&
334-
stackTiming.sameWidthsStart &&
335-
stackTiming.sameWidthsEnd &&
336-
this._sameWidthsRangeLength !== null &&
337-
this._sameWidthsIndexAtStart !== null
338-
) {
339-
floatX =
340-
cssToDeviceScale *
341-
(marginLeft +
342-
(innerContainerWidth *
343-
(stackTiming.sameWidthsStart[i] -
344-
this._sameWidthsIndexAtStart)) /
345-
this._sameWidthsRangeLength);
346-
floatW =
347-
(innerDevicePixelsWidth *
348-
(stackTiming.sameWidthsEnd[i] -
349-
stackTiming.sameWidthsStart[i])) /
350-
this._sameWidthsRangeLength -
351-
1;
352-
} else {
353-
const viewportAtStartTime: UnitIntervalOfProfileRange =
354-
(stackTiming.start[i] - rangeStart) / rangeLength;
355-
const viewportAtEndTime: UnitIntervalOfProfileRange =
356-
(stackTiming.end[i] - rangeStart) / rangeLength;
357-
floatX = pixelAtViewportPosition(viewportAtStartTime);
358-
floatW =
359-
((viewportAtEndTime - viewportAtStartTime) *
360-
innerDevicePixelsWidth) /
361-
viewportLength -
362-
1;
363-
}
364-
365-
// Determine if there is enough pixel space to draw this box, and snap the
366-
// box to the pixels.
367-
let snappedFloatX = floatX;
368-
let snappedFloatW = floatW;
369-
let skipDraw = true;
370-
if (floatX >= lastDrawnPixelX) {
371-
// The x value is past the last lastDrawnPixelX, so it can be drawn.
372-
skipDraw = false;
373-
} else if (floatX + floatW > lastDrawnPixelX) {
374-
// The left side of the box is before the lastDrawnPixelX value, but the
375-
// right hand side is within a range to be drawn. Truncate the box a little
376-
// bit in order to draw it to the screen in the free space.
377-
snappedFloatW = floatW - (lastDrawnPixelX - floatX);
378-
snappedFloatX = lastDrawnPixelX;
379-
skipDraw = false;
380-
}
365+
floatX =
366+
cssToDeviceScale *
367+
(marginLeft +
368+
(innerContainerWidth *
369+
(stackTiming.sameWidthsStart[i] -
370+
this._sameWidthsIndexAtViewportStart)) /
371+
this._sameWidthsRangeLength);
372+
floatW =
373+
(innerDevicePixelsWidth *
374+
(stackTiming.sameWidthsEnd[i] - stackTiming.sameWidthsStart[i])) /
375+
this._sameWidthsRangeLength -
376+
1;
377+
} else {
378+
const viewportAtStartTime: UnitIntervalOfProfileRange =
379+
(stackTiming.start[i] - rangeStart) / rangeLength;
380+
const viewportAtEndTime: UnitIntervalOfProfileRange =
381+
(stackTiming.end[i] - rangeStart) / rangeLength;
382+
floatX = pixelAtViewportPosition(viewportAtStartTime);
383+
floatW =
384+
((viewportAtEndTime - viewportAtStartTime) *
385+
innerDevicePixelsWidth) /
386+
viewportLength -
387+
1;
388+
}
381389

382-
if (skipDraw) {
383-
// This box didn't satisfy the constraints in the above if checks, so skip it.
384-
continue;
385-
}
390+
// Determine if there is enough pixel space to draw this box, and snap the
391+
// box to the pixels.
392+
let snappedFloatX = floatX;
393+
let snappedFloatW = floatW;
394+
let skipDraw = true;
395+
if (floatX >= lastDrawnPixelX) {
396+
// The x value is past the last lastDrawnPixelX, so it can be drawn.
397+
skipDraw = false;
398+
} else if (floatX + floatW > lastDrawnPixelX) {
399+
// The left side of the box is before the lastDrawnPixelX value, but the
400+
// right hand side is within a range to be drawn. Truncate the box a little
401+
// bit in order to draw it to the screen in the free space.
402+
snappedFloatW = floatW - (lastDrawnPixelX - floatX);
403+
snappedFloatX = lastDrawnPixelX;
404+
skipDraw = false;
405+
}
386406

387-
// Convert or compute all of the integer values for drawing the box.
388-
// Note, this should all be Math.round instead of floor and ceil, but some
389-
// off by one errors appear to be creating gaps where there shouldn't be any.
390-
const intX = Math.floor(snappedFloatX);
391-
const intY = Math.round(
392-
depth * rowDevicePixelsHeight - viewportDevicePixelsTop
393-
);
394-
const intW = Math.ceil(Math.max(1, snappedFloatW));
395-
const intH = Math.round(
396-
rowDevicePixelsHeight - oneCssPixelInDevicePixels
397-
);
398-
399-
// Look up information about this stack frame.
400-
let text, category, isSelected;
401-
if (stackTiming.callNode) {
402-
const callNodeIndex = stackTiming.callNode[i];
403-
const funcIndex = callNodeTable.func[callNodeIndex];
404-
const funcNameIndex = thread.funcTable.name[funcIndex];
405-
text = thread.stringTable.getString(funcNameIndex);
406-
const categoryIndex = callNodeTable.category[callNodeIndex];
407-
category = categories[categoryIndex];
408-
isSelected = selectedCallNodeIndex === callNodeIndex;
409-
} else {
410-
const markerIndex = stackTiming.index[i];
411-
const markerPayload = ((getMarker(markerIndex)
412-
.data: any): UserTimingMarkerPayload);
413-
text = markerPayload.name;
414-
category = categories[categoryForUserTiming];
415-
isSelected = selectedCallNodeIndex === markerIndex;
416-
}
407+
if (skipDraw) {
408+
// This box didn't satisfy the constraints in the above if checks, so skip it.
409+
continue;
410+
}
417411

418-
const isHovered =
419-
hoveredItem &&
420-
depth === hoveredItem.depth &&
421-
i === hoveredItem.stackTimingIndex;
422-
423-
const colorStyles = mapCategoryColorNameToStackChartStyles(
424-
category.color
425-
);
426-
// Draw the box.
427-
fastFillStyle.set(
428-
isHovered || isSelected
429-
? colorStyles.selectedFillStyle
430-
: colorStyles.unselectedFillStyle
431-
);
432-
ctx.fillRect(
433-
intX,
434-
intY,
435-
// Add on a bit of BORDER_OPACITY to the end of the width, to draw a partial
436-
// pixel. This will effectively draw a transparent version of the fill color
437-
// without having to change the fill color. At the time of this writing it
438-
// was the same performance cost as only providing integer values here.
439-
intW + BORDER_OPACITY,
440-
intH
441-
);
442-
lastDrawnPixelX =
443-
intX +
444-
intW +
445-
// The border on the right is 1 device pixel wide.
446-
1;
412+
// Convert or compute all of the integer values for drawing the box.
413+
// Note, this should all be Math.round instead of floor and ceil, but some
414+
// off by one errors appear to be creating gaps where there shouldn't be any.
415+
const intX = Math.floor(snappedFloatX);
416+
const intY = Math.round(
417+
depth * rowDevicePixelsHeight - viewportDevicePixelsTop
418+
);
419+
const intW = Math.ceil(Math.max(1, snappedFloatW));
420+
const intH = Math.round(
421+
rowDevicePixelsHeight - oneCssPixelInDevicePixels
422+
);
423+
424+
// Look up information about this stack frame.
425+
let text, category, isSelected;
426+
if (stackTiming.callNode) {
427+
const callNodeIndex = stackTiming.callNode[i];
428+
const funcIndex = callNodeTable.func[callNodeIndex];
429+
const funcNameIndex = thread.funcTable.name[funcIndex];
430+
text = thread.stringTable.getString(funcNameIndex);
431+
const categoryIndex = callNodeTable.category[callNodeIndex];
432+
category = categories[categoryIndex];
433+
isSelected = selectedCallNodeIndex === callNodeIndex;
434+
} else {
435+
const markerIndex = stackTiming.index[i];
436+
const markerPayload = ((getMarker(markerIndex)
437+
.data: any): UserTimingMarkerPayload);
438+
text = markerPayload.name;
439+
category = categories[categoryForUserTiming];
440+
isSelected = selectedCallNodeIndex === markerIndex;
441+
}
447442

448-
// Draw the text label if it fits. Use the original float values here so that
449-
// the text doesn't snap around when moving. Only the boxes should snap.
450-
const textX: DevicePixels =
451-
// Constrain the x coordinate to the leftmost area.
452-
Math.max(floatX, 0) + textDevicePixelsOffsetStart;
453-
const textW: DevicePixels = Math.max(0, floatW - (textX - floatX));
454-
455-
if (textW > textMeasurement.minWidth) {
456-
const fittedText = textMeasurement.getFittedText(text, textW);
457-
if (fittedText) {
458-
fastFillStyle.set(
459-
isHovered || isSelected
460-
? colorStyles.selectedTextColor
461-
: '#000000'
462-
);
463-
ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop);
464-
}
443+
const isHovered =
444+
hoveredItem &&
445+
depth === hoveredItem.depth &&
446+
i === hoveredItem.stackTimingIndex;
447+
448+
const colorStyles = mapCategoryColorNameToStackChartStyles(
449+
category.color
450+
);
451+
// Draw the box.
452+
fastFillStyle.set(
453+
isHovered || isSelected
454+
? colorStyles.selectedFillStyle
455+
: colorStyles.unselectedFillStyle
456+
);
457+
ctx.fillRect(
458+
intX,
459+
intY,
460+
// Add on a bit of BORDER_OPACITY to the end of the width, to draw a partial
461+
// pixel. This will effectively draw a transparent version of the fill color
462+
// without having to change the fill color. At the time of this writing it
463+
// was the same performance cost as only providing integer values here.
464+
intW + BORDER_OPACITY,
465+
intH
466+
);
467+
lastDrawnPixelX =
468+
intX +
469+
intW +
470+
// The border on the right is 1 device pixel wide.
471+
1;
472+
473+
// Draw the text label if it fits. Use the original float values here so that
474+
// the text doesn't snap around when moving. Only the boxes should snap.
475+
const textX: DevicePixels =
476+
// Constrain the x coordinate to the leftmost area.
477+
Math.max(floatX, 0) + textDevicePixelsOffsetStart;
478+
const textW: DevicePixels = Math.max(0, floatW - (textX - floatX));
479+
480+
if (textW > textMeasurement.minWidth) {
481+
const fittedText = textMeasurement.getFittedText(text, textW);
482+
if (fittedText) {
483+
fastFillStyle.set(
484+
isHovered || isSelected
485+
? colorStyles.selectedTextColor
486+
: '#000000'
487+
);
488+
ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop);
465489
}
466490
}
467491
}
@@ -666,10 +690,10 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
666690

667691
if (
668692
this._sameWidthsRangeLength === null ||
669-
this._sameWidthsIndexAtStart === null
693+
this._sameWidthsIndexAtViewportStart === null
670694
) {
671695
console.warn(
672-
'The local variables sameWidthsRangeLength or samewidthsIndexAtStart are null when they should be present.'
696+
'The local variables sameWidthsRangeLength or sameWidthsIndexAtViewportStart are null when they should be present.'
673697
);
674698
return null;
675699
}
@@ -680,7 +704,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
680704
const xMinusMargin = x - marginLeft;
681705
const hoveredBox =
682706
(xMinusMargin / innerContainerWidth) * this._sameWidthsRangeLength +
683-
this._sameWidthsIndexAtStart;
707+
this._sameWidthsIndexAtViewportStart;
684708

685709
for (let i = 0; i < stackTiming.length; i++) {
686710
const start = stackTiming.sameWidthsStart[i];

0 commit comments

Comments
 (0)