Skip to content

Commit

Permalink
color mapping for categoricals
Browse files Browse the repository at this point in the history
  • Loading branch information
sgratzl committed Nov 23, 2018
1 parent 60292bf commit 119c5ef
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 27 deletions.
74 changes: 74 additions & 0 deletions src/model/CategoricalColorMappingFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {ICategory} from './ICategoricalColumn';
import Column from './Column';


export interface ICategoricalColorMappingFunction {
apply(v: ICategory): string;

dump(): any;

clone(): ICategoricalColorMappingFunction;

eq(other: ICategoricalColorMappingFunction): boolean;
}

/**
* @internal
*/
export const DEFAULT_COLOR_FUNCTION: ICategoricalColorMappingFunction = {
apply: (v) => v ? v.color : Column.DEFAULT_COLOR,
dump: () => null,
clone: () => DEFAULT_COLOR_FUNCTION,
eq: (other) => other === DEFAULT_COLOR_FUNCTION
};

export class ReplacmentColorMappingFunction implements ICategoricalColorMappingFunction {
constructor(private readonly map: Map<ICategory, string>) {
// nothing to do
}

apply(v: ICategory) {
return this.map.has(v) ? this.map.get(v)! : DEFAULT_COLOR_FUNCTION.apply(v);
}

dump() {
const r: any = {};
this.map.forEach((v, k) => r[k.name] = v);
return r;
}

clone() {
return new ReplacmentColorMappingFunction(new Map(this.map.entries()));
}

eq(other: ICategoricalColorMappingFunction): boolean {
if (!(other instanceof ReplacmentColorMappingFunction)) {
return false;
}
if (other.map.size !== this.map.size) {
return false;
}
return Array.from(this.map.keys()).every((k) => this.map.get(k) === other.map.get(k));
}

static restore(dump: any, categories: ICategory[]) {
const lookup = new Map(categories.map((d) => <[string, ICategory]>[d.name, d]));
const r = new Map<ICategory, string>();
for (const key of Object.keys(dump)) {
if (lookup.has(key)) {
r.set(lookup.get(key)!, dump[key]);
}
}
return new ReplacmentColorMappingFunction(r);
}
}

