import { ITree } from "@hpcc-js/api"; import { HTMLWidget, Palette, PropertyExt, Utility } from "@hpcc-js/common"; import { rgb as d3rgb } from "d3-color"; import { hierarchy as d3Hierarchy, treemap as d3Treemap, treemapBinary as d3treemapBinary, treemapDice as d3treemapDice, treemapResquarify as d3treemapResquarify, treemapSlice as d3treemapSlice, treemapSliceDice as d3treemapSliceDice, treemapSquarify as d3treemapSquarify } from "d3-hierarchy"; import "../src/Treemap.css"; export class TreemapColumn extends PropertyExt { _owner: Treemap; constructor() { super(); } owner(): Treemap; owner(_: Treemap): this; owner(_?: Treemap): Treemap | this { if (!arguments.length) return this._owner; this._owner = _; return this; } valid(): boolean { return !!this.column(); } column: { (): string; (_: string): TreemapColumn; }; } TreemapColumn.prototype._class += " tree_Dendrogram.TreemapColumn"; TreemapColumn.prototype.publish("column", null, "set", "Field", function (this: TreemapColumn) { return this._owner ? this._owner.columns() : []; }, { optional: true }); // === export class Treemap extends HTMLWidget { Column; protected _d3Treemap; protected _elementDIV; protected _selection; constructor() { super(); ITree.call(this); Utility.SimpleSelectionMixin.call(this); } private getTilingMethod() { switch (this.tilingMethod()) { case "treemapBinary": return d3treemapBinary; case "treemapDice": return d3treemapDice; case "treemapSlice": return d3treemapSlice; case "treemapSliceDice": return d3treemapSliceDice; case "treemapResquarify": return d3treemapResquarify; case "treemapSquarify": default: return d3treemapSquarify; } } treemapData() { if (!this.mappings().filter(mapping => mapping.valid()).length) { return this.data(); } const view = this._db.aggregateView(this.mappings().map(function (mapping) { return mapping.column(); }), this.aggrType(), this.aggrColumn()); const retVal = { key: "root", values: view.entries() }; return formatData(retVal); function formatData(node): any { if (node.values instanceof Array) { const children = node.values.filter(function (value) { return !(value instanceof Array); }).map(function (value) { return formatData(value); }); const retVal2: any = { label: node.key }; if (children.length) { retVal2.children = children; } else { retVal2.size = 22; } return retVal2; } return { label: node.key, size: node.values.aggregate, origRows: node.values }; } } enter(domNode, element) { super.enter(domNode, element); this._d3Treemap = d3Treemap(); this._elementDIV = element.append("div"); this._selection.widgetElement(this._elementDIV); } update(domNode, element) { super.update(domNode, element); const context = this; this._palette = this._palette.switch(this.paletteID()); if (this.useClonedPalette()) { this._palette = this._palette.cloneNotExists(this.paletteID() + "_" + this.id()); } const root = d3Hierarchy(this.treemapData()) .sum(this.nodeWeight) ; this._d3Treemap .size([this.width(), this.height()]) .paddingInner(this.paddingInner()) .paddingOuter(this.paddingOuter()) .paddingTop(this.paddingTop()) ; if (["treemapSquarify", "treemapResquarify"].indexOf(this.tilingMethod()) !== -1) { this._d3Treemap.tile(this.getTilingMethod()["ratio"](this.squarifyRatio())); } else { this._d3Treemap.tile(this.getTilingMethod()); } this._d3Treemap(root); this._elementDIV .style("font-size", this.fontSize_exists() ? this.fontSize() + "px" : null) .style("line-height", this.fontSize_exists() ? (this.fontSize() + 2) + "px" : null) ; const node = this._elementDIV.selectAll(".node").data(root.descendants()); node.enter().append("div") .attr("class", "node") .call(this._selection.enter.bind(this._selection)) .on("click", function (d) { if (d && d.origRows) { let columnLabel = ""; context.mappings().forEach(function (mapping) { if (mapping.column()) { columnLabel = mapping.column(); } }); context.click(context.rowToObj(d.origRows[0]), columnLabel, context._selection.selected(this)); } }) .on("dblclick", function (d) { if (d && d.origRows) { let columnLabel = ""; context.mappings().forEach(function (mapping) { if (mapping.column()) { columnLabel = mapping.column(); } }); context.dblclick(context.rowToObj(d.origRows[0]), columnLabel, context._selection.selected(this)); } }) .merge(node) .style("left", function (d) { return (d.x0 + Math.max(0, d.x1 - d.x0) / 2) + "px"; }) .style("top", function (d) { return (d.y0 + Math.max(0, d.y1 - d.y0) / 2) + "px"; }) .style("width", function () { return 0 + "px"; }) .style("height", function () { return 0 + "px"; }) .style("font-size", function (d) { return (d.children ? context.parentFontSize() : context.leafFontSize()) + "px"; }) .style("line-height", function (d) { return (d.children ? context.parentFontSize() : context.leafFontSize()) + "px"; }) .attr("title", tooltip) .html(function (d) { if (!context.showRoot() && d.depth === 0) { return null; } if (d.children) { if (context.enableParentLabels()) { return context.parentWeightHTML(d); } else { return null; } } else { return context.leafWeightHTML(d); } }) .style("background", function (d) { if (!context.showRoot() && d.depth === 0) { this.style.color = "transparent"; return "transparent"; } const light_dark = context.brighterLeafNodes() ? "brighter" : "darker"; let _color; if (context.usePaletteOnParentNodes()) { _color = d.children ? context._palette(d.data.label) : d3rgb(context._palette(d.parent.data.label))[light_dark](1); } else { if (d.depth > context.depthColorLimit()) { _color = d3rgb(d.parent.color)[light_dark](1); } else { _color = context._palette(d.data.label); } d.color = _color; } this.style.color = Palette.textColor(_color); return _color; }) .transition().duration(this.transitionDuration()) .style("pointer-events", function (d) { return !context.showRoot() && d.depth === 0 ? "none" : "all"; }) .style("opacity", function (d) { return d.children ? 1 : null; }) .style("left", function (d) { return d.x0 + "px"; }) .style("top", function (d) { return d.y0 + "px"; }) .style("width", function (d) { return Math.max(0, d.x1 - d.x0) + "px"; }) .style("height", function (d) { return Math.max(0, d.y1 - d.y0) + "px"; }) .each(function (d) { if (d.depth === 0) { this.style.color = !context.showRoot() ? "transparent" : ""; this.style.borderColor = !context.showRoot() ? "transparent" : ""; } }) ; node.exit().transition().duration(this.transitionDuration()) .style("opacity", 0) .remove() ; function tooltip(d) { if (d.children && !context.enableParentTooltips()) { return null; } let retVal = d.data.label + " (" + d.value + ")"; while (d.parent && d.parent.parent) { retVal = d.parent.data.label + " -> " + retVal; d = d.parent; } return retVal; } } exit(domNode, element) { super.exit(domNode, element); } nodeWeight(d) { return d.size || 1; } parentWeightHTML(d) { return this.showParentWeight() ? `${d.data.label}${d.value}${this.weightSuffix()}` : `${d.data.label}`; } leafWeightHTML(d) { return this.showLeafWeight() ? `${d.data.label}${d.value}${this.weightSuffix()}` : `${d.data.label}`; } paletteID: { (): string[]; (_: string[]): Treemap; }; useClonedPalette: { (): boolean[]; (_: boolean[]): Treemap; }; mappings: { (): TreemapColumn[]; (_: TreemapColumn[]): Treemap; }; aggrType: { (): string; (_: string): Treemap; }; aggrColumn: { (): string; (_: string): Treemap; }; fontSize: { (): number; (_: number): Treemap; }; fontSize_exists: () => boolean; paddingInner: { (): number; (_: number): Treemap; }; paddingOuter: { (): number; (_: number): Treemap; }; paddingTop: { (): number; (_: number): Treemap; }; parentFontSize: { (): number; (_: number): Treemap; }; leafFontSize: { (): number; (_: number): Treemap; }; brighterLeafNodes: { (): boolean; (_: boolean): Treemap; }; showRoot: { (): boolean; (_: boolean): Treemap; }; enableParentLabels: { (): boolean; (_: boolean): Treemap; }; enableParentTooltips: { (): boolean; (_: boolean): Treemap; }; showParentWeight: { (): boolean; (_: boolean): Treemap; }; showLeafWeight: { (): boolean; (_: boolean): Treemap; }; usePaletteOnParentNodes: { (): boolean; (_: boolean): Treemap; }; depthColorLimit: { (): number; (_: number): Treemap; }; squarifyRatio: { (): number; (_: number): Treemap; }; weightSuffix: { (): string; (_: string): Treemap; }; tilingMethod: { (): string; (_: string): Treemap; }; transitionDuration: { (): number[]; (_: number[]): Treemap; }; // ITree _palette; click: (row, column, selected) => void; dblclick: (row, column, selected) => void; } Treemap.prototype._class += " tree_Treemap"; Treemap.prototype.implements(ITree.prototype); Treemap.prototype.mixin(Utility.SimpleSelectionMixin); Treemap.prototype.Column = TreemapColumn; Treemap.prototype.publish("paletteID", "default", "set", "Color palette for this widget", Treemap.prototype._palette.switch(), { tags: ["Basic", "Shared"] }); Treemap.prototype.publish("useClonedPalette", false, "boolean", "Enable or disable using a cloned palette", null, { tags: ["Intermediate", "Shared"] }); Treemap.prototype.publish("mappings", [], "propertyArray", "Source Columns", null, { autoExpand: TreemapColumn }); Treemap.prototype.publish("aggrType", null, "set", "Aggregation Type", [null, "mean", "median", "sum", "min", "max"], { optional: true }); Treemap.prototype.publish("aggrColumn", null, "set", "Aggregation Field", function () { return this.columns(); }, { optional: true, disable: (w) => !w.aggrType() }); Treemap.prototype.publish("fontSize", null, "number", "Font Size", null, { optional: true }); Treemap.prototype.publish("paddingInner", 18.6, "number", "Pixel spacing between each sibling node"); Treemap.prototype.publish("paddingOuter", 30, "number", "Pixel padding of parent nodes"); Treemap.prototype.publish("paddingTop", 41.4, "number", "Additional top pixel padding of parent nodes"); Treemap.prototype.publish("showRoot", false, "boolean", "Show root element"); Treemap.prototype.publish("parentFontSize", 18, "number", "Parent font-size"); Treemap.prototype.publish("leafFontSize", 16, "number", "Leaf font-size"); Treemap.prototype.publish("usePaletteOnParentNodes", false, "boolean", "Assign a color from the palette to every parent node"); Treemap.prototype.publish("depthColorLimit", 1, "number", "Assign a color from the palette to node with depth lower than this value", null, { optional: true, disable: (w) => w.usePaletteOnParentNodes() }); Treemap.prototype.publish("squarifyRatio", 1, "number", "Specifies the desired aspect ratio of the generated rectangles (must be >= 1)", null, { optional: true, disable: (w) => ["treemapSquarify", "treemapResquarify"].indexOf(w.tilingMethod()) === -1 }); Treemap.prototype.publish("showParentWeight", true, "boolean", "Show weight of parent nodes"); Treemap.prototype.publish("showLeafWeight", true, "boolean", "Show weight of leaf nodes"); Treemap.prototype.publish("weightSuffix", "", "string", "Weight suffix (ex: 'ms')"); Treemap.prototype.publish("brighterLeafNodes", false, "boolean", "Brighter/darker leaf node color (false = darker)"); Treemap.prototype.publish("enableParentLabels", true, "boolean", "Enable parent labels"); Treemap.prototype.publish("enableParentTooltips", true, "boolean", "Enable parent tooltips"); Treemap.prototype.publish("transitionDuration", 250, "number", "Transition Duration"); Treemap.prototype.publish("tilingMethod", "treemapSquarify", "set", "Transition Duration", ["treemapBinary", "treemapDice", "treemapResquarify", "treemapSlice", "treemapSliceDice", "treemapSquarify"]);