Skip to content

Commit ba893ea

Browse files
make generic X Axis label reduced if overlapping
If the label is not overlapping, don't reduce. If there is overlap, reduce and add an ellipsis. If there is too much overlap, don't display anything. Signed-off-by: Matthew Khouzam <[email protected]>
1 parent ff944c7 commit ba893ea

File tree

3 files changed

+179
-37
lines changed

3 files changed

+179
-37
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/***************************************************************************************
2+
* Copyright (c) 2024 BlackBerry Limited and contributors.
3+
*
4+
* Licensed under the MIT license. See LICENSE file in the project root for details.
5+
***************************************************************************************/
6+
export declare class ItemPropertiesSignalPayload {
7+
private outputDescriptorId;
8+
private experimentUUID;
9+
private properties;
10+
constructor(props: {
11+
[key: string]: string;
12+
}, expUUID?: string, descriptorId?: string);
13+
getOutputDescriptorId(): string | undefined;
14+
getExperimentUUID(): string | undefined;
15+
getProperties(): {
16+
[key: string]: string;
17+
};
18+
}
19+
//# sourceMappingURL=item-properties-signal-payload.d.ts.map
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export interface TimeRangeString {
2+
start: string;
3+
end: string;
4+
offset?: string;
5+
}
6+
export declare const ITimeRange: import("tsp-typescript-client/lib/protocol/serialization").Normalizer<ITimeRange>;
7+
export interface ITimeRange {
8+
start: bigint;
9+
end: bigint;
10+
offset: bigint | undefined;
11+
toString(): TimeRangeString;
12+
}
13+
export declare class TimeRange implements ITimeRange {
14+
start: bigint;
15+
end: bigint;
16+
offset: bigint | undefined;
17+
/**
18+
* Constructor.
19+
* @param start Range start time
20+
* @param end Range end time
21+
* @param offset Time offset, if this is defined the start and end time should be relative to this value
22+
*/
23+
constructor(start: bigint, end: bigint, offset?: bigint);
24+
/**
25+
* Constructor.
26+
* @param timeRangeString string object returned by this.toString()
27+
*/
28+
constructor(timeRangeString: TimeRangeString);
29+
/**
30+
* Constructor.
31+
* Default TimeRange with 0 for values
32+
*/
33+
constructor();
34+
/**
35+
* Get the range start time.
36+
* If an offset is present the return value is start + offset.
37+
*/
38+
getStart(): bigint;
39+
/**
40+
* Get the range end time.
41+
* If an offset is present the return value is end + offset.
42+
*/
43+
getEnd(): bigint;
44+
/**
45+
* Get range duration
46+
*/
47+
getDuration(): bigint;
48+
/**
49+
* Return the time offset
50+
*/
51+
getOffset(): bigint | undefined;
52+
/**
53+
* Create a string object that can be JSON.stringified
54+
*/
55+
toString(): TimeRangeString;
56+
}
57+
//# sourceMappingURL=time-range.d.ts.map

local-libs/traceviewer-libs/react-components/src/components/generic-xy-output-component.tsx

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,10 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
356356

357357
// Track min start and max end from axisDomain
358358
if (axisDomain && axisDomain.type === 'range') {
359-
const start = Number(axisDomain.start);
360-
const end = Number(axisDomain.end);
361-
if (start < minStart) minStart = start;
362-
if (end > maxEnd) maxEnd = end;
359+
const currentStart = Number(axisDomain.start);
360+
const currentEnd = Number(axisDomain.end);
361+
if (currentStart < minStart) minStart = currentStart;
362+
if (currentEnd > maxEnd) maxEnd = currentEnd;
363363
}
364364

365365
// Check if all series have same label/unit/dataType
@@ -375,9 +375,6 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
375375
}
376376
});
377377

378-
// Calculate union range
379-
const unionRange = maxEnd - minStart;
380-
381378
// Default to 'time' if datatype differs across series
382379
const finalDataType = commonDataType || 'time';
383380

