Skip to content

Commit

Permalink
Refactor ColorScale 2, docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
midlik committed May 20, 2024
1 parent a6db112 commit 28c4758
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 42 deletions.
2 changes: 1 addition & 1 deletion demo/demo1.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</div>

<script>
HeatmapComponent.demo1('app');
HeatmapComponent.demos.demo1('app');
</script>
</body>

Expand Down
2 changes: 1 addition & 1 deletion demo/demo2.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</div>

<script>
HeatmapComponent.demo2('app');
HeatmapComponent.demos.demo2('app');
</script>
</body>

Expand Down
2 changes: 1 addition & 1 deletion demo/demo3.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h1>Demo 3</h1>
</div>

<script>
HeatmapComponent.demo3('app');
HeatmapComponent.demos.demo3('app');
</script>
</body>

Expand Down
150 changes: 113 additions & 37 deletions src/heatmap-component/data/color-scale.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clamp, range } from 'lodash';
import { clamp, isArray, range, uniq } from 'lodash';
import * as d3 from '../d3-modules';
import { Color } from './color';
import { Domain } from './domain';
Expand All @@ -11,80 +11,156 @@ type RemovePrefix<P extends string, T> = T extends `${P}${infer S}` ? S : never

/** Set of continuous D3 color scheme names */
type D3ContinuousSchemeName = RemovePrefix<'interpolate', KeysWith<typeof d3, (t: number) => string>>
// /** Set of categorical D3 color scheme names */
// type D3CategoricalSchemeName = RemovePrefix<'scheme', KeysWith<typeof d3, readonly string[]>>
/** Set of categorical D3 color scheme names */
type D3CategoricalSchemeName = RemovePrefix<'scheme', KeysWith<typeof d3, readonly string[]>>
/** Set of all D3 color scheme names */
type D3SchemeName = D3ContinuousSchemeName | D3CategoricalSchemeName

/** List of names of available color schemes */
const AvailableSchemes = Object.keys(d3).filter(k => k.indexOf('interpolate') === 0).map(k => k.replace(/^interpolate/, '')) as D3ContinuousSchemeName[];
// const AllSchemes = Object.keys(d3).filter(k => k.indexOf('scheme') === 0).map(k => k.replace(/^scheme/, '')) as D3ContinuousSchemeName[];// TODO: type
/** List of names of available continuous color schemes */
const D3ContinuousSchemes = Object.keys(d3).filter(k => k.indexOf('interpolate') === 0).map(k => k.replace(/^interpolate/, '')) as D3ContinuousSchemeName[];
/** List of names of available categorical color schemes */
const D3CategoricalSchemes = Object.keys(d3).filter(k => k.indexOf('scheme') === 0 && isStringArray((d3 as any)[k])).map(k => k.replace(/^scheme/, '')) as D3CategoricalSchemeName[];
/** List of names of all available color schemes */
const D3Schemes = uniq([...D3ContinuousSchemes, ...D3CategoricalSchemes]).sort() as D3SchemeName[];

export type ColorScale = (x: number) => Color

