in src/visual.ts [385:690]
private updateInternal(options: VisualUpdateOptions, settings: Settings): void {
let dataView: DataView = this.dataView = options.dataViews[0];
let chartData: TableHeatMapChartData = this.converter(dataView, this.colors);
let suppressAnimations: boolean = false;
if (chartData.dataPoints) {
let minDataValue: number = d3.min(chartData.dataPoints, function (d: TableHeatMapDataPoint) {
return d.value as number;
});
let maxDataValue: number = d3.max(chartData.dataPoints, function (d: TableHeatMapDataPoint) {
return d.value as number;
});
let numBuckets: number = settings.general.buckets;
let colorbrewerScale: string = settings.general.colorbrewer;
let colorbrewerEnable: boolean = settings.general.enableColorbrewer;
let colors: Array<string>;
if (colorbrewerEnable) {
if (colorbrewerScale) {
let currentColorbrewer: IColorArray = colorbrewer[colorbrewerScale];
colors = (currentColorbrewer ? currentColorbrewer[numBuckets] : colorbrewer.Reds[numBuckets]);
}
else {
colors = colorbrewer.Reds[numBuckets]; // default color scheme
}
} else {
let startColor: string = settings.general.gradientStart;
let endColor: string = settings.general.gradientEnd;
let colorScale: LinearColorScale = createLinearColorScale([0, numBuckets], [startColor, endColor], true);
colors = [];
for (let bucketIndex: number = 0; bucketIndex < numBuckets; bucketIndex++) {
colors.push(colorScale(bucketIndex));
}
}
let colorScale: Quantile<string> = d3.scaleQuantile<string>()
.domain([minDataValue, maxDataValue])
.range(colors);
let xAxisHeight: number = this.getXAxisHeight(chartData);
let yAxisWidth: number = this.getYAxisWidth(chartData);
let yAxisHeight: number = this.getYAxisHeight(chartData);
if (!settings.yAxisLabels.show) {
yAxisWidth = 0;
}
if (!settings.xAxisLabels.show) {
xAxisHeight = 0;
}
let maxDataText: string = chartData.dataPoints[0].valueStr || "";
chartData.dataPoints.forEach((value: TableHeatMapDataPoint) => {
if ((value.valueStr || "").length > maxDataText.length) {
maxDataText = value.valueStr || "";
}
});
let textProperties: TextProperties = {
fontSize: PixelConverter.toString(settings.labels.fontSize),
fontFamily: settings.labels.fontFamily,
text: maxDataText
};
let textRect: SVGRect = TextMeasurementService.measureSvgTextRect(textProperties);
let gridSizeWidth: number = Math.floor((this.viewport.width - yAxisWidth) / (chartData.categoryX.length));
let gridSizeHeight: number = gridSizeWidth * TableHeatMap.ConstGridHeightWidthRaito;
if (gridSizeWidth < textRect.width && settings.labels.show) {
gridSizeWidth = textRect.width;
}
if (gridSizeHeight < textRect.height && settings.labels.show) {
gridSizeHeight = textRect.height;
}
if (gridSizeHeight > TableHeatMap.CellMaxHeightLimit) {
gridSizeHeight = TableHeatMap.CellMaxHeightLimit;
}
if (gridSizeWidth > gridSizeHeight * TableHeatMap.CellMaxWidthFactorLimit) {
gridSizeWidth = gridSizeHeight * TableHeatMap.CellMaxWidthFactorLimit;
}
if (gridSizeHeight < TableHeatMap.ConstGridMinHeight) {
gridSizeHeight = TableHeatMap.ConstGridMinHeight;
}
if (gridSizeWidth < TableHeatMap.ConstGridMinWidth) {
gridSizeWidth = TableHeatMap.ConstGridMinWidth;
}
let xOffset: number = this.margin.left + yAxisWidth; // add widht of y labels width
let yOffset: number = this.margin.top + xAxisHeight; // todo add height of x categoru labels height
const TableHeatMapCellRaito: number = 2 / 3;
let legendElementWidth: number = (this.viewport.width * TableHeatMapCellRaito - xOffset) / numBuckets;
let legendElementHeight: number = gridSizeHeight;
if (settings.yAxisLabels.show) {
let categoryYElements: d3.Selection<d3.BaseType, any, any, any> = this.mainGraphics.selectAll("." + TableHeatMap.ClsCategoryYLabel);
let categoryYElementsData = categoryYElements
.data(chartData.categoryY);
let categoryYElementsEntered = categoryYElementsData
.enter()
.append(TableHeatMap.HtmlObjText);
categoryYElementsEntered.exit().remove();
let categoryYElementsMerged = categoryYElementsEntered.merge(categoryYElements);
categoryYElementsMerged
.text((d: string) => {
return TableHeatMap.textLimit(d, settings.yAxisLabels.maxTextSymbol);
})
.attr(TableHeatMap.AttrDY, TableHeatMap.Const071em)
.attr(TableHeatMap.AttrX, this.margin.left)
.attr(TableHeatMap.AttrY, function (d, i) {
return i * gridSizeHeight - (gridSizeHeight / 2) + yOffset - yAxisHeight / 3;
})
.style(TableHeatMap.StTextAnchor, TableHeatMap.ConstBegin)
.style("font-size", settings.yAxisLabels.fontSize)
.style("font-family", settings.yAxisLabels.fontFamily)
.style("fill", settings.yAxisLabels.fill)
.attr(TableHeatMap.AttrTransform, translate(TableHeatMap.ConstShiftLabelFromGrid, gridSizeHeight))
.classed(TableHeatMap.ClsCategoryYLabel, true)
.classed(TableHeatMap.ClsMono, true)
.classed(TableHeatMap.ClsAxis, true);
this.mainGraphics.selectAll("." + TableHeatMap.ClsCategoryYLabel)
.call(this.wrap, gridSizeWidth + xOffset);
this.truncateTextIfNeeded(this.mainGraphics.selectAll("." + TableHeatMap.ClsCategoryYLabel), gridSizeWidth + xOffset);
}
if (settings.xAxisLabels.show) {
let categoryXElements: d3.Selection<d3.BaseType, any, any, any> = this.mainGraphics.selectAll("." + TableHeatMap.ClsCategoryXLabel);
let categoryXElementsData = categoryXElements
.data(chartData.categoryX);
categoryXElementsData.exit().remove();
let categoryXElementsEntered = categoryXElementsData
.enter().append(TableHeatMap.HtmlObjText);
let categoryXElementsMerged = categoryXElementsEntered.merge(categoryXElements);
categoryXElementsMerged
.text(function (d: string) {
return chartData.categoryValueFormatter.format(d);
})
.attr(TableHeatMap.AttrX, function (d: string, i: number) {
return i * gridSizeWidth + xOffset;
})
.attr(TableHeatMap.AttrY, xAxisHeight / 2)
.attr(TableHeatMap.AttrDY, TableHeatMap.Const0em)
.style(TableHeatMap.StTextAnchor, TableHeatMap.ConstMiddle)
.style("font-size", settings.xAxisLabels.fontSize)
.style("font-family", settings.xAxisLabels.fontFamily)
.style("fill", settings.xAxisLabels.fill)
.classed(TableHeatMap.ClsCategoryXLabel + " " + TableHeatMap.ClsMono + " " + TableHeatMap.ClsAxis, true)
.attr(TableHeatMap.AttrTransform, translate(gridSizeHeight, TableHeatMap.ConstShiftLabelFromGrid));
this.truncateTextIfNeeded(this.mainGraphics.selectAll("." + TableHeatMap.ClsCategoryXLabel), gridSizeWidth);
}
let heatMap: Selection<TableHeatMapDataPoint> = this.mainGraphics.selectAll("." + TableHeatMap.ClsCategoryX);
let heatMapData = heatMap
.data(chartData.dataPoints);
let heatMapEntered = heatMapData
.enter()
.append(TableHeatMap.HtmlObjRect);
let heatMapMerged = heatMapEntered.merge(heatMap);
heatMapMerged
.attr(TableHeatMap.AttrX, function (d: TableHeatMapDataPoint) {
return chartData.categoryX.indexOf(d.categoryX) * gridSizeWidth + xOffset;
})
.attr(TableHeatMap.AttrY, function (d: TableHeatMapDataPoint) {
return chartData.categoryY.indexOf(d.categoryY) * gridSizeHeight + yOffset;
})
.classed(TableHeatMap.ClsCategoryX + " " + TableHeatMap.ClsBordered, true)
.attr(TableHeatMap.AttrWidth, gridSizeWidth)
.attr(TableHeatMap.AttrHeight, gridSizeHeight)
.style(TableHeatMap.StFill, colors[0])
.style("stroke", settings.general.stroke);
if (chartData.categoryX.length * gridSizeWidth + xOffset > options.viewport.width) {
this.svg.attr("width", chartData.categoryX.length * gridSizeWidth);
}
// add data labels
let textHeight: number = textRect.height;
let heatMapDataLables: Selection<TableHeatMapDataPoint> = this.mainGraphics.selectAll("." + TableHeatMap.CLsHeatMapDataLabels);
if (settings.labels.show && textHeight <= gridSizeHeight) {
let heatMapDataLablesData: Selection<TableHeatMapDataPoint> = heatMapDataLables.data(chartData.dataPoints);
heatMapDataLables.exit().remove();
let heatMapDataLablesEntered = heatMapDataLablesData
.enter().append("text");
heatMapDataLablesEntered
.classed(TableHeatMap.CLsHeatMapDataLabels, true)
.attr(TableHeatMap.AttrX, function (d: TableHeatMapDataPoint) {
return chartData.categoryX.indexOf(d.categoryX) * gridSizeWidth + xOffset + gridSizeWidth / 2;
})
.attr(TableHeatMap.AttrY, function (d: TableHeatMapDataPoint) {
return chartData.categoryY.indexOf(d.categoryY) * gridSizeHeight + yOffset + gridSizeHeight / 2 + textHeight / 2.6;
})
.style("text-anchor", TableHeatMap.ConstMiddle)
.style("font-size", this.settings.labels.fontSize)
.style("font-family", this.settings.labels.fontFamily)
.style("fill", this.settings.labels.fill)
.text((dataPoint: TableHeatMapDataPoint) => {
let textValue: string = ValueFormatter.format(dataPoint.value);
textProperties.text = textValue;
textValue = TextMeasurementService.getTailoredTextOrDefault(textProperties, gridSizeWidth);
return dataPoint.value === 0 ? 0 : textValue;
});
}
let elementAnimation: Selection<D3Element> = <Selection<D3Element>>this.getAnimationMode(heatMapMerged, suppressAnimations);
if (!this.settings.general.fillNullValuesCells) {
heatMapMerged.style(TableHeatMap.StOpacity, function (d: any) {
return d.value === null ? 0 : 1;
});
}
elementAnimation.style(TableHeatMap.StFill, function (d: any) {
return <string>colorScale(d.value);
});
this.tooltipServiceWrapper.addTooltip(heatMapMerged, (tooltipEvent: TooltipEventArgs<TooltipEnabledDataPoint>) => {
return tooltipEvent.data.tooltipInfo;
});
// legend
let legendDataValues = [minDataValue].concat(colorScale.quantiles());
let legendData = legendDataValues.concat(maxDataValue).map((value, index) => {
return {
value: value,
tooltipInfo: [{
displayName: `Min value`,
value: value && typeof value.toFixed === "function" ? value.toFixed(0) : chartData.categoryValueFormatter.format(value)
},
{
displayName: `Max value`,
value: legendDataValues[index + 1] && typeof legendDataValues[index + 1].toFixed === "function" ? legendDataValues[index + 1].toFixed(0) : chartData.categoryValueFormatter.format(maxDataValue)
}]
};
});
let legendSelection: Selection<any> = this.mainGraphics.selectAll("." + TableHeatMap.ClsLegend);
let legendSelectionData = legendSelection.data(legendData);
let legendSelectionEntered = legendSelectionData
.enter()
.append(TableHeatMap.HtmlObjG);
legendSelectionData.exit().remove();
let legendSelectionMerged = legendSelectionData.merge(legendSelection);
legendSelectionMerged.classed(TableHeatMap.ClsLegend, true);
let legendOffsetCellsY: number = this.margin.top
+ gridSizeHeight * (chartData.categoryY.length + TableHeatMap.ConstLegendOffsetFromChartByY)
+ xAxisHeight;
let legendOffsetTextY: number = this.margin.top
- gridSizeHeight / 2
+ gridSizeHeight * (chartData.categoryY.length + TableHeatMap.ConstLegendOffsetFromChartByY)
+ legendElementHeight * 2
+ xAxisHeight;
legendSelectionEntered
.append(TableHeatMap.HtmlObjRect)
.attr(TableHeatMap.AttrX, function (d, i) {
return legendElementWidth * i + xOffset;
})
.attr(TableHeatMap.AttrY, legendOffsetCellsY)
.attr(TableHeatMap.AttrWidth, legendElementWidth)
.attr(TableHeatMap.AttrHeight, legendElementHeight)
.style(TableHeatMap.StFill, function (d, i) {
return colors[i];
})
.style("stroke", settings.general.stroke)
.style("opacity", (d) => d.value !== maxDataValue ? 1 : 0)
.classed(TableHeatMap.ClsBordered, true);
legendSelectionEntered
.append(TableHeatMap.HtmlObjText)
.classed(TableHeatMap.ClsMono, true)
.attr(TableHeatMap.AttrX, function (d, i) {
return legendElementWidth * i + xOffset;
})
.attr(TableHeatMap.AttrY, legendOffsetTextY)
.text(function (d) {
return chartData.valueFormatter.format(d.value);
})
.style("fill", settings.general.textColor);
this.tooltipServiceWrapper.addTooltip(
legendSelectionEntered,
(tooltipEvent: TooltipEventArgs<TooltipEnabledDataPoint>) => {
return tooltipEvent.data.tooltipInfo;
}
);
if (legendOffsetTextY + gridSizeHeight > options.viewport.height) {
this.svg.attr("height", legendOffsetTextY + gridSizeHeight);
}
}
}