/**
* @internal
*/
export function restoreColorMapping(dump: any, categories: ICategory[]): ICategoricalColorMappingFunction {
if (!dump) {
return DEFAULT_COLOR_FUNCTION;
}
return ReplacmentColorMappingFunction.restore(dump, categories);
}
46 changes: 38 additions & 8 deletions src/model/CategoricalColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import {
import {IDataRow, IGroup, IGroupData} from './interfaces';
import {missingGroup} from './missing';
import {IEventListener} from '../internal/AEventDispatcher';
import {ICategoricalColorMappingFunction, DEFAULT_COLOR_FUNCTION, restoreColorMapping} from './CategoricalColorMappingFunction';


/**
* emitted when the color mapping property changes
* @asMemberOf CategoricalColumn
* @event
*/
export declare function colorMappingChanged(previous: ICategoricalColorMappingFunction, current: ICategoricalColorMappingFunction): void;


/**
* emitted when the filter property changes
Expand All @@ -20,15 +30,18 @@ export declare function filterChanged(previous: ICategoricalFilter | null, curre
/**
* column for categorical values
*/
@toolbar('group', 'groupBy', 'sortGroupBy', 'filterCategorical')
@toolbar('group', 'groupBy', 'sortGroupBy', 'filterCategorical', 'colorMappedCategorical')
@Category('categorical')
export default class CategoricalColumn extends ValueColumn<string> implements ICategoricalColumn {
static readonly EVENT_FILTER_CHANGED = 'filterChanged';
static readonly EVENT_COLOR_MAPPING_CHANGED = 'colorMappingChanged';

readonly categories: ICategory[];

private readonly missingCategory: ICategory | null;

private colorMapping: ICategoricalColorMappingFunction;

private readonly lookup = new Map<string, Readonly<ICategory>>();
/**
* set of categories to show
Expand All @@ -42,13 +55,15 @@ export default class CategoricalColumn extends ValueColumn<string> implements IC
this.categories = toCategories(desc);
this.missingCategory = desc.missingCategory ? toCategory(desc.missingCategory, NaN) : null;
this.categories.forEach((d) => this.lookup.set(d.name, d));
this.colorMapping = DEFAULT_COLOR_FUNCTION;
}

protected createEventList() {
return super.createEventList().concat([CategoricalColumn.EVENT_FILTER_CHANGED]);
return super.createEventList().concat([CategoricalColumn.EVENT_FILTER_CHANGED, CategoricalColumn.EVENT_COLOR_MAPPING_CHANGED]);
}

on(type: typeof CategoricalColumn.EVENT_FILTER_CHANGED, listener: typeof filterChanged | null): this;
on(type: typeof CategoricalColumn.EVENT_COLOR_MAPPING_CHANGED, listener: typeof colorMappingChanged | null): this;
on(type: typeof ValueColumn.EVENT_DATA_LOADED, listener: typeof dataLoaded | null): this;
on(type: typeof Column.EVENT_WIDTH_CHANGED, listener: typeof widthChanged | null): this;
on(type: typeof Column.EVENT_LABEL_CHANGED, listener: typeof labelChanged | null): this;
Expand Down Expand Up @@ -91,11 +106,6 @@ export default class CategoricalColumn extends ValueColumn<string> implements IC
return v ? v.label : '';
}

getColor(row: IDataRow) {
const v = this.getCategory(row);
return v ? v.color : Column.DEFAULT_COLOR;
}

getValues(row: IDataRow) {
const v = this.getCategory(row);
return this.categories.map((d) => d === v);
Expand Down Expand Up @@ -131,12 +141,16 @@ export default class CategoricalColumn extends ValueColumn<string> implements IC
dump(toDescRef: (desc: any) => any): any {
const r = super.dump(toDescRef);
r.filter = this.currentFilter;
r.colorMapping = this.colorMapping.dump();
return r;
}

restore(dump: any, factory: (dump: any) => Column | null) {
super.restore(dump, factory);
if (!('filter' in dump)) {

this.colorMapping = restoreColorMapping(dump.colorMapping, this.categories);

if ('filter' in dump) {
this.currentFilter = null;
return;
}
Expand All @@ -148,6 +162,22 @@ export default class CategoricalColumn extends ValueColumn<string> implements IC
}
}

getColor(row: IDataRow) {
const v = this.getCategory(row);
return v ? this.colorMapping.apply(v) : Column.DEFAULT_COLOR;
}

getColorMapping() {
return this.colorMapping.clone();
}

setColorMapping(mapping: ICategoricalColorMappingFunction) {
if (this.colorMapping.eq(mapping)) {
return;
}
this.fire([CategoricalColumn.EVENT_COLOR_MAPPING_CHANGED, Column.EVENT_DIRTY_VALUES, Column.EVENT_DIRTY_HEADER, Column.EVENT_DIRTY], this.colorMapping.clone(), this.colorMapping = mapping);
}

isFiltered() {
return this.currentFilter != null;
}
Expand Down
62 changes: 62 additions & 0 deletions src/model/CategoricalMapColumn.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,60 @@
import {ICategoricalDesc, ICategory, toCategories, toCategory} from './ICategoricalColumn';
import {IDataRow} from './interfaces';
import MapColumn, {IMapColumnDesc} from './MapColumn';
import {ICategoricalColorMappingFunction, DEFAULT_COLOR_FUNCTION, restoreColorMapping} from './CategoricalColorMappingFunction';
import CategoricalColumn from './CategoricalColumn';
import ValueColumn, {dataLoaded} from './ValueColumn';
import Column, {labelChanged, metaDataChanged, dirty, dirtyHeader, dirtyValues, rendererTypeChanged, groupRendererChanged, summaryRendererChanged, visibilityChanged, widthChanged} from './Column';
import {IEventListener} from '../internal/AEventDispatcher';
import {toolbar} from './annotations';


export declare type ICategoricalMapColumnDesc = ICategoricalDesc & IMapColumnDesc<string | null>;

/**
* emitted when the color mapping property changes
* @asMemberOf CategoricalMapColumn
* @event
*/
export declare function colorMappingChanged(previous: ICategoricalColorMappingFunction, current: ICategoricalColorMappingFunction): void;

@toolbar('colorMappedCategorical')
export default class CategoricalMapColumn extends MapColumn<string | null> {
static readonly EVENT_COLOR_MAPPING_CHANGED = CategoricalColumn.EVENT_COLOR_MAPPING_CHANGED;

readonly categories: ICategory[];

private readonly missingCategory: ICategory | null;

private readonly lookup = new Map<string, Readonly<ICategory>>();

private colorMapping: ICategoricalColorMappingFunction;

constructor(id: string, desc: Readonly<ICategoricalMapColumnDesc>) {
super(id, desc);
this.categories = toCategories(desc);
this.missingCategory = desc.missingCategory ? toCategory(desc.missingCategory, NaN) : null;
this.categories.forEach((d) => this.lookup.set(d.name, d));
this.colorMapping = DEFAULT_COLOR_FUNCTION;
}
protected createEventList() {
return super.createEventList().concat([CategoricalMapColumn.EVENT_COLOR_MAPPING_CHANGED]);
}

on(type: typeof CategoricalMapColumn.EVENT_COLOR_MAPPING_CHANGED, listener: typeof colorMappingChanged | null): this;
on(type: typeof ValueColumn.EVENT_DATA_LOADED, listener: typeof dataLoaded | null): this;
on(type: typeof Column.EVENT_WIDTH_CHANGED, listener: typeof widthChanged | null): this;
on(type: typeof Column.EVENT_LABEL_CHANGED, listener: typeof labelChanged | null): this;
on(type: typeof Column.EVENT_METADATA_CHANGED, listener: typeof metaDataChanged | null): this;
on(type: typeof Column.EVENT_DIRTY, listener: typeof dirty | null): this;
on(type: typeof Column.EVENT_DIRTY_HEADER, listener: typeof dirtyHeader | null): this;
on(type: typeof Column.EVENT_DIRTY_VALUES, listener: typeof dirtyValues | null): this;
on(type: typeof Column.EVENT_RENDERER_TYPE_CHANGED, listener: typeof rendererTypeChanged | null): this;
on(type: typeof Column.EVENT_GROUP_RENDERER_TYPE_CHANGED, listener: typeof groupRendererChanged | null): this;
on(type: typeof Column.EVENT_SUMMARY_RENDERER_TYPE_CHANGED, listener: typeof summaryRendererChanged | null): this;
on(type: typeof Column.EVENT_VISIBILITY_CHANGED, listener: typeof visibilityChanged | null): this;
on(type: string | string[], listener: IEventListener | null): this {
return super.on(<any>type, listener);
}


Expand All @@ -34,6 +73,10 @@ export default class CategoricalMapColumn extends MapColumn<string | null> {
}));
}