function createScaleFromColors(values: number[], colors: (Color | string)[]): ColorScale {
export type ContinuousColorScale = (x: number) => Color
export type DiscreteColorScale<T> = (x: T) => Color


function continuousScaleFromColors(values: number[], colors: (Color | string)[]): ContinuousColorScale {
if (values.length !== colors.length) throw new Error('`values` and `colors` must have the same length');
const n = values.length;
const theDomain = Domain.create(values);
const theColors = colors.map(c => typeof c === 'string' ? Color.fromString(c) : c);
if (!theDomain.isNumeric || theDomain.sortDirection === 'none') {
throw new Error('Provided list of `values` is not numeric and monotonous');
}
return (x: number) => {
function colorScale(x: number): Color {
const contIndex = clamp(Domain.interpolateIndex(theDomain, x)!, 0, n - 1);
const index = Math.floor(contIndex);
if (index === n) return theColors[n];
else return Color.mix(theColors[index], theColors[index + 1], contIndex - index);
};
return colorScale;
}

function createScaleFromScheme(schemeName: D3ContinuousSchemeName, domain: [number, number] = [0, 1], range_: [number, number] = [0, 1]): ColorScale {
const colorInterpolator = d3[`interpolate${schemeName}`];
if (!colorInterpolator) {
const schemes = Object.keys(d3).filter(k => k.indexOf('interpolate') === 0).map(k => k.replace(/^interpolate/, ''));
throw new Error(`Invalid color scheme name: "${schemeName}".\n(Available schemes: ${schemes})`);
function continuousScaleFromScheme(schemeName: D3ContinuousSchemeName, domain: [number, number] = [0, 1], range_: [number, number] = [0, 1]): ContinuousColorScale {
const colorInterpolator = d3[`interpolate${schemeName as D3ContinuousSchemeName}`];
if (colorInterpolator !== undefined) {
const n = 101;
const values = linspace(domain, n);
const colors = colorsFromInterpolator(colorInterpolator, range_, n);
return continuousScaleFromColors(values, colors);
}
const n = 101;
const domainScale = d3.scaleLinear([0, n - 1], domain);
const values = range(n).map(i => domainScale(i));
const colors = colorsFromInterpolator(colorInterpolator, range_, n);
return createScaleFromColors(values, colors);
}

function colorsFromInterpolator(colorInterpolator: (t: number) => string, range_: [number, number], nColors: number): Color[] {
const rangeScale = d3.scaleLinear([0, nColors - 1], range_);
return range(nColors).map(i => Color.fromString(colorInterpolator(rangeScale(i))));
throw new Error(`Invalid color scheme name: "${schemeName}".\n(Available schemes: ${ColorScale.ContinuousSchemes})`);
}




/** Create a continuous color scale based on a named scheme,
* e.g. `continuous('Magma', [0, 1], [0, 1])`.
* `schemeName` is the name of the used scheme (list available from `ColorScale.AvailableSchemes`).
* `schemeName` is the name of the used scheme (list available from `ColorScale.ContinuousSchemes`).
* `domain` is the range of numbers that maps to the colors in the scheme: `domain[0]` maps to the first color in the scheme, `domain[1]` to the last color (default domain is [0, 1]).
* `range` can be used to select only a section of the whole scheme, e.g. [0, 0.5] uses only the first half of the scheme, [1, 0] reverses the scheme direction. */
function continuous(schemeName: D3ContinuousSchemeName, domain: [number, number], range?: [number, number]): ColorScale;

function continuous(schemeName: D3ContinuousSchemeName, domain: [number, number], range?: [number, number]): ContinuousColorScale;
/** Create a continuous color scale based on a list of numeric values and a list of colors mapped to these values, interpolating inbetween,
* e.g. `continuous([0, 0.5, 1], ['white', 'orange', 'brown'])`.
* `values` must be either ascending or descending. */
function continuous(values: number[], colors: (Color | string)[]): ColorScale;
function continuous(values: number[], colors: (Color | string)[]): ContinuousColorScale;
function continuous(a: D3ContinuousSchemeName | number[], b?: any, c?: any): ContinuousColorScale {
if (typeof a === 'string') return continuousScaleFromScheme(a, b, c);
else return continuousScaleFromColors(a, b);
}


function discreteScaleFromColors<T>(values: T[], colors: (Color | string)[], unknownColor: Color | string = '#888888'): DiscreteColorScale<T> {
if (values.length !== colors.length) throw new Error('`values` and `colors` must have the same length');
const n = values.length;
const map = new Map<T, Color>();
for (let i = 0; i < n; i++) {
const color = colors[i];
map.set(values[i], (typeof color === 'string') ? Color.fromString(color) : color);
}
const fallbackColor = (typeof unknownColor === 'string') ? Color.fromString(unknownColor) : unknownColor;
function discreteColorScale(x: T): Color {
return map.get(x) ?? fallbackColor;
};
return discreteColorScale;
}

function discreteScaleFromScheme<T>(schemeName: D3SchemeName, values: T[], unknownColor?: Color | string): DiscreteColorScale<T> {
const scheme = d3[`scheme${schemeName as D3CategoricalSchemeName}`];
if (isStringArray(scheme)) {
const colorList = scheme.map(s => Color.fromString(s));
return discreteScaleFromColors(values, cycle(colorList, values.length), unknownColor);
}
const colorInterpolator = d3[`interpolate${schemeName as D3ContinuousSchemeName}`];
if (colorInterpolator !== undefined) {
const n = values.length;
const colors = colorsFromInterpolator(colorInterpolator, [0, 1], n);
return discreteScaleFromColors(values, colors, unknownColor);
}

function continuous(a: D3ContinuousSchemeName | number[], b?: any, c?: any): ColorScale {
if (typeof a === 'string') return createScaleFromScheme(a, b, c);
else return createScaleFromColors(a, b);
throw new Error(`Invalid color scheme name: "${schemeName}".\n(Available schemes: ${ColorScale.DiscreteSchemes})`);
}

/** Create a discrete (categorical) color scale based on a named scheme,
* e.g. `discrete('Set1', ['dog', 'cat', 'fish'], 'gray')`.
* `schemeName` is the name of the used scheme (list available from `ColorScale.DiscreteSchemes`).
* `values` is the set of values (of any type) that map to the colors in the scheme.
* `unknownColor` parameter is the color that will be used for any value not present in `values`. */
function discrete<T>(schemeName: D3SchemeName, values: T[], unknownColor?: Color | string): DiscreteColorScale<T>;
/** Create a discrete (categorical) color scale based on a list of values (of any type) and a list of colors mapped to these values,
* e.g. `discrete(['dog', 'cat', 'fish'], ['red', 'green', 'blue'], 'gray')`.
* `unknownColor` parameter is the color that will be used for any value not present in `values`. */
function discrete<T>(values: T[], colors: (Color | string)[], unknownColor?: Color | string): DiscreteColorScale<T>;
function discrete<T>(a: D3SchemeName | T[], b: any, c: any): DiscreteColorScale<T> {
if (typeof a === 'string') return discreteScaleFromScheme(a, b, c);
else return discreteScaleFromColors(a, b, c);
}


function linspace(range_: [number, number], n: number): number[] {
const scale = d3.scaleLinear([0, n - 1], range_);
return range(n).map(i => scale(i));
}
function colorsFromInterpolator(colorInterpolator: (t: number) => string, range_: [number, number], nColors: number): Color[] {
return linspace(range_, nColors).map(x => Color.fromString(colorInterpolator(x)));
}
function isStringArray(value: any): value is string[] {
return isArray(value) && value.length > 0 && typeof (value[0]) === 'string';
}
function cycle<T>(source: T[], n: number): T[] {
const result = [];
while (result.length < n) {
result.push(...source);
}
result.length = n;
return result;
}


/** Functions for creating color scales to be used with heatmap. */
export const ColorScale = {
/** List of names of available color schemes */
AvailableSchemes,
// AllSchemes,
/** List of available color schemes for `ColorScale.continuous()` */
ContinuousSchemes: D3ContinuousSchemes, // continuous can only be used with continuous D3 schemes

/** List of available color schemes for `ColorScale.discrete()` */
DiscreteSchemes: D3Schemes, // discrete can be used with both categorical and continuous D3 schemes

/** Create a continuous color scale, i.e. a mapping from real number to color.
* Examples:
* ```
* continuous('Magma', [0, 1], [0, 1])
* continuous([0, 0.5, 1], ['white', 'orange', 'brown'])
* ``` */
continuous: continuous,
continuous,

/** Create a discrete (categorical) color scale, i.e. a mapping from values of any type to colors.
* Examples:
* ```
* discrete('Set1', ['dog', 'cat', 'fish'], 'gray')
* discrete(['dog', 'cat', 'fish'], ['red', 'green', 'blue'], 'gray')
* ``` */
discrete,
};
2 changes: 1 addition & 1 deletion src/heatmap-component/extensions/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class TooltipBehavior<TX, TY, TDatum> extends BehaviorBase<TooltipExtensi
this.subscribe(this.state.events.zoom, () => this.updatePinnedTooltipPosition());
this.subscribe(this.state.events.resize, () => this.updatePinnedTooltipPosition());
}

/** Add a div with tooltip or update position of existing tooltip, for the `pointed` grid cell.
* Remove existing tooltip, if `pointed` is `undefined`. */
private drawTooltip(pointed: CellEventValue<TX, TY, TDatum> | undefined): void {
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Main file for importing `HeatmapComponent` as a dependency */

export { ColorScale } from './heatmap-component/data/color-scale';
export { demo1, demo2, demo3 } from './heatmap-component/demo';
export * as demos from './heatmap-component/demo';
export { Heatmap } from './heatmap-component/heatmap';

0 comments on commit 28c4758

Please sign in to comment.