Skip to content

Commit

Permalink
Add heatmap chart. Closes #673. (#681)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelBurgess authored and kaidaguerre committed Jan 29, 2025
1 parent 333c68a commit bd2e10d
Show file tree
Hide file tree
Showing 6 changed files with 462 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,33 @@ import * as echarts from "echarts/core";
import {
BarChart,
GraphChart,
HeatmapChart,
LineChart,
PieChart,
SankeyChart,
TreeChart,
} from "echarts/charts";
import { CanvasRenderer } from "echarts/renderers";
import {
CalendarComponent,
DatasetComponent,
GridComponent,
LegendComponent,
TitleComponent,
TooltipComponent,
MarkLineComponent,
VisualMapComponent,
} from "echarts/components";
import { LabelLayout } from "echarts/features";

echarts.use([
BarChart,
CalendarComponent,
CanvasRenderer,
DatasetComponent,
GraphChart,
GridComponent,
HeatmapChart,
LabelLayout,
LegendComponent,
LineChart,
Expand All @@ -33,6 +38,7 @@ echarts.use([
TitleComponent,
TooltipComponent,
TreeChart,
VisualMapComponent,
]);

export { echarts };
163 changes: 161 additions & 2 deletions ui/dashboard/src/components/dashboards/charts/Chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { registerComponent } from "@powerpipe/components/dashboards";
import { useDashboardSearchPath } from "@powerpipe/hooks/useDashboardSearchPath";
import { useDashboardTheme } from "@powerpipe/hooks/useDashboardTheme";
import { useNavigate } from "react-router-dom";
import { parseDate } from "@powerpipe/utils/date";

const getThemeColorsWithPointOverrides = (
type: ChartType = "column",
Expand Down Expand Up @@ -94,7 +95,7 @@ const getThemeColorsWithPointOverrides = (
}
};

const getCommonBaseOptions = () => ({
const getCommonBaseOptions = (themeColors) => ({
animation: false,
grid: {
left: "5%",
Expand All @@ -119,7 +120,11 @@ const getCommonBaseOptions = () => ({
},
tooltip: {
appendToBody: true,
backgroundColor: themeColors.dashboard,
borderColor: themeColors.dashboardPanel,
borderWidth: 1,
textStyle: {
color: themeColors.foreground,
fontSize: 11,
},
trigger: "item",
Expand Down Expand Up @@ -166,8 +171,87 @@ const getCommonBaseOptionsForChartType = (
series: any[],
seriesOverrides: ChartSeries | undefined,
themeColors,
dataConfig: any = {},
) => {
switch (type) {
case "heatmap": {
return {
grid: {
top: "17%",
},
tooltip: {
position: "top",
formatter: function (params) {
if (dataConfig.interval === "hourly") {
return `${params.value[2].toLocaleString()} entries on ${params.value[0]} at ${params.value[1]}`;
} else {
return `${params.value[2].toLocaleString()} entries on ${params.value[0]}`;
}
},
},
visualMap: {
type: "piecewise", // Use piecewise for custom range-color mapping
show: true, // Display the legend at the top
orient: "horizontal", // Horizontal layout for the legend
top: 10, // Position the legend at the top
left: "center",
min: 0,
max: dataConfig.maxValue,
textStyle: {
color: themeColors.foreground,
},
pieces: [
{ value: 0, color: themeColors.foregroundLightest, label: "0" },
{
min: 1,
max: Math.floor(dataConfig.maxValue * 0.2) - 1,
color: "#dae2fa",
label: `1-${(Math.floor(dataConfig.maxValue * 0.2) - 1).toLocaleString()}`,
},
{
min: Math.floor(dataConfig.maxValue * 0.2),
max: Math.floor(dataConfig.maxValue * 0.4) - 1,
color: "#b6c5f6",
label: `${Math.floor(dataConfig.maxValue * 0.2).toLocaleString()}-${(Math.floor(dataConfig.maxValue * 0.4) - 1).toLocaleString()}`,
},
{
min: Math.floor(dataConfig.maxValue * 0.4),
max: Math.floor(dataConfig.maxValue * 0.6) - 1,
color: "#91a7f1",
label: `${Math.floor(dataConfig.maxValue * 0.4).toLocaleString()}-${(Math.floor(dataConfig.maxValue * 0.6) - 1).toLocaleString()}`,
},
{
min: Math.floor(dataConfig.maxValue * 0.6),
max: Math.floor(dataConfig.maxValue * 0.8) - 1,
color: "#6d8aed",
label: `${Math.floor(dataConfig.maxValue * 0.6).toLocaleString()}-${(Math.floor(dataConfig.maxValue * 0.8) - 1).toLocaleString()}`,
},
{
min: Math.floor(dataConfig.maxValue * 0.8),
max: dataConfig.maxValue,
color: "#486de8",
label: `${Math.floor(dataConfig.maxValue * 0.8).toLocaleString()}-${dataConfig.maxValue.toLocaleString()}`,
},
],
},
xAxis: {
type: "category",
data: dataConfig.xAxisData,
axisLabel: {
color: themeColors.foreground,
//fontSize: 10,
},
},
yAxis: {
type: "category",
data: dataConfig.yAxisData,
axisLabel: {
color: themeColors.foreground,
//fontSize: 10,
},
},
};
}
case "bar":
return {
color: getThemeColorsWithPointOverrides(
Expand Down Expand Up @@ -536,6 +620,7 @@ const getSeriesForChartType = (
transform: ChartTransform,
shouldBeTimeSeries: boolean,
themeColors,
dataConfig: any = {},
) => {
if (!data) {
return [];
Expand Down Expand Up @@ -563,6 +648,24 @@ const getSeriesForChartType = (
}

switch (type) {
case "heatmap": {
series.push({
type: "heatmap",
data: dataConfig.heatmapData,
emphasis: {
itemStyle: {
borderColor: themeColors.dashboardPanel,
borderWidth: 1,
},
},
itemStyle: {
borderRadius: [3, 3, 3, 3],
borderWidth: 2, // Add a larger border for padding effect
borderColor: themeColors.dashboardPanel, // Border color matches the background
},
});
break;
}
case "bar":
case "column":
series.push({
Expand Down Expand Up @@ -706,6 +809,59 @@ const adjustGridConfig = (
return newConfig;
};

const getDataConfigForChartType = (
type: ChartType = "column",
dataset: any[][],
) => {
switch (type) {
case "heatmap": {
const rawData = dataset.slice(1);

// Infer the interval
const timestamps = rawData.map((d) => parseDate(d[0])?.unix());
const differences = timestamps.slice(1).map((t, i) => t - timestamps[i]);
const avgDifference =
differences.reduce((a, b) => a + b, 0) / differences.length;
const interval = avgDifference <= 3600000 ? "hourly" : "daily"; // 3600000 ms = 1 hour

// Generate x and y axes based on the inferred interval
let xAxisData,
yAxisData,
heatmapData,
maxValue = 0;

if (interval === "hourly") {
xAxisData = Array.from(new Set(rawData.map((d) => d[0].split("T")[0]))); // Unique days
yAxisData = Array.from(
{ length: 24 },
(_, i) => `${i < 10 ? `0${i}` : i}:00`,
); // Hours
heatmapData = rawData.map((d) => {
const [date, time] = d[0].split("T");
if (!maxValue || d[1] > maxValue) {
maxValue = d[1];
}
return [date, time.split(":")[0] + ":00", d[1]];
});
} else {
xAxisData = Array.from(new Set(rawData.map((d) => d[0].split("T")[0]))); // Unique days
yAxisData = ["Daily"];
heatmapData = rawData.map((d) => {
const date = d[0].split("T")[0];
if (!maxValue || d[1] > maxValue) {
maxValue = d[1];
}
return [date, "Daily", d[1]];
});
}

return { interval, heatmapData, xAxisData, yAxisData, maxValue };
}
default:
return {};
}
};

const buildChartOptions = (props: ChartProps, themeColors: any) => {
const { dataset, rowSeriesLabels, transform } = buildChartDataset(
props.data,
Expand All @@ -714,6 +870,7 @@ const buildChartOptions = (props: ChartProps, themeColors: any) => {
const treatAsTimeSeries = ["timestamp", "timestamptz", "date"].includes(
props.data?.columns[0].data_type.toLowerCase() || "",
);
const dataConfig = getDataConfigForChartType(props.display_type, dataset);
const series = getSeriesForChartType(
props.display_type || "column",
props.data,
Expand All @@ -722,9 +879,10 @@ const buildChartOptions = (props: ChartProps, themeColors: any) => {
transform,
treatAsTimeSeries,
themeColors,
dataConfig,
);
const config = merge(
getCommonBaseOptions(),
getCommonBaseOptions(themeColors),
getCommonBaseOptionsForChartType(
props.display_type || "column",
props.width,
Expand All @@ -733,6 +891,7 @@ const buildChartOptions = (props: ChartProps, themeColors: any) => {
series,
props.properties?.series,
themeColors,
dataConfig,
),
getOptionOverridesForChartType(
props.display_type || "column",
Expand Down
Loading

0 comments on commit bd2e10d

Please sign in to comment.