import { format as d3Format, HTMLWidget, Palette } from "@hpcc-js/common"; import { QuartileCandlestick } from "./QuartileCandlestick.ts"; import { Scatter } from "./Scatter.ts"; const rainbow = Palette.rainbow("Blues"); const palette = Palette.ordinal("Quartile", [rainbow(100, 0, 100), rainbow(50, 0, 100), rainbow(50, 0, 100), rainbow(75, 0, 100)]); palette("Std. Dev."); palette("MinMax"); palette("25%"); palette("50%"); type View = "min_max" | "25_75" | "normal"; type Tick = { label: string, value: number }; type Ticks = Tick[]; type AxisTick = { label: string, value: string }; type AxisTicks = AxisTick[]; function myFormatter(format: string): (num: number) => string { const formatter = d3Format(format); return function (num: number) { const strVal = (Math.round(num * 100) / 100).toString(); if (strVal.length <= 4) return strVal; return formatter(num); }; } export type StatChartView = "min_max" | "25_75" | "normal"; export type Quartiles = [number, number, number, number, number]; export type Data = [[number, number, number, number, number, number, number]]; export class StatChart extends HTMLWidget { protected _selectElement: any; protected _tickFormatter: (_: number) => string; protected _bellCurve: Scatter = new Scatter() .columns(["", "Std. Dev."]) .paletteID("Quartile") .interpolate_default("basis") .pointSize(0) .xAxisType("linear") .xAxisOverlapMode("none") .xAxisTickFormat(",") .yAxisHidden(true) .yAxisDomainLow(0) .yAxisDomainHigh(110) .yAxisGuideLines(false) as Scatter ; protected _candle = new QuartileCandlestick() .columns(["Min", "25%", "50%", "75%", "Max"]) .edgePadding(0) .roundedCorners(1) .lineWidth(1) .upperTextRotation(-90) .lowerTextRotation(-90) .labelFontSize(0) .valueFontSize(0) .lineColor(rainbow(90, 0, 100)) .innerRectColor(rainbow(10, 0, 100)) ; constructor() { super(); this .columns(["Min", "25%", "50%", "75%", "Max", "Mean", "Std. Dev."]) ; } protected stdDev(degrees: number): number { return this.mean() + degrees * this.standardDeviation(); } protected formatStdDev(degrees: number): string { return this._tickFormatter(this.stdDev(degrees)); } protected quartile(q: 0 | 1 | 2 | 3 | 4): number { return this.quartiles()[q]; } protected formatQ(q: 0 | 1 | 2 | 3 | 4): string { return this._tickFormatter(this.quartile(q)); } protected domain(mode: View): [number, number] { switch (mode) { case "25_75": return [this.quartile(1), this.quartile(3)]; case "normal": return [this.stdDev(-4), this.stdDev(4)]; case "min_max": default: return [this.quartile(0), this.quartile(4)]; } } protected min(): number { return this.quartile(0); } protected max(): number { return this.quartile(4); } data(): Data; data(_: Data): this; data(_?: Data): Data | this { if (!arguments.length) return [[...this.quartiles(), this.mean(), this.standardDeviation()]]; const row = _[0]; this.quartiles([row[0], row[1], row[2], row[3], row[4]]); this.mean(row[5]); this.standardDeviation(row[6]); return this; } enter(domNode, element) { super.enter(domNode, element); this._bellCurve.target(element.append("div").node()); this._candle.target(element.append("div").node()); this._selectElement = element.append("div") .style("position", "absolute") .style("top", "0px") .style("right", "0px").append("select") .on("change", () => { this.view(this._selectElement.node().value); this.lazyRender(); }) ; this._selectElement.append("option").attr("value", "min_max").text("Min / Max"); this._selectElement.append("option").attr("value", "25_75").text("25% / 75%"); this._selectElement.append("option").attr("value", "normal").text("Normal"); } protected bellTicks(mode: View): AxisTicks { let ticks: Ticks; switch (mode) { case "25_75": ticks = [ { label: this.formatQ(1), value: this.quartile(1) }, { label: this.formatQ(2), value: this.quartile(2) }, { label: this.formatQ(3), value: this.quartile(3) } ]; break; case "normal": ticks = [ { label: this.formatStdDev(-4), value: this.stdDev(-4) }, { label: "-3σ", value: this.stdDev(-3) }, { label: "-2σ", value: this.stdDev(-2) }, { label: "-1σ", value: this.stdDev(-1) }, { label: this.formatStdDev(0), value: this.stdDev(0) }, { label: "+1σ", value: this.stdDev(1) }, { label: "+2σ", value: this.stdDev(2) }, { label: "+3σ", value: this.stdDev(3) }, { label: this.formatStdDev(4), value: this.stdDev(4) } ]; break; case "min_max": default: ticks = [ { label: this.formatQ(0), value: this.quartile(0) }, { label: this.formatQ(1), value: this.quartile(1) }, { label: this.formatQ(2), value: this.quartile(2) }, { label: this.formatQ(3), value: this.quartile(3) }, { label: this.formatQ(4), value: this.quartile(4) } ]; } const [domainLow, domainHigh] = this.domain(this._selectElement.node().value); return ticks .filter(sd => sd.value >= domainLow && sd.value <= domainHigh) .map(sd => ({ label: sd.label, value: sd.value.toString() })) ; } updateScatter() { const mode = this._selectElement.node().value; const [domainLow, domainHigh] = this.domain(mode); const padding = (domainHigh - domainLow) * (this.domainPadding() / 100); this._bellCurve .xAxisDomainLow(domainLow - padding) .xAxisDomainHigh(domainHigh + padding) .xAxisTicks(this.bellTicks(mode)) .data([ [this.stdDev(-4), 0], [this.stdDev(-3), 0.3], [this.stdDev(-2), 5], [this.stdDev(-1), 68], [this.stdDev(0), 100], [this.stdDev(1), 68], [this.stdDev(2), 5], [this.stdDev(3), 0.3], [this.stdDev(4), 0] ]) .resize({ width: this.width(), height: this.height() - this.candleHeight() }) .render() ; } updateCandle() { const candleX = this._bellCurve.dataPos(this.quartile(0)); const candleW = this._bellCurve.dataPos(this.quartile(4)) - candleX; this._candle .resize({ width: this.width(), height: this.candleHeight() }) .pos({ x: (candleX + candleW / 2) + 2, y: this.candleHeight() / 2 }) .width(candleW) .candleWidth(this.candleHeight()) .data(this.quartiles()) .render() ; } update(domNode, element) { super.update(domNode, element); this._tickFormatter = myFormatter(this.tickFormat()); this._selectElement.node().value = this.view(); this.updateScatter(); this.updateCandle(); } exit(domNode, element) { this._bellCurve.target(null); this._candle.target(null); this._selectElement.remove(); super.exit(domNode, element); } } StatChart.prototype._class += " chart_StatChart"; export interface StatChart { view(): StatChartView; view(_: StatChartView): this; tickFormat(): string; tickFormat(_: string): this; candleHeight(): number; candleHeight(_: number): this; domainPadding(): number; domainPadding(_: number): this; mean(): number; mean(_: number): this; standardDeviation(): number; standardDeviation(_: number): this; quartiles(): Quartiles; quartiles(_: Quartiles): this; } StatChart.prototype.publish("view", "min_max", "set", "View", ["min_max", "25_75", "normal"]); StatChart.prototype.publish("tickFormat", ".2e", "string", "X-Axis Tick Format"); StatChart.prototype.publish("candleHeight", 20, "number", "Height of candle widget (pixels)"); StatChart.prototype.publish("domainPadding", 10, "number", "Domain value padding"); StatChart.prototype.publish("mean", .5, "number", "Mean"); StatChart.prototype.publish("standardDeviation", .125, "number", "Standard Deviation (σ)"); StatChart.prototype.publish("quartiles", [0, .25, .5, .75, 1], "object", "Quartiles (Min, 25%, 50%, 75%, Max)");