/** * Created by mm28969 on 1/28/17. */ import * as d3 from "d3"; import { areaToRadius, ChartDrawArea, convertValueToAttribute, Theme } from "../mmviz-common/index"; export enum ScaleTypeEnum { LINEAR = 1, SQRT, POW, BAND, ORDINAL, TIME, THRESHOLD, QUANTILE, QUANTIZE } /** * Get ScaleTypeEnum that matches provided scaleType string * @param scaleType */ export function getScaleTypeEnum(scaleType: string): ScaleTypeEnum { let scaleTypeEnum: ScaleTypeEnum; switch (scaleType){ case "linear": scaleTypeEnum = ScaleTypeEnum.LINEAR; break; case "sqrt": scaleTypeEnum = ScaleTypeEnum.SQRT; break; case "pow": scaleTypeEnum = ScaleTypeEnum.POW; break; case "band": scaleTypeEnum = ScaleTypeEnum.BAND; break; case "ordinal": scaleTypeEnum = ScaleTypeEnum.ORDINAL; break; case "time": scaleTypeEnum = ScaleTypeEnum.TIME; break; case "threshold": scaleTypeEnum = ScaleTypeEnum.THRESHOLD; break; case "quantile": scaleTypeEnum = ScaleTypeEnum.QUANTILE; break; case "quantize": scaleTypeEnum = ScaleTypeEnum.QUANTIZE; break; default: throw "Invaild scale type"; } return scaleTypeEnum; } export enum ScaleContainerTypeEnum { COLOR_CATEGORICAL = 1, COLOR_CATEGORICAL_LIGHT, COLOR_CATEGORICAL_DARK, COLOR_SEQUENTIAL, COLOR_SEQUENTIAL_LIGHT, COLOR_SEQUENTIAL_DARK, COLOR_DIVERGING, COLOR_DIVERGING_LIGHT, COLOR_DIVERGING_DARK, COLOR_SEMANTIC_POSITIVE, COLOR_SEMANTIC_NEGATIVE, COLOR_SEMANTIC_CAUTION, COLOR_SEMANTIC_NEUTRAL, COLOR_SEQUENTIAL_BRAND_BLUE, COLOR_SEQUENTIAL_PURPLE, COLOR_SEQUENTIAL_GREEN, COLOR_SEQUENTIAL_ORANGE, COLOR_SEQUENTIAL_GRAY, AREA, SHAPE } function getScale(scaleType: ScaleTypeEnum) { let scale; switch (scaleType) { case ScaleTypeEnum.LINEAR: scale = d3.scaleLinear(); break; case ScaleTypeEnum.SQRT: scale = d3.scaleSqrt(); break; case ScaleTypeEnum.POW: scale = d3.scalePow(); break; case ScaleTypeEnum.TIME: scale = d3.scaleTime(); break; case ScaleTypeEnum.BAND: scale = d3.scaleBand(); break; case ScaleTypeEnum.ORDINAL: scale = d3.scaleOrdinal(); break; case ScaleTypeEnum.THRESHOLD: scale = d3.scaleThreshold(); break; case ScaleTypeEnum.QUANTILE: scale = d3.scaleQuantile(); break; case ScaleTypeEnum.QUANTIZE: scale = d3.scaleQuantize(); break; default: throw new Error("Unknown scale"); } return scale; } export class ScaleContainer { key; type: ScaleTypeEnum; containerType: ScaleContainerTypeEnum; scale; label; defaultValue; hasDomain; hasRange; constructor(key: string, type: ScaleTypeEnum){ this.key = key; this.type = type; this.scale = getScale(type); this.hasDomain = false; this.hasRange = false; } get domain() { return this.scale.domain(); } set domain(newDomain: any[]) { this.scale.domain(newDomain); this.hasDomain = true; } get range() { return this.scale.range(); } set range(newRange: any[]) { this.scale.range(newRange); this.hasRange = true; } makeRangeDomainMultiple(multiple: number){ let d; const range = []; const domain = this.domain; for (d of domain){ range.push(d * multiple); } this.range = range; return this; } extendDomain(newDomain: any[]){ let d, domain = this.scale.domain(), domainAll; if (!this.hasDomain) { domain = newDomain; this.hasDomain = true; } else { if (ScaleTypeEnum.ORDINAL === this.type || ScaleTypeEnum.BAND === this.type){ domainAll = domain.concat(newDomain); domain = []; for (d of domainAll){ if (domain.indexOf(d) < 0){ domain.push(d); } } } else { domain = domain.concat(newDomain); domain = d3.extent(domain); } } this.scale.domain(domain); return this; } extendRange(newRange: any[]){ let range = this.scale.range(); if (!this.hasRange) { range = newRange; this.hasRange = true; } else { range = range.concat(newRange); range = d3.extent(range); } this.scale.range(range); return this; } mapValue(dataModel, d){ let result; const valueMapper = dataModel[this.key + "ValueMap"]; result = { key: "default", value: this.defaultValue }; if (valueMapper){ result.key = String(valueMapper(d)); result.value = this.scale(result.key); } return result; } get model(){ let d; let item; let attr; const itemArray = []; const domain = this.scale.domain(); const range = this.scale.range(); for (d of domain){ attr = "item-" + convertValueToAttribute(String(d)); item = { key: d, attr, value: this.scale(d), min: range[0], max: range[range.length - 1] }; itemArray.push(item); if (this.containerType === ScaleContainerTypeEnum.AREA){ item.radius = areaToRadius(item.value); item.min = areaToRadius(item.min); item.max = areaToRadius(item.max); item.wMax = item.max * 2; } } return { key: convertValueToAttribute(this.key), itemArray }; } } export function scaleContainerFactory(key: string, containerType: ScaleContainerTypeEnum, drawArea: ChartDrawArea): ScaleContainer { const theme = Theme.getInstance(); let scaleContainer: ScaleContainer; switch (containerType){ case ScaleContainerTypeEnum.COLOR_CATEGORICAL: case ScaleContainerTypeEnum.COLOR_CATEGORICAL_LIGHT: case ScaleContainerTypeEnum.COLOR_CATEGORICAL_DARK: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteQual; scaleContainer.defaultValue = theme.colors.colorDefault; break; case ScaleContainerTypeEnum.COLOR_SEQUENTIAL: case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_LIGHT: case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_DARK: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.LINEAR); scaleContainer.range = [theme.colors.colorSequentialMid, theme.colors.colorSequentialHigh]; scaleContainer.defaultValue = theme.colors.colorSequentialMid; break; case ScaleContainerTypeEnum.COLOR_DIVERGING: case ScaleContainerTypeEnum.COLOR_DIVERGING_LIGHT: case ScaleContainerTypeEnum.COLOR_DIVERGING_DARK: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.LINEAR); scaleContainer.range = [theme.colors.colorSequentialLow, theme.colors.colorSequentialMid, theme.colors.colorSequentialHigh]; scaleContainer.defaultValue = theme.colors.colorSequentialMid; break; case ScaleContainerTypeEnum.COLOR_SEMANTIC_POSITIVE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSemanticPositive; scaleContainer.defaultValue = theme.colors.colorPaletteSemanticPositive[0]; break; case ScaleContainerTypeEnum.COLOR_SEMANTIC_NEGATIVE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSemanticNegative; scaleContainer.defaultValue = theme.colors.colorPaletteSemanticNegative[0]; break; case ScaleContainerTypeEnum.COLOR_SEMANTIC_CAUTION: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSemanticCaution; scaleContainer.defaultValue = theme.colors.colorPaletteSemanticCaution[0]; break; case ScaleContainerTypeEnum.COLOR_SEMANTIC_NEGATIVE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSemanticNeutral; scaleContainer.defaultValue = theme.colors.colorPaletteSemanticNeutral[0]; break; case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_BRAND_BLUE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSequentialBrandBlue; scaleContainer.defaultValue = theme.colors.colorPaletteSequentialBrandBlue[0]; break; case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_PURPLE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSequentialPurple; scaleContainer.defaultValue = theme.colors.colorPaletteSequentialPurple[0]; break; case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_GREEN: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSequentialGreen; scaleContainer.defaultValue = theme.colors.colorPaletteSequentialGreen[0]; break; case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_ORANGE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSequentialOrange; scaleContainer.defaultValue = theme.colors.colorPaletteSequentialOrange[0]; break; case ScaleContainerTypeEnum.COLOR_SEQUENTIAL_GRAY: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.colors.colorPaletteSequentialGray; scaleContainer.defaultValue = theme.colors.colorPaletteSequentialGray[0]; break; case ScaleContainerTypeEnum.AREA: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.LINEAR); scaleContainer.range = theme.getAreaRange(drawArea); scaleContainer.defaultValue = theme.getArea(drawArea); break; case ScaleContainerTypeEnum.SHAPE: scaleContainer = new ScaleContainer(key, ScaleTypeEnum.ORDINAL); scaleContainer.range = theme.shapes; scaleContainer.defaultValue = theme.shapes[0]; break; default: throw new Error("Unknow scale container"); } if (scaleContainer){ scaleContainer.containerType = containerType; } return scaleContainer; } export function buildCustomScaleContainer(key: string, type: ScaleTypeEnum, domain: any[], range: any[], defaultValue?: any): ScaleContainer { const scaleContainer = new ScaleContainer(key, type); scaleContainer.domain = domain; scaleContainer.range = range; if (defaultValue){ scaleContainer.defaultValue = defaultValue; } return scaleContainer; } export function buildCustomScaleObjectContainer(key: string, type: ScaleTypeEnum, scaleObject: any): ScaleContainer { return buildCustomScaleContainer(key, type, scaleObject.domain, scaleObject.range, scaleObject.defaultValue); } /*** * Extends the Y domain in the case of one data point * @param domain Extents calculated by d3, ex: [min, max] * @param offsetPercent The percentage of the diff between man and min to use as padding * @returns If needed an extended domain. */ export function extendNumberDomain(domain, offsetPercent = 0.1){ // max - min to get length of the domain const diff = domain[1] - domain[0]; // Extend domain when the diff is 0 if (diff === 0) { let offset = offsetPercent * domain[0]; // Account for case of 0 values if (offset === 0) { offset = 1; } return [domain[0] - offset, domain[1] + offset]; } else { return domain; } } /*** * Extend the X domain in the case of one data point. * Assumes the Date type * @param domain Extents calculated by d3, ex: [min, max] * @param offsetTime The time in seconds to use as padding * @returns If needed an extended domain. */ export function extendTimeDomain(domain, offsetTime = 1000 * 60 * 60 * 24){ const low = domain[0]; const high = domain[1]; // max - min to get length of the domain const diff = high.getTime() - low.getTime(); // Extend domain when the diff is 0 if (diff === 0) { return [new Date(low.getTime() - offsetTime), new Date(high.getTime() + offsetTime)]; } else { return domain; } }