Skip to content

Commit ff944c7

Browse files
update generic Xaxis for updated tsp
Signed-off-by: Matthew Khouzam <matthew.khouzam@ericsson.com>
1 parent 0f6a98f commit ff944c7

File tree

1 file changed

+207
-23
lines changed

1 file changed

+207
-23
lines changed

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

Lines changed: 207 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ interface GenericXYState extends AbstractTreeOutputState {
7171
cursor: string;
7272
timelineUnit: string;
7373
timelineUnitType: TimelineUnitType;
74+
xAxisTitle?: string;
75+
range: TimeRange;
7476
}
7577

76-
type TimelineUnitType = 'time' | 'cycles' | 'bytes' | 'calls' | 'bytes/sec' | 'iterations/sec';
78+
type TimelineUnitType = 'time' | 'DURATION' | 'cycles' | 'bytes' | 'calls' | 'bytes/sec' | 'iterations/sec';
7779

7880
interface GenericXYProps extends AbstractOutputProps {
7981
formatX?: (x: number | bigint | string) => string;
@@ -143,9 +145,10 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
143145
allMax: 0,
144146
allMin: 0,
145147
cursor: 'default',
146-
showTree: true,
147148
timelineUnit: this.props.timelineUnit || 'ms',
148-
timelineUnitType: this.props.timelineUnitType || 'time'
149+
timelineUnitType: this.props.timelineUnitType || 'DURATION',
150+
showTree: true,
151+
range: new TimeRange()
149152
};
150153

151154
this.addPinViewOptions(() => ({
@@ -202,7 +205,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
202205
}
203206

204207
renderYAxis(): React.ReactNode {
205-
const chartHeight = parseInt(String(this.props.style.height), 10);
208+
const chartHeight = parseInt(String(this.props.style.height), 10) - this.timelineHeight;
206209
let yMin = this.state.allMin;
207210
let yMax = this.state.allMax;
208211
if (!Number.isFinite(yMin) || !Number.isFinite(yMax)) {
@@ -326,6 +329,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
326329
})
327330
);
328331
this.calculateYRange();
332+
329333
this.updateTimeline();
330334
return;
331335
}
@@ -338,6 +342,61 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
338342
const xy = this.buildXYData(series, this.mode);
339343
flushSync(() => this.setState({ xyData: xy, outputStatus: model.status ?? ResponseStatus.COMPLETED }));
340344
this.calculateYRange();
345+
346+
// Extract model types from all series
347+
let minStart = Number.MAX_SAFE_INTEGER;
348+
let maxEnd = Number.MIN_SAFE_INTEGER;
349+
let commonLabel: string | undefined;
350+
let commonUnit: string | undefined;
351+
let commonDataType: string | undefined;
352+
353+
series.forEach((s, index) => {
354+
if (s.xValuesDescription) {
355+
const { axisDomain, label, unit, dataType } = s.xValuesDescription;
356+
357+
// Track min start and max end from axisDomain
358+
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;
363+
}
364+
365+
// Check if all series have same label/unit/dataType
366+
if (index === 0) {
367+
commonLabel = label;
368+
commonUnit = unit;
369+
commonDataType = dataType;
370+
} else {
371+
if (commonLabel !== label) commonLabel = undefined;
372+
if (commonUnit !== unit) commonUnit = undefined;
373+
if (commonDataType !== dataType) commonDataType = undefined;
374+
}
375+
}
376+
});
377+
378+
// Calculate union range
379+
const unionRange = maxEnd - minStart;
380+
381+
// Default to 'time' if datatype differs across series
382+
const finalDataType = commonDataType || 'time';
383+
384+
// Update state with extracted info
385+
const updates: Partial<GenericXYState> = {};
386+
if (commonUnit && commonUnit !== this.state.timelineUnit) {
387+
updates.timelineUnit = commonUnit;
388+
}
389+
if (finalDataType !== this.state.timelineUnitType) {
390+
updates.timelineUnitType = finalDataType as TimelineUnitType;
391+
}
392+
if (commonLabel !== this.state.xAxisTitle) {
393+
updates.xAxisTitle = commonLabel;
394+
}
395+
updates.range = new TimeRange(BigInt(minStart - 1), BigInt(maxEnd - 1), BigInt(1));
396+
if (Object.keys(updates).length > 0) {
397+
this.setState(updates as any);
398+
}
399+
341400
this.updateTimeline();
342401
}
343402