getColors(row: IDataRow) {
return this.getCategories(row).map(({key, value}) => ({key, value: value ? this.colorMapping.apply(value): Column.DEFAULT_COLOR}));
}

getValue(row: IDataRow) {
return this.getCategories(row).map(({key, value}) => ({
key,
Expand All @@ -47,4 +90,23 @@ export default class CategoricalMapColumn extends MapColumn<string | null> {
value: value ? value.label : ''
}));
}

getColorMapping() {
return this.colorMapping.clone();
}

setColorMapping(mapping: ICategoricalColorMappingFunction) {
return CategoricalColumn.prototype.setColorMapping.call(this, mapping);
}

dump(toDescRef: (desc: any) => any): any {
const r = super.dump(toDescRef);
r.colorMapping = this.colorMapping.dump();
return r;
}

restore(dump: any, factory: (dump: any) => Column | null) {
super.restore(dump, factory);
this.colorMapping = restoreColorMapping(dump.colorMapping, this.categories);
}
}
65 changes: 64 additions & 1 deletion src/model/CategoricalsColumn.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
import ArrayColumn, {IArrayColumnDesc} from './ArrayColumn';
import ArrayColumn, {IArrayColumnDesc, spliceChanged} from './ArrayColumn';
import {ICategoricalDesc, ICategory, toCategories, toCategory} from './ICategoricalColumn';
import {IDataRow} from './interfaces';
import {toolbar} from './annotations';
import CategoricalColumn from './CategoricalColumn';
import {ICategoricalColorMappingFunction, DEFAULT_COLOR_FUNCTION, restoreColorMapping} from './CategoricalColorMappingFunction';
import ValueColumn, {dataLoaded} from './ValueColumn';
import Column, {labelChanged, metaDataChanged, dirty, dirtyHeader, dirtyValues, rendererTypeChanged, groupRendererChanged, summaryRendererChanged, visibilityChanged, widthChanged} from './Column';
import {IEventListener} from '../internal/AEventDispatcher';

export declare type ICategoricalsColumnDesc = ICategoricalDesc & IArrayColumnDesc<string | null>;

/**
* emitted when the color mapping property changes
* @asMemberOf CategoricalsColumn
* @event
*/
export declare function colorMappingChanged(previous: ICategoricalColorMappingFunction, current: ICategoricalColorMappingFunction): void;

