;
@@ -35,14 +45,16 @@ export function HeatMapChartPanel(props: HeatMapChartPanelProps): ReactElement |
const {
data,
xAxisCategories,
- yAxisCategories,
+ min,
+ max,
countMin,
countMax,
timeScale,
}: {
data: HeatMapDataItem[];
xAxisCategories: number[];
- yAxisCategories: string[];
+ min?: number;
+ max?: number;
countMin: number;
countMax: number;
timeScale?: TimeScale;
@@ -51,7 +63,8 @@ export function HeatMapChartPanel(props: HeatMapChartPanelProps): ReactElement |
return {
data: [],
xAxisCategories: [],
- yAxisCategories: [],
+ min: 0,
+ max: 0,
countMin: 0,
countMax: 0,
timeScale: undefined,
@@ -66,7 +79,8 @@ export function HeatMapChartPanel(props: HeatMapChartPanelProps): ReactElement |
return {
data: [],
xAxisCategories: [],
- yAxisCategories: [],
+ min: 0,
+ max: 0,
countMin: 0,
countMax: 0,
timeScale: undefined,
@@ -78,20 +92,31 @@ export function HeatMapChartPanel(props: HeatMapChartPanelProps): ReactElement |
const timeScale = getCommonTimeScaleForQueries(queryResults);
const xAxisCategories: number[] = generateCompleteTimestamps(timeScale);
+ const logBase = pluginSpec.logBase;
+
// Dummy value that will be replaced at the first iteration
let lowestBound = Infinity;
let highestBound = -Infinity;
let countMin = Infinity;
let countMax = -Infinity;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- for (const [_, histogram] of series?.histograms ?? []) {
+ for (const [, histogram] of series?.histograms ?? []) {
for (const bucket of histogram?.buckets ?? []) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [_, lowerBound, upperBound, count] = bucket;
- const lowerBoundFloat = parseFloat(lowerBound);
+ const [, lowerBound, upperBound, count] = bucket;
+ let lowerBoundFloat = parseFloat(lowerBound);
const upperBoundFloat = parseFloat(upperBound);
const countFloat = parseFloat(count);
+
+ // For logarithmic scales, skip buckets that would be entirely non-positive
+ if (logBase !== undefined && upperBoundFloat <= 0) {
+ continue;
+ }
+
+ // For log scales, adjust non-positive lower bounds
+ if (logBase !== undefined) {
+ lowerBoundFloat = getEffectiveLowerBound(lowerBoundFloat, upperBoundFloat, logBase);
+ }
+
if (lowerBoundFloat < lowestBound) {
lowestBound = lowerBoundFloat;
}
@@ -107,45 +132,67 @@ export function HeatMapChartPanel(props: HeatMapChartPanelProps): ReactElement |
}
}
- const height = contentDimensions?.height ?? HEATMAP_MIN_HEIGHT;
- const totalRange = highestBound - lowestBound;
- const rangePerItem = (totalRange * HEATMAP_ITEM_MIN_HEIGHT) / height;
- const totalItems = Math.ceil(height / HEATMAP_ITEM_MIN_HEIGHT);
-
- // Generating value of the Y axis based on the height divided by the size of a cell (item)
- const yAxisCategories: string[] = Array.from({ length: totalItems }, (_, index) =>
- (lowestBound + index * rangePerItem).toFixed(3)
- );
-
const data: HeatMapDataItem[] = [];
- // Logic for filling all cells where a bucket is present
+ // Each bucket becomes a rectangle spanning [lowerBound, upperBound] at the given x index
for (const [time, histogram] of series?.histograms ?? []) {
const itemIndexOnXaxis = xAxisCategories.findIndex((v) => v === time * 1000);
for (const bucket of histogram?.buckets ?? []) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [_, lowerBound, upperBound, count] = bucket;
- const yLowerBoundItem = Math.floor((parseFloat(lowerBound) - lowestBound) / rangePerItem);
- const yUpperBoundItem = Math.ceil((parseFloat(upperBound) - lowestBound) / rangePerItem);
-
- for (let i = 0; i < yUpperBoundItem - yLowerBoundItem; i++) {
- // TODO: some bucket may have overlapping cells, we could use avg value. Probably will need to move to a matrix data structure for performance reasons
- data.push({
- value: [itemIndexOnXaxis, yLowerBoundItem + i, parseFloat(count)],
- label: count,
- });
+ const [, lowerBound, upperBound, count] = bucket;
+ let lowerBoundFloat = parseFloat(lowerBound);
+ const upperBoundFloat = parseFloat(upperBound);
+
+ // For logarithmic scales, skip buckets that would be entirely non-positive
+ if (logBase !== undefined && upperBoundFloat <= 0) {
+ continue;
}
+
+ // For log scales, adjust non-positive lower bounds
+ if (logBase !== undefined) {
+ lowerBoundFloat = getEffectiveLowerBound(lowerBoundFloat, upperBoundFloat, logBase);
+ }
+
+ data.push({
+ value: [itemIndexOnXaxis, lowerBoundFloat, upperBoundFloat, parseFloat(count)],
+ label: count,
+ });
}
}
return {
data,
xAxisCategories,
- yAxisCategories,
+ min: lowestBound === Infinity ? undefined : lowestBound,
+ max: highestBound === -Infinity ? undefined : highestBound,
countMin,
countMax,
timeScale,
};
- }, [contentDimensions?.height, queryResults]);
+ }, [pluginSpec.logBase, queryResults]);
+
+ // Use configured min/max if provided, otherwise use calculated values
+ // For logarithmic scales, ignore user-provided min if it's <= 0 (log of non-positive is undefined)
+ // and let ECharts auto-calculate the range to avoid rendering issues
+ const finalMin = useMemo(() => {
+ if (pluginSpec.logBase !== undefined) {
+ // For log scale, ignore min if it's <= 0 or let ECharts auto-calculate
+ if (pluginSpec.min !== undefined && pluginSpec.min <= 0) {
+ return undefined; // Let ECharts auto-calculate
+ }
+ return pluginSpec.min ?? min;
+ }
+ return pluginSpec.min ?? min;
+ }, [pluginSpec.logBase, pluginSpec.min, min]);
+
+ const finalMax = useMemo(() => {
+ if (pluginSpec.logBase !== undefined) {
+ // For log scale, ignore max if it's <= 0
+ if (pluginSpec.max !== undefined && pluginSpec.max <= 0) {
+ return undefined; // Let ECharts auto-calculate
+ }
+ return pluginSpec.max ?? max;
+ }
+ return pluginSpec.max ?? max;
+ }, [pluginSpec.logBase, pluginSpec.max, max]);
// TODO: add support for multiple queries
if (queryResults.length > 1) {
@@ -178,13 +225,15 @@ export function HeatMapChartPanel(props: HeatMapChartPanelProps): ReactElement |
height={contentDimensions.height}
data={data}
xAxisCategories={xAxisCategories}
- yAxisCategories={yAxisCategories}
yAxisFormat={yAxisFormat}
countFormat={countFormat}
countMin={countMin}
countMax={countMax}
timeScale={timeScale}
showVisualMap={pluginSpec.showVisualMap}
+ min={finalMin}
+ max={finalMax}
+ logBase={pluginSpec.logBase}
/>
);
diff --git a/heatmapchart/src/components/HeatMapTooltip.ts b/heatmapchart/src/components/HeatMapTooltip.ts
index 4cf6fe7d..e429e0a9 100644
--- a/heatmapchart/src/components/HeatMapTooltip.ts
+++ b/heatmapchart/src/components/HeatMapTooltip.ts
@@ -21,7 +21,6 @@ interface CustomTooltipProps {
label: string;
marker: string;
xAxisCategories: number[];
- yAxisCategories: string[];
theme: Theme;
yAxisFormat?: FormatOptions;
countFormat?: FormatOptions;
@@ -32,13 +31,12 @@ export function generateTooltipHTML({
label,
marker,
xAxisCategories,
- yAxisCategories,
theme,
yAxisFormat,
countFormat,
}: CustomTooltipProps): string {
- const [x, y] = data;
- const xAxisLabel = xAxisCategories[x];
+ const [xIndex, yLower, yUpper] = data;
+ const xAxisLabel = xAxisCategories[xIndex];
const { formattedDate, formattedTime } = getDateAndTime(xAxisLabel);
@@ -57,10 +55,8 @@ export function generateTooltipHTML({
margin-right: 16px;
`;
- const lowerBound = parseFloat(yAxisCategories[y]!);
- const upperBound = yAxisCategories[y + 1]
- ? parseFloat(yAxisCategories[y + 1]!)
- : parseFloat(yAxisCategories[y]!) + parseFloat(yAxisCategories[y]!) - parseFloat(yAxisCategories[y - 1]!); // Top cell, upper bound need to be calculated from previous cell
+ const lowerBound = yLower;
+ const upperBound = yUpper;
return `
diff --git a/heatmapchart/src/heat-map-chart-model.ts b/heatmapchart/src/heat-map-chart-model.ts
index 7b4ff14f..0ea2ed71 100644
--- a/heatmapchart/src/heat-map-chart-model.ts
+++ b/heatmapchart/src/heat-map-chart-model.ts
@@ -20,6 +20,19 @@ export const DEFAULT_MAX_PERCENT = 100;
export const DEFAULT_MIN_PERCENT_DECIMAL = 0;
export const DEFAULT_MAX_PERCENT_DECIMAL = 1;
+export type LOG_BASE = undefined | 2 | 10;
+
+export const LOG_BASE_CONFIG: Record = {
+ none: { label: 'None', log: undefined },
+ '2': { label: '2', log: 2 },
+ '10': { label: '10', log: 10 },
+};
+
+export const LOG_BASE_OPTIONS = Object.entries(LOG_BASE_CONFIG).map(([id, config]) => ({
+ id: id as string,
+ ...config,
+}));
+
/**
* The schema for a HeatMapChart panel.
*/
@@ -34,6 +47,9 @@ export interface HeatMapChartOptions {
yAxisFormat?: FormatOptions;
countFormat?: FormatOptions;
showVisualMap?: boolean;
+ logBase?: LOG_BASE;
+ min?: number;
+ max?: number;
}
export type HeatMapChartOptionsEditorProps = OptionsEditorProps;