@@ -456,6 +515,14 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
456515
this._debouncedUpdateXY();
457516
}
458517
}
518+
519+
// Update timeline after render if data changed
520+
if (prevState.xyData !== this.state.xyData) {
521+
setTimeout(() => {
522+
this.updateTimeline();
523+
}, 0);
524+
}
525+
459526
}
460527

461528
componentWillUnmount(): void {
@@ -529,7 +596,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
529596

530597
const chartProps = {
531598
data: data,
532-
height: parseInt(String(this.props.style.height)),
599+
height: parseInt(String(this.props.style.height)) - this.timelineHeight,
533600
options: options,
534601
ref: this.chartRef
535602
};
@@ -726,57 +793,134 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
726793
}
727794

728795
private renderTimeline(): React.ReactNode {
729-
if (this.state.timelineUnitType === 'time' && !this.isTimeAxis) return <svg/>;
730-
731796
const chartWidth = this.getChartWidth();
732-
if (chartWidth <= 0) return <svg/>;
733797

734-
return (
735-
<svg
798+
const svgElement = (
799+
<svg
736800
ref={this.timelineRef}
737-
width={chartWidth}
801+
width={Math.max(chartWidth, 1)}
738802
height={this.timelineHeight}
739-
style={{ marginLeft: this.margin.left }}
803+
style={{
804+
position: 'relative'
805+
}}
740806
/>
741807
);
808+
809+
return svgElement;
742810
}
743811

744812
private updateTimeline(): void {
745813
if (!this.timelineRef.current) return;
746-
if (this.state.timelineUnitType === 'time' && !this.isTimeAxis) return;
747814

748815
const svg = d3.select(this.timelineRef.current);
749816
svg.selectAll('*').remove();
750817

751818
const chartWidth = this.getChartWidth();
819+
if (chartWidth <= 0) return;
820+
752821
const { start, end } = this.getTimelineRange();
753-
822+
754823
const scale = d3.scaleLinear()
755824
.domain([start, end])
756825
.range([0, chartWidth]);
757826

758827
const axis = d3.axisBottom(scale)
759828
.tickFormat(d => this.formatTimelineValue(Number(d)));
760829

761-
svg.append('g')
830+
const axisGroup = svg.append('g')
762831
.attr('transform', `translate(0, 0)`)
763832
.call(axis);
833+
834+
// Add minor ticks for range items
835+
this.addMinorTicks(svg, scale, chartWidth);
836+
}
837+
838+
private addMinorTicks(svg: d3.Selection<SVGSVGElement, unknown, null, undefined>, scale: d3.ScaleLinear<number, number>, chartWidth: number): void {
839+
const labels = this.state.xyData.labels;
840+
if (!labels.length) return;
841+
842+
// For each range item, add minor ticks at start and end
843+
labels.forEach((label) => {
844+
if (label.includes('[') && label.includes(',')) {
845+
// Parse range format: "[start unit, end unit]"
846+
const match = label.match(/\[(.*?),\s*(.*?)\]/);
847+
if (match) {
848+
const startVal = parseFloat(match[1]);
849+
const endVal = parseFloat(match[2]);
850+
851+
if (!isNaN(startVal) && !isNaN(endVal)) {
852+
const startX = scale(startVal);
853+
const endX = scale(endVal);
854+
855+
// Add minor ticks
856+
if (startX >= 0 && startX <= chartWidth) {
857+
svg.append('line')
858+
.attr('x1', startX)
859+
.attr('x2', startX)
860+
.attr('y1', 0)
861+
.attr('y2', 3)
862+
.attr('stroke', '#666')
863+
.attr('stroke-width', 0.5);
864+
}
865+
866+
if (endX >= 0 && endX <= chartWidth) {
867+
svg.append('line')
868+
.attr('x1', endX)
869+
.attr('x2', endX)
870+
.attr('y1', 0)
871+
.attr('y2', 3)
872+
.attr('stroke', '#666')
873+
.attr('stroke-width', 0.5);
874+
}
875+
}
876+
}
877+
}
878+
});
764879
}
765880

766881
private getTimelineRange(): { start: number; end: number } {
882+
const labels = this.state.xyData.labels;
883+
884+
// If we have range labels, extract the actual range from the data
885+
if (labels.length > 0 && labels[0].includes('[') && labels[0].includes(',')) {
886+
let minStart = Number.MAX_SAFE_INTEGER;
887+
let maxEnd = Number.MIN_SAFE_INTEGER;
888+
889+
labels.forEach(label => {
890+
const match = label.match(/\[(.*?),\s*(.*?)\]/);
891+
if (match) {
892+
const start = parseFloat(match[1]);
893+
const end = parseFloat(match[2]);
894+
if (!isNaN(start) && !isNaN(end)) {
895+
minStart = Math.min(minStart, start);
896+
maxEnd = Math.max(maxEnd, end);
897+
}
898+
}
899+
});
900+
901+
if (minStart !== Number.MAX_SAFE_INTEGER && maxEnd !== Number.MIN_SAFE_INTEGER) {
902+
return { start: minStart, end: maxEnd };
903+
}
904+
}
905+
906+
// Fallback to view/range based on timeline unit type
767907
switch (this.state.timelineUnitType) {
768908
case 'time':
769909
return {
770910
start: Number(this.props.viewRange.getStart()),
771911
end: Number(this.props.viewRange.getEnd())
772912
};
913+
case 'DURATION':
914+
return {
915+
start: Number(this.state.range.getStart()),
916+
end: Number(this.state.range.getEnd())
917+
};
773918
case 'cycles':
774919
case 'bytes':
775920
case 'calls':
776921
case 'bytes/sec':
777922
case 'iterations/sec':
778923
// For non-time units, use the data range
779-
const labels = this.state.xyData.labels;
780924
if (labels.length === 0) return { start: 0, end: 1 };
781925
return { start: 0, end: labels.length - 1 };
782926
default:
@@ -787,7 +931,8 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
787931
private formatTimelineValue(value: number): string {
788932
switch (this.state.timelineUnitType) {
789933
case 'time':
790-
return `${d3.format('.2f')(value)} ${this.state.timelineUnit}`;
934+
case 'DURATION':
935+
return this.formatTime(value);
791936
case 'cycles':
792937
case 'calls':
793938
return `${d3.format('.0f')(value)} ${this.state.timelineUnit}`;
@@ -802,29 +947,51 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
802947
}
803948
}
804949

950+
private formatTime(value: number): string {
951+
const units = [
952+
{ name: 'ns', factor: 1 },
953+
{ name: 'μs', factor: 1000 },
954+
{ name: 'ms', factor: 1000000 },
955+
{ name: 's', factor: 1000000000 }
956+
];
957+
958+
let unitIndex = 0;
959+
let scaledValue = value;
960+
961+
for (let i = units.length - 1; i >= 0; i--) {
962+
if (Math.abs(value) >= units[i].factor) {
963+
scaledValue = value / units[i].factor;
964+
unitIndex = i;
965+
break;
966+
}
967+
}
968+
969+
return `${d3.format('.1f')(scaledValue)} ${units[unitIndex].name}`;
970+
}
971+
805972
private formatDataRate(rate: number): string {
806973
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
807974
let value = rate;
808975
let unitIndex = 0;
809-
976+
810977
while (value >= 1024 && unitIndex < units.length - 1) {
811978
value /= 1024;
812979
unitIndex++;
813980
}
814-
981+
815982
return `${d3.format('.1f')(value)} ${units[unitIndex]}`;
816983
}
817984

818985
private formatBytes(bytes: number): string {
819986
const units = ['B', 'KB', 'MB', 'GB'];
820987
let value = bytes;
821988
let unitIndex = 0;
822-
989+
823990
while (value >= 1024 && unitIndex < units.length - 1) {
824991
value /= 1024;
825992
unitIndex++;
826993
}
827-
994+
828995
return `${d3.format('.1f')(value)} ${units[unitIndex]}`;
829996
}
830997

@@ -849,7 +1016,7 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
8491016
onContextMenu={e => e.preventDefault()}
8501017
onMouseLeave={e => this.onMouseLeave(e)}
8511018
onMouseDown={e => this.onMouseDown(e)}
852-
style={{ height: this.props.style.height, position: 'relative', cursor: this.state.cursor }}
1019+
style={{ height: parseInt(String(this.props.style.height)) - this.timelineHeight, position: 'relative', cursor: this.state.cursor }}
8531020
ref={this.divRef}
8541021
>
8551022
{this.chooseReactChart()}
@@ -861,7 +1028,24 @@ export class GenericXYOutputComponent extends AbstractTreeOutputComponent<Generi
8611028
</div>
8621029
)}
8631030
</div>
864-
{this.renderTimeline()}
1031+
<div style={{ position: 'relative' }}>
1032+
{this.renderTimeline()}
1033+
{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+
}}>
1045+
{this.state.xAxisTitle}
1046+
</div>
1047+
)}
1048+
</div>
8651049
</div>
8661050
);
8671051
}

0 commit comments

Comments
 (0)