oncoprintjs
Version:
A data visualization for cancer genomic data.
347 lines (321 loc) • 9.49 kB
text/typescript
import { Datum } from './oncoprintmodel';
import { RGBAColor } from './oncoprintruleset';
type StringParameter = 'type';
type PercentNumberParameter =
| 'width'
| 'height'
| 'x'
| 'y'
| 'x1'
| 'x2'
| 'x3'
| 'y1'
| 'y2'
| 'y3';
type PlainNumberParameter = 'z' | 'stroke-width' | 'stroke-opacity';
type RGBAParameter = 'stroke' | 'fill';
type NumberParameter = PercentNumberParameter | PlainNumberParameter;
type Parameter = StringParameter | NumberParameter | RGBAParameter;
const default_parameter_values: { [x in StringParameter]?: string } &
{ [x in NumberParameter]?: number } &
{ [x in RGBAParameter]?: RGBAColor } = {
width: 100,
height: 100,
x: 0,
y: 0,
z: 0,
x1: 0,
x2: 0,
x3: 0,
y1: 0,
y2: 0,
y3: 0,
stroke: [0, 0, 0, 0],
fill: [23, 23, 23, 1],
'stroke-width': 0,
'stroke-opacity': 0,
};
const percent_parameter_name_to_dimension_index: {
[x in PercentNumberParameter]: number;
} = {
width: 0,
x: 0,
x1: 0,
x2: 0,
x3: 0,
height: 1,
y: 1,
y1: 1,
y2: 1,
y3: 1,
};
const hash_parameter_order: Parameter[] = [
'width',
'height',
'x',
'y',
'z',
'x1',
'x2',
'x3',
'y1',
'y2',
'y3',
'stroke',
'fill',
'stroke-width',
'stroke-opacity',
'type',
];
type StringParamFunction = (d: Datum) => string;
type NumberParamFunction = (d: Datum) => number;
type RGBAParamFunction = (d: Datum) => RGBAColor;
type ParamFunction =
| StringParamFunction
| NumberParamFunction
| RGBAParamFunction;
export type ShapeParams = {
[x in StringParameter]?: string | StringParamFunction;
} &
{ [x in NumberParameter]?: number | NumberParamFunction } &
{ [x in RGBAParameter]?: RGBAColor | RGBAParamFunction };
type ShapeParamsWithType = {
[x in StringParameter]?:
| { type: 'function'; value: StringParamFunction }
| { type: 'value'; value: string };
} &
{
[x in NumberParameter]?:
| { type: 'function'; value: NumberParamFunction }
| { type: 'value'; value: number };
} &
{
[x in RGBAParameter]?:
| { type: 'function'; value: RGBAParamFunction }
| { type: 'value'; value: RGBAColor };
};
export type ComputedShapeParams = { [x in StringParameter]?: string } &
{ [x in NumberParameter]?: number } &
{ [x in RGBAParameter]?: RGBAColor };
function isPercentParam(
param_name: string
): param_name is PercentNumberParameter {
return param_name in percent_parameter_name_to_dimension_index;
}
export class Shape {
private static cache: { [hash: string]: ComputedShapeParams } = {}; // shape cache to reuse objects and thus save memory
private params_with_type: ShapeParamsWithType = {};
private onlyDependsOnWidthAndHeight: boolean;
private instanceCache = {
lastComputedParams: null as ComputedShapeParams | null,
lastWidth: -1,
lastHeight: -1,
};
constructor(private params: ShapeParams) {
this.completeWithDefaults();
this.markParameterTypes();
}
public static hashComputedShape(
computed_params: ComputedShapeParams,
z_index?: number | string
) {
return (
hash_parameter_order.reduce(function(
hash: string,
param_name: Parameter
) {
return hash + ',' + computed_params[param_name];
},
'') +
',' +
z_index
);
}
private static getCachedShape(computed_params: ComputedShapeParams) {
const hash = Shape.hashComputedShape(computed_params);
Shape.cache[hash] = Shape.cache[hash] || Object.freeze(computed_params);
return Shape.cache[hash];
}
public getRequiredParameters(): Parameter[] {
throw 'Not defined for base class';
}
public completeWithDefaults() {
const required_parameters = this.getRequiredParameters();
for (let i = 0; i < required_parameters.length; i++) {
const param = required_parameters[i];
this.params[param] = (typeof this.params[param] === 'undefined'
? default_parameter_values[param]
: this.params[param]) as any;
}
}
public markParameterTypes() {
const parameters = Object.keys(this.params) as Parameter[];
let onlyDependsOnWidthAndHeight = true;
for (let i = 0; i < parameters.length; i++) {
const param_name = parameters[i];
const param_val = this.params[param_name];
if (typeof param_val === 'function') {
this.params_with_type[param_name] = {
type: 'function',
value: param_val as any,
};
onlyDependsOnWidthAndHeight = false;
} else {
this.params_with_type[param_name] = {
type: 'value',
value: param_val,
} as any;
}
}
this.onlyDependsOnWidthAndHeight = onlyDependsOnWidthAndHeight;
}
public getComputedParams(
d: Datum,
base_width: number,
base_height: number
) {
if (
this.onlyDependsOnWidthAndHeight &&
this.instanceCache.lastWidth === base_width &&
this.instanceCache.lastHeight === base_height
) {
return this.instanceCache.lastComputedParams!;
}
const computed_params: Partial<ComputedShapeParams> = {};
const param_names = Object.keys(this.params_with_type) as Parameter[];
const dimensions: [number, number] = [base_width, base_height];
for (let i = 0; i < param_names.length; i++) {
const param_name = param_names[i];
const param_val_map = this.params_with_type[param_name];
let param_val = param_val_map.value;
if (param_name !== 'type') {
if (param_val_map.type === 'function') {
param_val = (param_val as ParamFunction)(d);
}
// at this point, param_val is resolved to either a string or number
if (isPercentParam(param_name)) {
// if its a percentage param, compute value as percentage of width or height
param_val =
((param_val as number) / 100) *
dimensions[
percent_parameter_name_to_dimension_index[
param_name
]
];
}
}
//@ts-ignore
computed_params[param_name] = param_val;
}
if (this.onlyDependsOnWidthAndHeight) {
// only cache if its cacheable, otherwise it would be a waste of memory to save
this.instanceCache.lastHeight = base_height;
this.instanceCache.lastWidth = base_width;
this.instanceCache.lastComputedParams = Shape.getCachedShape(
computed_params
);
}
return Shape.getCachedShape(computed_params);
}
}
type SpecificComputedShapeParams<ShapeParamType> = {
[x in ShapeParamType & StringParameter]: string;
} &
{ [x in ShapeParamType & NumberParameter]: number } &
{ [x in ShapeParamType & RGBAParameter]: RGBAColor };
type RectangleParameter =
| 'width'
| 'height'
| 'x'
| 'y'
| 'z'
| 'stroke'
| 'stroke-width'
| 'fill';
export type ComputedRectangleParams = SpecificComputedShapeParams<
RectangleParameter
>;
export class Rectangle extends Shape {
public getRequiredParameters(): RectangleParameter[] {
return [
'width',
'height',
'x',
'y',
'z',
'stroke',
'fill',
'stroke-width',
];
}
}
type TriangleParameter =
| 'x1'
| 'x2'
| 'x3'
| 'y1'
| 'y2'
| 'y3'
| 'z'
| 'stroke'
| 'stroke-width'
| 'fill';
export type ComputedTriangleParams = SpecificComputedShapeParams<
TriangleParameter
>;
export class Triangle extends Shape {
public getRequiredParameters(): TriangleParameter[] {
return [
'x1',
'x2',
'x3',
'y1',
'y2',
'y3',
'z',
'stroke',
'fill',
'stroke-width',
];
}
}
export type EllipseParameter =
| 'width'
| 'height'
| 'x'
| 'y'
| 'z'
| 'stroke'
| 'stroke-width'
| 'fill';
export type ComputedEllipseParams = SpecificComputedShapeParams<
EllipseParameter
>;
export class Ellipse extends Shape {
public getRequiredParameters(): EllipseParameter[] {
return [
'width',
'height',
'x',
'y',
'z',
'stroke',
'fill',
'stroke-width',
];
}
}
export type LineParameter =
| 'x1'
| 'y1'
| 'x2'
| 'y2'
| 'z'
| 'stroke'
| 'stroke-width';
export type ComputedLineParams = SpecificComputedShapeParams<LineParameter>;
export class Line extends Shape {
public getRequiredParameters(): LineParameter[] {
return ['x1', 'x2', 'y1', 'y2', 'z', 'stroke', 'stroke-width'];
}
}