@@ -522,7 +519,6 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
522519
this.updateTimeline();
523520
}, 0);
524521
}
525-
526522
}
527523

528524
componentWillUnmount(): void {
@@ -820,38 +816,102 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
820816

821817
const { start, end } = this.getTimelineRange();
822818

823-
const scale = d3.scaleLinear()
824-
.domain([start, end])
825-
.range([0, chartWidth]);
819+
const scale = d3.scaleLinear().domain([start, end]).range([0, chartWidth]);
820+
821+
const axis = d3.axisBottom(scale).tickFormat(d => this.formatTimelineValue(Number(d)));
826822

827-
const axis = d3.axisBottom(scale)
828-
.tickFormat(d => this.formatTimelineValue(Number(d)));
823+
const axisGroup = svg.append('g').attr('transform', `translate(0, 0)`).call(axis);
829824

830-
const axisGroup = svg.append('g')
831-
.attr('transform', `translate(0, 0)`)
832-
.call(axis);
825+
// Handle label overlaps
826+
this.handleLabelOverlaps(axisGroup);
833827

834828
// Add minor ticks for range items
835829
this.addMinorTicks(svg, scale, chartWidth);
836830
}
837831

838-
private addMinorTicks(svg: d3.Selection<SVGSVGElement, unknown, null, undefined>, scale: d3.ScaleLinear<number, number>, chartWidth: number): void {
832+
private handleLabelOverlaps(axisGroup: d3.Selection<SVGGElement, unknown, null, undefined>): void {
833+
const labels = axisGroup.selectAll('.tick text');
834+
const labelNodes = labels.nodes() as SVGTextElement[];
835+
836+
for (let i = labelNodes.length - 1; i >= 0; i--) {
837+
const current = labelNodes[i];
838+
if (!current.textContent || current.textContent.trim() === '') continue;
839+
840+
const currentBBox = current.getBoundingClientRect();
841+
842+
let overlaps = false;
843+
let maxLabelWidth = currentBBox.width;
844+
845+
// Check overlap with adjacent labels and calculate available space
846+
for (let j = i - 1; j >= 0; j--) {
847+
const other = labelNodes[j];
848+
if (!other.textContent || other.textContent.trim() === '') continue;
849+
850+
const otherBBox = other.getBoundingClientRect();
851+
852+
// Calculate available space between labels (with 5px margin)
853+
const availableSpace = Math.abs(currentBBox.x - (otherBBox.x + otherBBox.width)) - 5;
854+
maxLabelWidth = Math.min(maxLabelWidth, availableSpace);
855+
856+
// Check if bounding boxes overlap (with 5px margin)
857+
if (
858+
currentBBox.x < otherBBox.x + otherBBox.width + 5 &&
859+
currentBBox.x + currentBBox.width + 5 > otherBBox.x
860+
) {
861+
overlaps = true;
862+
}
863+
}
864+
865+
if (overlaps && maxLabelWidth > 0) {
866+
const originalText = current.textContent || '';
867+
const truncated = this.truncateWithEllipsis(originalText, maxLabelWidth);
868+
869+
if (truncated.length <= 3) {
870+
current.style.display = 'none';
871+
} else {
872+
current.textContent = truncated;
873+
}
874+
}
875+
}
876+
}
877+
878+
private truncateWithEllipsis(text: string, maxWidth: number = 80): string {
879+
const canvas = document.createElement('canvas');
880+
const ctx = canvas.getContext('2d')!;
881+
ctx.font = '12px sans-serif';
882+
883+
if (ctx.measureText(text).width <= maxWidth) return text;
884+
885+
const ellipsis = '…';
886+
let truncated = text;
887+
while (truncated.length > 0 && ctx.measureText(truncated + ellipsis).width > maxWidth) {
888+
truncated = truncated.slice(0, -1);
889+
}
890+
891+
return truncated + ellipsis;
892+
}
893+
894+
private addMinorTicks(
895+
svg: d3.Selection<SVGSVGElement, unknown, null, undefined>,
896+
scale: d3.ScaleLinear<number, number>,
897+
chartWidth: number
898+
): void {
839899
const labels = this.state.xyData.labels;
840900
if (!labels.length) return;
841901

842902
// For each range item, add minor ticks at start and end
843-
labels.forEach((label) => {
903+
labels.forEach(label => {
844904
if (label.includes('[') && label.includes(',')) {
845905
// Parse range format: "[start unit, end unit]"
846906
const match = label.match(/\[(.*?),\s*(.*?)\]/);
847907
if (match) {
848908
const startVal = parseFloat(match[1]);
849909
const endVal = parseFloat(match[2]);
850-
910+
851911
if (!isNaN(startVal) && !isNaN(endVal)) {
852912
const startX = scale(startVal);
853913
const endX = scale(endVal);
854-
914+
855915
// Add minor ticks
856916
if (startX >= 0 && startX <= chartWidth) {
857917
svg.append('line')
@@ -862,7 +922,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
862922
.attr('stroke', '#666')
863923
.attr('stroke-width', 0.5);
864924
}
865-
925+
866926
if (endX >= 0 && endX <= chartWidth) {
867927
svg.append('line')
868928
.attr('x1', endX)
@@ -880,12 +940,12 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
880940

881941
private getTimelineRange(): { start: number; end: number } {
882942
const labels = this.state.xyData.labels;
883-
943+
884944
// If we have range labels, extract the actual range from the data
885945
if (labels.length > 0 && labels[0].includes('[') && labels[0].includes(',')) {
886946
let minStart = Number.MAX_SAFE_INTEGER;
887947
let maxEnd = Number.MIN_SAFE_INTEGER;
888-
948+
889949
labels.forEach(label => {
890950
const match = label.match(/\[(.*?),\s*(.*?)\]/);
891951
if (match) {
@@ -897,12 +957,12 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
897957
}
898958
}
899959
});
900-
960+
901961
if (minStart !== Number.MAX_SAFE_INTEGER && maxEnd !== Number.MIN_SAFE_INTEGER) {
902962
return { start: minStart, end: maxEnd };
903963
}
904964
}
905-
965+
906966
// Fallback to view/range based on timeline unit type
907967
switch (this.state.timelineUnitType) {
908968
case 'time':
@@ -1016,7 +1076,11 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
10161076
onContextMenu={e => e.preventDefault()}
10171077
onMouseLeave={e => this.onMouseLeave(e)}
10181078
onMouseDown={e => this.onMouseDown(e)}
1019-
style={{ height: parseInt(String(this.props.style.height)) - this.timelineHeight, position: 'relative', cursor: this.state.cursor }}
1079+
style={{
1080+
height: parseInt(String(this.props.style.height)) - this.timelineHeight,
1081+
position: 'relative',
1082+
cursor: this.state.cursor
1083+
}}
10201084
ref={this.divRef}
10211085
>
10221086
{this.chooseReactChart()}
@@ -1031,17 +1095,19 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
10311095
<div style={{ position: 'relative' }}>
10321096
{this.renderTimeline()}
10331097
{this.state.xAxisTitle && (
1034-
<div style={{
1035-
position: 'absolute',
1036-
top: '50%',
1037-
left: '50%',
1038-
transform: 'translate(-50%, -50%)',
1039-
textAlign: 'center',
1040-
fontSize: '12px',
1041-
color: 'rgba(102, 102, 102, 0.5)',
1042-
pointerEvents: 'none',
1043-
zIndex: 10
1044-
}}>
1098+
<div
1099+
style={{
1100+
position: 'absolute',
1101+
top: '50%',
1102+
left: '50%',
1103+
transform: 'translate(-50%, -50%)',
1104+
textAlign: 'center',
1105+
fontSize: '12px',
1106+
color: 'rgba(102, 102, 102, 0.5)',
1107+
pointerEvents: 'none',
1108+
zIndex: 10
1109+
}}
1110+
>
10451111
{this.state.xAxisTitle}
10461112
</div>
10471113
)}

0 commit comments

Comments
 (0)