mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-19 02:37:25 +00:00
269 lines
8.1 KiB
TypeScript
269 lines
8.1 KiB
TypeScript
|
import * as Plotly from "plotly.js-cartesian-dist-min";
|
||
|
import dayjs from "dayjs";
|
||
|
import {
|
||
|
ChartSettings,
|
||
|
DataPayload,
|
||
|
DisplaySettings,
|
||
|
FontSettings,
|
||
|
HeatmapCaptions,
|
||
|
HeatmapData,
|
||
|
LayoutSettings,
|
||
|
PartitionTimeStampToData,
|
||
|
PortalTheme
|
||
|
} from "./HeatmapDatatypes";
|
||
|
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||
|
import { MessageHandler } from "../../Common/MessageHandler";
|
||
|
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||
|
import { StyleConstants } from "../../Common/Constants";
|
||
|
import "./Heatmap.less";
|
||
|
|
||
|
export class Heatmap {
|
||
|
public static readonly elementId: string = "heatmap";
|
||
|
|
||
|
private _chartData: HeatmapData;
|
||
|
private _heatmapCaptions: HeatmapCaptions;
|
||
|
private _theme: PortalTheme;
|
||
|
private _defaultFontColor: string;
|
||
|
|
||
|
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
|
||
|
this._theme = theme;
|
||
|
this._defaultFontColor = StyleConstants.BaseDark;
|
||
|
this._setThemeColorForChart();
|
||
|
this._chartData = this.generateMatrixFromMap(data);
|
||
|
this._heatmapCaptions = heatmapCaptions;
|
||
|
}
|
||
|
|
||
|
private _setThemeColorForChart() {
|
||
|
if (isDarkTheme(this._theme)) {
|
||
|
this._defaultFontColor = StyleConstants.BaseLight;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color: string = "#838383"): FontSettings {
|
||
|
return {
|
||
|
family: StyleConstants.DataExplorerFont,
|
||
|
size,
|
||
|
color
|
||
|
};
|
||
|
}
|
||
|
|
||
|
public generateMatrixFromMap(data: DataPayload): HeatmapData {
|
||
|
// all keys in data payload, sorted...
|
||
|
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
|
||
|
if (parseInt(a) < parseInt(b)) {
|
||
|
return -1;
|
||
|
} else {
|
||
|
if (parseInt(a) > parseInt(b)) {
|
||
|
return 1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
const output: HeatmapData = {
|
||
|
yAxisPoints: [],
|
||
|
dataPoints: [],
|
||
|
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
|
||
|
if (a < b) {
|
||
|
return -1;
|
||
|
} else {
|
||
|
if (a > b) {
|
||
|
return 1;
|
||
|
} else {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
};
|
||
|
// go thru all rows and create 2d matrix for heatmap...
|
||
|
for (let i = 0; i < rows.length; i++) {
|
||
|
output.yAxisPoints.push(rows[i]);
|
||
|
let dataPoints: number[] = [];
|
||
|
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
||
|
let row: PartitionTimeStampToData = data[rows[i]];
|
||
|
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
||
|
}
|
||
|
output.dataPoints.push(dataPoints);
|
||
|
}
|
||
|
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
||
|
const dateTime = output.xAxisPoints[a];
|
||
|
// convert to local users timezone...
|
||
|
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
|
||
|
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
|
||
|
// coerce to ISOString format since that is what plotly wants...
|
||
|
output.xAxisPoints[a] = `${day}T${hour}Z`;
|
||
|
}
|
||
|
return output;
|
||
|
}
|
||
|
|
||
|
private _getChartSettings(): ChartSettings[] {
|
||
|
return [
|
||
|
{
|
||
|
z: this._chartData.dataPoints,
|
||
|
type: "heatmap",
|
||
|
zmin: 0,
|
||
|
zmid: 50,
|
||
|
zmax: 100,
|
||
|
colorscale: [
|
||
|
[0.0, "#1FD338"],
|
||
|
[0.1, "#1CAD2F"],
|
||
|
[0.2, "#50A527"],
|
||
|
[0.3, "#719F21"],
|
||
|
[0.4, "#95991B"],
|
||
|
[0.5, "#CE8F11"],
|
||
|
[0.6, "#E27F0F"],
|
||
|
[0.7, "#E46612"],
|
||
|
[0.8, "#E64914"],
|
||
|
[0.9, "#B80016"],
|
||
|
[1.0, "#B80016"]
|
||
|
],
|
||
|
name: "",
|
||
|
hovertemplate: this._heatmapCaptions.tooltipText,
|
||
|
colorbar: {
|
||
|
thickness: 15,
|
||
|
outlinewidth: 0,
|
||
|
tickcolor: StyleConstants.BaseDark,
|
||
|
tickfont: this._getFontStyles(10, this._defaultFontColor)
|
||
|
},
|
||
|
y: this._chartData.yAxisPoints,
|
||
|
x: this._chartData.xAxisPoints
|
||
|
}
|
||
|
];
|
||
|
}
|
||
|
|
||
|
private _getLayoutSettings(): LayoutSettings {
|
||
|
return {
|
||
|
margin: {
|
||
|
l: 40,
|
||
|
r: 10,
|
||
|
b: 35,
|
||
|
t: 30,
|
||
|
pad: 0
|
||
|
},
|
||
|
paper_bgcolor: "transparent",
|
||
|
plot_bgcolor: "transparent",
|
||
|
width: 462,
|
||
|
height: 240,
|
||
|
yaxis: {
|
||
|
title: this._heatmapCaptions.yAxisTitle,
|
||
|
titlefont: this._getFontStyles(11),
|
||
|
autorange: true,
|
||
|
showgrid: false,
|
||
|
zeroline: false,
|
||
|
showline: false,
|
||
|
autotick: true,
|
||
|
fixedrange: true,
|
||
|
ticks: "",
|
||
|
showticklabels: false
|
||
|
},
|
||
|
xaxis: {
|
||
|
fixedrange: true,
|
||
|
title: "*White area in heatmap indicates there is no available data",
|
||
|
titlefont: this._getFontStyles(11),
|
||
|
autorange: true,
|
||
|
showgrid: false,
|
||
|
zeroline: false,
|
||
|
showline: false,
|
||
|
autotick: true,
|
||
|
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
|
||
|
showticklabels: true,
|
||
|
tickfont: this._getFontStyles(10)
|
||
|
},
|
||
|
title: {
|
||
|
text: this._heatmapCaptions.chartTitle,
|
||
|
x: 0.01,
|
||
|
font: this._getFontStyles(13, this._defaultFontColor)
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
private _getChartDisplaySettings(): DisplaySettings {
|
||
|
return {
|
||
|
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
|
||
|
responsive: true,*/
|
||
|
displayModeBar: false
|
||
|
};
|
||
|
}
|
||
|
|
||
|
public drawHeatmap(): void {
|
||
|
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
|
||
|
Plotly.plot(
|
||
|
Heatmap.elementId,
|
||
|
this._getChartSettings(),
|
||
|
this._getLayoutSettings(),
|
||
|
this._getChartDisplaySettings()
|
||
|
);
|
||
|
let plotDiv: any = document.getElementById(Heatmap.elementId);
|
||
|
plotDiv.on("plotly_click", (data: any) => {
|
||
|
let timeSelected: string = data.points[0].x;
|
||
|
timeSelected = timeSelected.replace(" ", "T");
|
||
|
timeSelected = `${timeSelected}Z`;
|
||
|
let xAxisIndex;
|
||
|
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
||
|
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
||
|
xAxisIndex = i;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
let output = [];
|
||
|
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
||
|
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
||
|
}
|
||
|
MessageHandler.sendCachedDataMessage(MessageTypes.LogInfo, output);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function isDarkTheme(theme: PortalTheme) {
|
||
|
return theme === PortalTheme.dark;
|
||
|
}
|
||
|
|
||
|
export function handleMessage(event: MessageEvent) {
|
||
|
if (isInvalidParentFrameOrigin(event)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
|
||
|
return;
|
||
|
}
|
||
|
if (
|
||
|
typeof event.data.data !== "object" ||
|
||
|
!("chartData" in event.data.data) ||
|
||
|
!("chartSettings" in event.data.data)
|
||
|
) {
|
||
|
return;
|
||
|
}
|
||
|
Plotly.purge(Heatmap.elementId);
|
||
|
document.getElementById(Heatmap.elementId).innerHTML = "";
|
||
|
const data = event.data.data;
|
||
|
const chartData: DataPayload = data.chartData;
|
||
|
const chartSettings: HeatmapCaptions = data.chartSettings;
|
||
|
const chartTheme: PortalTheme = data.theme;
|
||
|
if (Object.keys(chartData).length) {
|
||
|
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
|
||
|
} else {
|
||
|
const chartTitleElement = document.createElement("div");
|
||
|
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
|
||
|
chartTitleElement.classList.add("chartTitle");
|
||
|
|
||
|
const noDataMessageElement = document.createElement("div");
|
||
|
noDataMessageElement.classList.add("noDataMessage");
|
||
|
const noDataMessageContent = document.createElement("div");
|
||
|
noDataMessageContent.innerHTML = data.errorMessage;
|
||
|
|
||
|
noDataMessageElement.appendChild(noDataMessageContent);
|
||
|
|
||
|
if (isDarkTheme(chartTheme)) {
|
||
|
chartTitleElement.classList.add("dark-theme");
|
||
|
noDataMessageElement.classList.add("dark-theme");
|
||
|
noDataMessageContent.classList.add("dark-theme");
|
||
|
}
|
||
|
|
||
|
document.getElementById(Heatmap.elementId).appendChild(chartTitleElement);
|
||
|
document.getElementById(Heatmap.elementId).appendChild(noDataMessageElement);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
window.addEventListener("message", handleMessage, false);
|
||
|
MessageHandler.sendMessage("ready");
|