/**
* a string column with optional alignment
*/
@toolbar('colorMappedCategorical')
export default class CategoricalsColumn extends ArrayColumn<string | null> {
static readonly EVENT_COLOR_MAPPING_CHANGED = CategoricalColumn.EVENT_COLOR_MAPPING_CHANGED;

readonly categories: ICategory[];

private readonly missingCategory: ICategory | null;

private readonly lookup = new Map<string, Readonly<ICategory>>();

private colorMapping: ICategoricalColorMappingFunction;

constructor(id: string, desc: Readonly<ICategoricalsColumnDesc>) {
super(id, desc);
this.categories = toCategories(desc);
this.missingCategory = desc.missingCategory ? toCategory(desc.missingCategory, NaN) : null;
this.categories.forEach((d) => this.lookup.set(d.name, d));
this.colorMapping = DEFAULT_COLOR_FUNCTION;
}

protected createEventList() {
return super.createEventList().concat([CategoricalsColumn.EVENT_COLOR_MAPPING_CHANGED]);
}

on(type: typeof CategoricalsColumn.EVENT_COLOR_MAPPING_CHANGED, listener: typeof colorMappingChanged | null): this;
on(type: typeof ArrayColumn.EVENT_SPLICE_CHANGED, listener: typeof spliceChanged | null): this;
on(type: typeof ValueColumn.EVENT_DATA_LOADED, listener: typeof dataLoaded | null): this;
on(type: typeof Column.EVENT_WIDTH_CHANGED, listener: typeof widthChanged | null): this;
on(type: typeof Column.EVENT_LABEL_CHANGED, listener: typeof labelChanged | null): this;
on(type: typeof Column.EVENT_METADATA_CHANGED, listener: typeof metaDataChanged | null): this;
on(type: typeof Column.EVENT_DIRTY, listener: typeof dirty | null): this;
on(type: typeof Column.EVENT_DIRTY_HEADER, listener: typeof dirtyHeader | null): this;
on(type: typeof Column.EVENT_DIRTY_VALUES, listener: typeof dirtyValues | null): this;
on(type: typeof Column.EVENT_RENDERER_TYPE_CHANGED, listener: typeof rendererTypeChanged | null): this;
on(type: typeof Column.EVENT_GROUP_RENDERER_TYPE_CHANGED, listener: typeof groupRendererChanged | null): this;
on(type: typeof Column.EVENT_SUMMARY_RENDERER_TYPE_CHANGED, listener: typeof summaryRendererChanged | null): this;
on(type: typeof Column.EVENT_VISIBILITY_CHANGED, listener: typeof visibilityChanged | null): this;
on(type: string | string[], listener: IEventListener | null): this {
return super.on(<any>type, listener);
}

getCategories(row: IDataRow) {
Expand All @@ -31,6 +71,10 @@ export default class CategoricalsColumn extends ArrayColumn<string | null> {
});
}

getColors(row: IDataRow) {
return this.getCategories(row).map((d) => d ? this.colorMapping.apply(d): Column.DEFAULT_COLOR);
}

getSet(row: IDataRow) {
const r = new Set(this.getCategories(row));
r.delete(this.missingCategory);
Expand All @@ -44,4 +88,23 @@ export default class CategoricalsColumn extends ArrayColumn<string | null> {
getLabels(row: IDataRow) {
return this.getCategories(row).map((v) => v ? v.label : '');
}

getColorMapping() {
return this.colorMapping.clone();
}

setColorMapping(mapping: ICategoricalColorMappingFunction) {
return CategoricalColumn.prototype.setColorMapping.call(this, mapping);
}

dump(toDescRef: (desc: any) => any): any {
const r = super.dump(toDescRef);
r.colorMapping = this.colorMapping.dump();
return r;
}

restore(dump: any, factory: (dump: any) => Column | null) {
super.restore(dump, factory);
this.colorMapping = restoreColorMapping(dump.colorMapping, this.categories);
}
}
1 change: 1 addition & 0 deletions src/model/ColorMappingFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {IMapAbleDesc} from './MappingFunction';
import Column from './Column';
import {equal} from '../internal/utils';
import {scaleLinear} from 'd3-scale';
import {ICategory} from './ICategoricalColumn';

export interface IColorMappingFunctionBase {
apply(v: number): string;
Expand Down
Loading

0 comments on commit 119c5ef

Please sign in to comment.