Skip to content

Commit 5eed56e

Browse files
authored
Allow markers to provide a marker color. (#5607)
1 parent 701dcfd commit 5eed56e

File tree

5 files changed

+234
-6
lines changed

5 files changed

+234
-6
lines changed

src/components/marker-chart/Canvas.tsx

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,17 @@ import type {
3333
MarkerTiming,
3434
MarkerTimingAndBuckets,
3535
MarkerIndex,
36+
MarkerSchemaByName,
37+
GraphColor,
3638
} from 'firefox-profiler/types';
3739
import { getStartEndRangeForMarker } from 'firefox-profiler/utils';
40+
import {
41+
getStrokeColor,
42+
getFillColor,
43+
getDotColor,
44+
isValidGraphColor,
45+
} from 'firefox-profiler/profile-logic/graph-color';
46+
import { getSchemaFromMarker } from 'firefox-profiler/profile-logic/marker-schema';
3847

3948
import type {
4049
ChartCanvasScale,
@@ -59,6 +68,7 @@ type OwnProps = {
5968
readonly rowHeight: CssPixels;
6069
readonly getMarker: (param: MarkerIndex) => Marker;
6170
readonly getMarkerLabel: (param: MarkerIndex) => string;
71+
readonly markerSchemaByName: MarkerSchemaByName;
6272
readonly markerListLength: number;
6373
readonly threadsKey: ThreadsKey;
6474
readonly updatePreviewSelection: WrapFunctionInDispatch<UpdatePreviewSelection>;
@@ -82,10 +92,64 @@ const TEXT_OFFSET_START = 3;
8292
const MARKER_DOT_RADIUS = 0.25;
8393
const LABEL_PADDING = 5;
8494
const MARKER_BORDER_COLOR = '#2c77d1';
95+
const DEFAULT_FILL_COLOR = '#8ac4ff'; // Light blue for non-highlighted
8596

8697
class MarkerChartCanvasImpl extends React.PureComponent<Props> {
8798
_textMeasurement: TextMeasurement | null = null;
8899

100+
/**
101+
* Get the fill and stroke colors for a marker based on its schema and data.
102+
* If the marker schema has a colorField, use that field's value.
103+
* Fall back to default blue if no color is specified.
104+
*/
105+
_getMarkerColors(
106+
markerIndex: MarkerIndex,
107+
isHighlighted: boolean
108+
): {
109+
fillColor: string;
110+
strokeColor: string;
111+
} {
112+
const { getMarker, markerSchemaByName } = this.props;
113+
const marker = getMarker(markerIndex);
114+
115+
let color: GraphColor | null = null;
116+
117+
// Try to get color from the marker schema's colorField
118+
const schema = getSchemaFromMarker(markerSchemaByName, marker.data);
119+
120+
if (
121+
schema &&
122+
schema.colorField &&
123+
marker.data &&
124+
typeof marker.data === 'object'
125+
) {
126+
// Use type assertion to safely access dynamic property
127+
const fieldValue = (marker.data as any)[schema.colorField];
128+
// Validate that the field value is a valid GraphColor
129+
if (typeof fieldValue === 'string' && isValidGraphColor(fieldValue)) {
130+
color = fieldValue as GraphColor;
131+
}
132+
}
133+
134+
if (color) {
135+
if (isHighlighted) {
136+
return {
137+
fillColor: getStrokeColor(color),
138+
strokeColor: getDotColor(color),
139+
};
140+
}
141+
return {
142+
fillColor: getFillColor(color),
143+
strokeColor: getStrokeColor(color),
144+
};
145+
}
146+
// Fall back to default blue colors
147+
return {
148+
fillColor: isHighlighted ? BLUE_60 : DEFAULT_FILL_COLOR,
149+
strokeColor: isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR,
150+
};
151+
}
152+
89153
drawCanvas = (
90154
ctx: CanvasRenderingContext2D,
91155
scale: ChartCanvasScale,
@@ -232,7 +296,7 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
232296
isHighlighted: boolean = false
233297
) {
234298
if (isInstantMarker) {
235-
this.drawOneInstantMarker(ctx, x, y, h, isHighlighted);
299+
this.drawOneInstantMarker(ctx, x, y, h, markerIndex, isHighlighted);
236300
} else {
237301
this.drawOneIntervalMarker(ctx, x, y, w, h, markerIndex, isHighlighted);
238302
}
@@ -248,14 +312,18 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
248312
isHighlighted: boolean
249313
) {
250314
const { marginLeft, getMarkerLabel } = this.props;
315+
const { fillColor, strokeColor } = this._getMarkerColors(
316+
markerIndex,
317+
isHighlighted
318+
);
251319

252320
if (w <= 2) {
253321
// This is an interval marker small enough that if we drew it as a
254322
// rectangle, we wouldn't see any inside part. With a width of 2 pixels,
255323
// the rectangle-with-borders would only be borders. With less than 2
256324
// pixels, the borders would collapse.
257325
// So let's draw it directly as a rect.
258-
ctx.fillStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR;
326+
ctx.fillStyle = strokeColor;
259327

260328
// w is rounded in the caller, but let's make sure it's at least 1.
261329
w = Math.max(w, 1);
@@ -264,8 +332,8 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
264332
// This is a bigger interval marker.
265333
const textMeasurement = this._getTextMeasurement(ctx);
266334

267-
ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff';
268-
ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR;
335+
ctx.fillStyle = fillColor;
336+
ctx.strokeStyle = strokeColor;
269337

270338
ctx.beginPath();
271339

@@ -312,10 +380,15 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
312380
x: CssPixels,
313381
y: CssPixels,
314382
h: CssPixels,
383+
markerIndex: MarkerIndex,
315384
isHighlighted: boolean
316385
) {
317-
ctx.fillStyle = isHighlighted ? BLUE_60 : '#8ac4ff';
318-
ctx.strokeStyle = isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR;
386+
const { fillColor, strokeColor } = this._getMarkerColors(
387+
markerIndex,
388+
isHighlighted
389+
);
390+
ctx.fillStyle = fillColor;
391+
ctx.strokeStyle = strokeColor;
319392

320393
// We're drawing a diamond shape, whose height is h - 2, and width is h / 2.
321394
ctx.beginPath();

src/components/marker-chart/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { MarkerSettings } from 'firefox-profiler/components/shared/MarkerSetting
1414
import {
1515
getCommittedRange,
1616
getPreviewSelection,
17+
getMarkerSchemaByName,
1718
} from 'firefox-profiler/selectors/profile';
1819
import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread';
1920
import { getSelectedThreadsKey } from 'firefox-profiler/selectors/url-state';
@@ -28,6 +29,7 @@ import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMe
2829
import type {
2930
Marker,
3031
MarkerIndex,
32+
MarkerSchemaByName,
3133
MarkerTimingAndBuckets,
3234
UnitIntervalOfProfileRange,
3335
StartEndRange,
@@ -51,6 +53,7 @@ type DispatchProps = {
5153
type StateProps = {
5254
readonly getMarker: (param: MarkerIndex) => Marker;
5355
readonly getMarkerLabel: (param: MarkerIndex) => string;
56+
readonly markerSchemaByName: MarkerSchemaByName;
5457
readonly markerTimingAndBuckets: MarkerTimingAndBuckets;
5558
readonly maxMarkerRows: number;
5659
readonly markerListLength: number;
@@ -106,6 +109,7 @@ class MarkerChartImpl extends React.PureComponent<Props> {
106109
markerTimingAndBuckets,
107110
getMarker,
108111
getMarkerLabel,
112+
markerSchemaByName,
109113
previewSelection,
110114
updatePreviewSelection,
111115
changeMouseTimePosition,
@@ -152,6 +156,7 @@ class MarkerChartImpl extends React.PureComponent<Props> {
152156
markerTimingAndBuckets,
153157
getMarker,
154158
getMarkerLabel,
159+
markerSchemaByName,
155160
markerListLength,
156161
updatePreviewSelection,
157162
changeMouseTimePosition,
@@ -190,6 +195,7 @@ export const MarkerChart = explicitConnect<{}, StateProps, DispatchProps>({
190195
return {
191196
getMarker: selectedThreadSelectors.getMarkerGetter(state),
192197
getMarkerLabel: selectedThreadSelectors.getMarkerChartLabelGetter(state),
198+
markerSchemaByName: getMarkerSchemaByName(state),
193199
markerTimingAndBuckets,
194200
maxMarkerRows: markerTimingAndBuckets.length,
195201
markerListLength: selectedThreadSelectors.getMarkerListLength(state),

src/profile-logic/graph-color.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,22 @@ export function getDotColor(color: GraphColor) {
8484
throw new Error('Unexpected track color: ' + color);
8585
}
8686
}
87+
88+
/**
89+
* Check if a string is a valid GraphColor value.
90+
*/
91+
export function isValidGraphColor(value: string): value is GraphColor {
92+
const validColors: GraphColor[] = [
93+
'blue',
94+
'green',
95+
'grey',
96+
'ink',
97+
'magenta',
98+
'orange',
99+
'purple',
100+
'red',
101+
'teal',
102+
'yellow',
103+
];
104+
return validColors.includes(value as GraphColor);
105+
}

src/test/components/MarkerChart.test.tsx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { Provider } from 'react-redux';
66
import { BLUE_60 } from 'photon-colors';
7+
import { getFillColor } from 'firefox-profiler/profile-logic/graph-color';
78

89
// This module is mocked.
910
import copy from 'copy-to-clipboard';
@@ -53,6 +54,9 @@ import { autoMockElementSize } from '../fixtures/mocks/element-size';
5354

5455
import type { CssPixels, Profile } from 'firefox-profiler/types';
5556

57+
// Constants matching those in Canvas.tsx
58+
const DEFAULT_FILL_COLOR = '#8ac4ff'; // Light blue for non-highlighted
59+
5660
const MARKERS: TestDefinedMarker[] = [
5761
['Marker A', 0, 10],
5862
['Marker A', 0, 10],
@@ -780,4 +784,125 @@ describe('MarkerChart', function () {
780784
expect(container.querySelector('.EmptyReasons')).toMatchSnapshot();
781785
});
782786
});
787+
788+
describe('Colored markers', () => {
789+
function setupColoredMarkerTest(markers: TestDefinedMarker[]) {
790+
const profile = getProfileWithMarkers(markers);
791+
792+
// Add marker schema with colorField to the profile
793+
profile.meta.markerSchema = [
794+
{
795+
name: 'Test',
796+
display: ['marker-chart', 'marker-table'],
797+
fields: [
798+
{ key: 'status', label: 'Status', format: 'string' },
799+
{
800+
key: 'statusColor',
801+
label: 'Color',
802+
format: 'string',
803+
hidden: true,
804+
},
805+
],
806+
colorField: 'statusColor',
807+
},
808+
];
809+
810+
const { flushRafCalls } = setupWithProfile(profile);
811+
flushRafCalls();
812+
const drawLog = flushDrawLog();
813+
814+
// Find fillStyle operations in the draw log
815+
const fillStyleOps = drawLog.filter(
816+
(op: any) => Array.isArray(op) && op[0] === 'set fillStyle'
817+
);
818+
819+
return fillStyleOps.map((op: any) => op[1]);
820+
}
821+
822+
it('renders markers with colors specified in schema colorField', () => {
823+
const markersWithColors: TestDefinedMarker[] = [
824+
[
825+
'Green Test',
826+
0,
827+
5,
828+
{
829+
type: 'Test',
830+
status: 'success',
831+
statusColor: 'green',
832+
},
833+
],
834+
[
835+
'Red Test',
836+
6,
837+
10,
838+
{
839+
type: 'Test',
840+
status: 'failure',
841+
statusColor: 'red',
842+
},
843+
],
844+
[
845+
'Yellow Test',
846+
11,
847+
15,
848+
{
849+
type: 'Test',
850+
status: 'warning',
851+
statusColor: 'yellow',
852+
},
853+
],
854+
];
855+
856+
const fillColors = setupColoredMarkerTest(markersWithColors);
857+
858+
// The colors should include our marker colors with transparency
859+
expect(fillColors).toEqual(
860+
expect.arrayContaining([
861+
getFillColor('green'),
862+
getFillColor('red'),
863+
getFillColor('yellow'),
864+
])
865+
);
866+
});
867+
868+
it('falls back to default blue for markers without color data', () => {
869+
const markersWithoutColors: TestDefinedMarker[] = [
870+
[
871+
'Test without color',
872+
0,
873+
5,
874+
{
875+
type: 'Test',
876+
status: 'unknown',
877+
// No statusColor field
878+
},
879+
],
880+
];
881+
882+
const fillColors = setupColoredMarkerTest(markersWithoutColors);
883+
884+
// Should use default blue color
885+
expect(fillColors).toEqual(expect.arrayContaining([DEFAULT_FILL_COLOR]));
886+
});
887+
888+
it('ignores invalid color values', () => {
889+
const markersWithInvalidColors: TestDefinedMarker[] = [
890+
[
891+
'Invalid color test',
892+
0,
893+
5,
894+
{
895+
type: 'Test',
896+
status: 'unknown',
897+
statusColor: 'not-a-valid-color',
898+
},
899+
],
900+
];
901+
902+
const fillColors = setupColoredMarkerTest(markersWithInvalidColors);
903+
904+
// Should fall back to default blue since the color is invalid
905+
expect(fillColors).toEqual(expect.arrayContaining([DEFAULT_FILL_COLOR]));
906+
});
907+
});
783908
});

src/types/markers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ export type MarkerSchema = {
171171
// if present, give the marker its own local track
172172
graphs?: Array<MarkerGraph>;
173173

174+
// If present, specifies the key of a marker field that contains the marker's color.
175+
// The field should contain one of the GraphColor values.
176+
// This allows individual markers to have different colors based on their data.
177+
colorField?: string;
178+
174179
// If set to true, markers of this type are assumed to be well-nested with all
175180
// other stack-based markers on the same thread. Stack-based markers may
176181
// be displayed in a different part of the marker chart than non-stack-based

0 commit comments

Comments
 (0)