Skip to content

Commit

Permalink
Add Chromium flamechart colors (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
taneliang authored Jul 30, 2020
1 parent 5fe70a3 commit e4927cb
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 5 deletions.
86 changes: 86 additions & 0 deletions src/canvas/__tests__/colors-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

import {hslaColorToString, dimmedColor, ColorGenerator} from '../colors';

describe(hslaColorToString, () => {
it('should transform colors to strings', () => {
expect(hslaColorToString({h: 1, s: 2, l: 3, a: 4})).toEqual(
'hsl(1deg 2% 3% / 4)',
);
expect(hslaColorToString({h: 3.14, s: 6.28, l: 1.68, a: 100})).toEqual(
'hsl(3.14deg 6.28% 1.68% / 100)',
);
});
});

describe(dimmedColor, () => {
it('should dim luminosity using delta', () => {
expect(dimmedColor({h: 1, s: 2, l: 3, a: 4}, 3)).toEqual({
h: 1,
s: 2,
l: 0,
a: 4,
});
expect(dimmedColor({h: 1, s: 2, l: 3, a: 4}, -3)).toEqual({
h: 1,
s: 2,
l: 6,
a: 4,
});
});
});

describe(ColorGenerator, () => {
describe(ColorGenerator.prototype.colorForID, () => {
it('should generate a color for an ID', () => {
expect(new ColorGenerator().colorForID('123')).toMatchInlineSnapshot(`
Object {
"a": 1,
"h": 190,
"l": 80,
"s": 67,
}
`);
});

it('should generate colors deterministically given an ID', () => {
expect(new ColorGenerator().colorForID('id1')).toEqual(
new ColorGenerator().colorForID('id1'),
);
expect(new ColorGenerator().colorForID('id2')).toEqual(
new ColorGenerator().colorForID('id2'),
);
});

it('should generate different colors for different IDs', () => {
expect(new ColorGenerator().colorForID('id1')).not.toEqual(
new ColorGenerator().colorForID('id2'),
);
});

it('should return colors that have been set manually', () => {
const generator = new ColorGenerator();
const manualColor = {h: 1, s: 2, l: 3, a: 4};
generator.setColorForID('id with set color', manualColor);
expect(generator.colorForID('id with set color')).toEqual(manualColor);
expect(generator.colorForID('some other id')).not.toEqual(manualColor);
});

it('should generate colors from fixed color spaces', () => {
const generator = new ColorGenerator(1, 2, 3, 4);
expect(generator.colorForID('123')).toEqual({h: 1, s: 2, l: 3, a: 4});
expect(generator.colorForID('234')).toEqual({h: 1, s: 2, l: 3, a: 4});
});

it('should generate colors from range color spaces', () => {
const generator = new ColorGenerator(
{min: 0, max: 360, count: 2},
2,
3,
4,
);
expect(generator.colorForID('123')).toEqual({h: 0, s: 2, l: 3, a: 4});
expect(generator.colorForID('234')).toEqual({h: 360, s: 2, l: 3, a: 4});
});
});
});
106 changes: 106 additions & 0 deletions src/canvas/colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// @flow

type ColorSpace = number | {|min: number, max: number, count?: number|};

// Docstrings from https://www.w3schools.com/css/css_colors_hsl.asp
type HslaColor = $ReadOnly<{|
/** Hue is a degree on the color wheel from 0 to 360. 0 is red, 120 is green, and 240 is blue. */
h: number,
/** Saturation is a percentage value, 0% means a shade of gray, and 100% is the full color. */
s: number,
/** Lightness is a percentage, 0% is black, 50% is neither light or dark, 100% is white. */
l: number,
/** Alpha is a percentage, 0% is fully transparent, and 100 is not transparent at all. */
a: number,
|}>;

export function hslaColorToString({h, s, l, a}: HslaColor): string {
return `hsl(${h}deg ${s}% ${l}% / ${a})`;
}

export function dimmedColor(color: HslaColor, dimDelta: number): HslaColor {
return {
...color,
l: color.l - dimDelta,
};
}

// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/platform/utilities.js;l=120
function hashCode(string: string): number {
// Hash algorithm for substrings is described in "Über die Komplexität der Multiplikation in
// eingeschränkten Branchingprogrammmodellen" by Woelfe.
// http://opendatastructures.org/versions/edition-0.1d/ods-java/node33.html#SECTION00832000000000000000
const p = (1 << 30) * 4 - 5; // prime: 2^32 - 5
const z = 0x5033d967; // 32 bits from random.org
const z2 = 0x59d2f15d; // random odd 32 bit number
let s = 0;
let zi = 1;
for (let i = 0; i < string.length; i++) {
const xi = string.charCodeAt(i) * z2;
s = (s + zi * xi) % p;
zi = (zi * z) % p;
}
s = (s + zi * (p - 1)) % p;
return Math.abs(s | 0);
}

function indexToValueInSpace(index: number, space: ColorSpace): number {
if (typeof space === 'number') {
return space;
}
const count = space.count || space.max - space.min;
index %= count;
return (
space.min + Math.floor((index / (count - 1)) * (space.max - space.min))
);
}

/**
* Deterministic color generator.
*
* Adapted from: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/common/Color.js
*/
export class ColorGenerator {
_hueSpace: ColorSpace;
_satSpace: ColorSpace;
_lightnessSpace: ColorSpace;
_alphaSpace: ColorSpace;
_colors: Map<string, HslaColor>;

constructor(
hueSpace?: ColorSpace,
satSpace?: ColorSpace,
lightnessSpace?: ColorSpace,
alphaSpace?: ColorSpace,
) {
this._hueSpace = hueSpace || {min: 0, max: 360};
this._satSpace = satSpace || 67;
this._lightnessSpace = lightnessSpace || 80;
this._alphaSpace = alphaSpace || 1;
this._colors = new Map();
}

setColorForID(id: string, color: HslaColor) {
this._colors.set(id, color);
}

colorForID(id: string): HslaColor {
const cachedColor = this._colors.get(id);
if (cachedColor) {
return cachedColor;
}
const color = this._generateColorForID(id);
this._colors.set(id, color);
return color;
}

_generateColorForID(id: string): HslaColor {
const hash = hashCode(id);
return {
h: indexToValueInSpace(hash, this._hueSpace),
s: indexToValueInSpace(hash >> 8, this._satSpace),
l: indexToValueInSpace(hash >> 16, this._lightnessSpace),
a: indexToValueInSpace(hash >> 24, this._alphaSpace),
};
}
}
3 changes: 1 addition & 2 deletions src/canvas/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const BAR_HORIZONTAL_SPACING = 1;
export const BAR_SPACER_SIZE = 6;
export const MIN_BAR_WIDTH = 1;
export const SECTION_GUTTER_SIZE = 4;
export const COLOR_HOVER_DIM_DELTA = 5;

export const INTERVAL_TIMES = [
1,
Expand Down Expand Up @@ -61,8 +62,6 @@ export const EVENT_ROW_HEIGHT_FIXED =

export const COLORS = Object.freeze({
BACKGROUND: '#ffffff',
FLAME_CHART: '#fff79f',
FLAME_CHART_HOVER: '#ffe900',
OTHER_SCRIPT: '#fff791',
OTHER_SCRIPT_HOVER: '#ffea00',
PRIORITY_BACKGROUND: '#ededf0',
Expand Down
34 changes: 31 additions & 3 deletions src/canvas/views/FlamechartView.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,35 @@ import {
FLAMECHART_FONT_SIZE,
FLAMECHART_FRAME_HEIGHT,
FLAMECHART_TEXT_PADDING,
COLOR_HOVER_DIM_DELTA,
REACT_WORK_BORDER_SIZE,
} from '../constants';
import {ColorGenerator, dimmedColor, hslaColorToString} from '../colors';

// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399
const colorGenerator = new ColorGenerator(
{min: 30, max: 330},
{min: 50, max: 80, count: 3},
85,
);
colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100});

function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) {
return colorGenerator.colorForID(scriptUrl || '');
}

function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string {
const color = defaultHslaColorForStackFrame(stackFrame);
return hslaColorToString(color);
}

function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
const color = dimmedColor(
defaultHslaColorForStackFrame(stackFrame),
COLOR_HOVER_DIM_DELTA,
);
return hslaColorToString(color);
}

class FlamechartStackLayerView extends View {
/** Layer to display */
Expand Down Expand Up @@ -108,7 +135,8 @@ class FlamechartStackLayerView extends View {
const scaleFactor = positioningScaleFactor(intrinsicSize.width, frame);

for (let i = 0; i < stackLayer.length; i++) {
const {name, timestamp, duration} = stackLayer[i];
const stackFrame = stackLayer[i];
const {name, timestamp, duration} = stackFrame;

const width = durationToWidth(duration, scaleFactor);
if (width < 1) {
Expand All @@ -129,8 +157,8 @@ class FlamechartStackLayerView extends View {

const showHoverHighlight = hoveredStackFrame === stackLayer[i];
context.fillStyle = showHoverHighlight
? COLORS.FLAME_CHART_HOVER
: COLORS.FLAME_CHART;
? hoverColorForStackFrame(stackFrame)
: defaultColorForStackFrame(stackFrame);

const drawableRect = rectIntersectionWithRect(nodeRect, visibleArea);
context.fillRect(
Expand Down

1 comment on commit e4927cb

@vercel
Copy link

@vercel vercel bot commented on e4927cb Jul 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.