mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
829 lines
26 KiB
TypeScript
829 lines
26 KiB
TypeScript
// Copyright 2023 OpenObserve Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import { formatUnitValue, getUnitValue } from "../../convertDataIntoUnitValue";
|
|
import {
|
|
calculateWidthText,
|
|
calculateDynamicNameGap,
|
|
calculateRotatedLabelBottomSpace,
|
|
} from "../../chartDimensionUtils";
|
|
import { ColorModeWithoutMinMax, getSQLMinMaxValue } from "../../colorPalette";
|
|
import { getDataValue } from "../../aliasUtils";
|
|
import {
|
|
createBaseLegendConfig,
|
|
getChartDimensions,
|
|
calculateChartDimensions,
|
|
calculatePieChartRadius,
|
|
} from "../../legendConfiguration";
|
|
import { getPropsByChartTypeForSeries } from "../../sqlChartSeriesProps";
|
|
import { processData } from "../../sqlProcessData";
|
|
import { fillMissingValues } from "../../sqlMissingValueFiller";
|
|
import { buildDataLookupMap, createSeriesBuilders } from "./seriesBuilder";
|
|
import { createTrellisHelpers } from "./trellisConfig";
|
|
import { type SQLContext } from "./types";
|
|
|
|
/**
|
|
* Finds the largest label in the given data array.
|
|
*/
|
|
export const largestLabel = (data: any) => {
|
|
const largestlabel = data.reduce((largest: any, label: any) => {
|
|
return label?.toString().length > largest?.toString().length
|
|
? label
|
|
: largest;
|
|
}, "");
|
|
|
|
return largestlabel;
|
|
};
|
|
|
|
/**
|
|
* Builds the complete SQLContext for a given set of raw inputs.
|
|
*
|
|
* This function replicates all the work that `convertSQLData` used to do
|
|
* *before* the chart-type switch statement ΓÇö axis-key extraction, data
|
|
* processing, base-options construction, and helper-function creation.
|
|
*
|
|
* @returns The fully populated SQLContext, or `null` if the input is invalid.
|
|
*/
|
|
export function buildSQLContext(
|
|
panelSchema: any,
|
|
searchQueryData: any,
|
|
store: any,
|
|
chartPanelRef: any,
|
|
hoveredSeriesState: any,
|
|
resultMetaData: any,
|
|
metadata: any,
|
|
chartPanelStyle: any,
|
|
annotations: any,
|
|
): SQLContext | null {
|
|
// Set gridlines visibility based on config.show_gridlines (default: true)
|
|
const showGridlines =
|
|
panelSchema?.config?.show_gridlines !== undefined
|
|
? panelSchema.config.show_gridlines
|
|
: true;
|
|
const extras: any = {};
|
|
|
|
// if no data than return it
|
|
if (
|
|
!Array.isArray(searchQueryData) ||
|
|
searchQueryData.length === 0 ||
|
|
!searchQueryData[0] ||
|
|
!panelSchema.queries[0].fields.x ||
|
|
!panelSchema.queries[0].fields.y
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// get the x axis key
|
|
const getXAxisKeys = () => {
|
|
return panelSchema?.queries[0]?.fields?.x?.length
|
|
? panelSchema?.queries[0]?.fields?.x.map((it: any) => it.alias)
|
|
: [];
|
|
};
|
|
|
|
// get the y axis key
|
|
const getYAxisKeys = () => {
|
|
return panelSchema?.queries[0]?.fields?.y?.length
|
|
? panelSchema?.queries[0]?.fields?.y.map((it: any) => it.alias)
|
|
: [];
|
|
};
|
|
|
|
// get the z axis key
|
|
const getZAxisKeys = () => {
|
|
return panelSchema?.queries[0]?.fields?.z?.length
|
|
? panelSchema?.queries[0]?.fields?.z.map((it: any) => it.alias)
|
|
: [];
|
|
};
|
|
|
|
// get the breakdown key
|
|
const getBreakDownKeys = () => {
|
|
return panelSchema?.queries[0]?.fields?.breakdown?.length
|
|
? panelSchema?.queries[0]?.fields?.breakdown.map((it: any) => it.alias)
|
|
: [];
|
|
};
|
|
|
|
// Step 1: Get the X-Axis key
|
|
const xAxisKeys = getXAxisKeys();
|
|
|
|
// Step 2: Get the Y-Axis key
|
|
const yAxisKeys = getYAxisKeys();
|
|
|
|
const zAxisKeys = getZAxisKeys();
|
|
|
|
const breakDownKeys = getBreakDownKeys();
|
|
|
|
const getMarkLineData = (panelSchema: any) => {
|
|
return (
|
|
panelSchema?.config?.mark_line?.map((markLine: any) => {
|
|
return {
|
|
name: markLine.name,
|
|
type: markLine.type,
|
|
xAxis: markLine.type == "xAxis" ? markLine.value : null,
|
|
yAxis: markLine.type == "yAxis" ? markLine.value : null,
|
|
label: {
|
|
formatter: markLine.name ? "{b}:{c}" : "{c}",
|
|
position: "insideEndTop",
|
|
},
|
|
};
|
|
}) ?? []
|
|
);
|
|
};
|
|
|
|
const noValueConfigOption = panelSchema?.config?.no_value_replacement ?? "";
|
|
|
|
const processedData = processData(
|
|
searchQueryData,
|
|
panelSchema,
|
|
store,
|
|
yAxisKeys,
|
|
breakDownKeys,
|
|
xAxisKeys,
|
|
extras,
|
|
);
|
|
|
|
const missingValueData = fillMissingValues(
|
|
processedData,
|
|
panelSchema,
|
|
resultMetaData,
|
|
metadata,
|
|
xAxisKeys,
|
|
yAxisKeys,
|
|
zAxisKeys,
|
|
breakDownKeys,
|
|
noValueConfigOption,
|
|
);
|
|
|
|
// get the axis data using key
|
|
const getAxisDataFromKey = (key: string) => {
|
|
const data =
|
|
missingValueData?.filter((item: any) => {
|
|
return (
|
|
xAxisKeys.every((key: any) => getDataValue(item, key) != null) &&
|
|
yAxisKeys.every((key: any) => getDataValue(item, key) != null) &&
|
|
breakDownKeys.every((key: any) => getDataValue(item, key) != null)
|
|
);
|
|
}) || [];
|
|
|
|
// if data is not there use {} as a default value
|
|
const keys = Object.keys((data.length && data[0]) || {}); // Assuming there's at least one object
|
|
|
|
const keyArrays: any = {};
|
|
|
|
for (const key of keys) {
|
|
keyArrays[key] = data.map((obj: any) => getDataValue(obj, key));
|
|
}
|
|
|
|
const result = getDataValue(keyArrays, key) || [];
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Returns the largest label from the stacked chart data.
|
|
* Calculates the largest value for each unique breakdown and sums those values.
|
|
* @param axisKey - key of the yaxis
|
|
* @param breakDownkey - key of the breakdown
|
|
* @returns {number} - the largest value
|
|
*/
|
|
const largestStackLabel = (axisKey: string, breakDownkey: string) => {
|
|
const data =
|
|
missingValueData?.filter((item: any) => {
|
|
return (
|
|
xAxisKeys.every((key: any) => getDataValue(item, key) != null) &&
|
|
yAxisKeys.every((key: any) => getDataValue(item, key) != null) &&
|
|
breakDownKeys.every((key: any) => getDataValue(item, key) != null)
|
|
);
|
|
}) || [];
|
|
|
|
const maxValues: any = {};
|
|
|
|
data.forEach((obj: any) => {
|
|
const breakDownValue = getDataValue(obj, breakDownkey);
|
|
const axisValue = getDataValue(obj, axisKey);
|
|
|
|
if (maxValues[breakDownValue]) {
|
|
if (axisValue > maxValues[breakDownValue]) {
|
|
maxValues[breakDownValue] = axisValue;
|
|
}
|
|
} else {
|
|
maxValues[breakDownValue] =
|
|
typeof axisValue === "number" ? axisValue : 0;
|
|
}
|
|
});
|
|
|
|
return Object.values(maxValues).reduce((a: any, b: any) => a + b, 0);
|
|
};
|
|
|
|
function getLargestLabel() {
|
|
if (
|
|
(panelSchema.type === "stacked" || panelSchema.type === "area-stacked") &&
|
|
breakDownKeys.length > 0
|
|
) {
|
|
return largestStackLabel(yAxisKeys[0], breakDownKeys[0]);
|
|
} else {
|
|
return largestLabel(getAxisDataFromKey(yAxisKeys[0]));
|
|
}
|
|
}
|
|
|
|
const convertedTimeStampToDataFormat = new Date(
|
|
annotations?.value?.[0]?.start_time / 1000,
|
|
).toString();
|
|
|
|
/**
|
|
* Returns the pie chart radius that accounts for legend space
|
|
* @param {any[]} seriesData - The series data for legend calculation
|
|
* @returns {number} - the radius percentage
|
|
*/
|
|
const getPieChartRadius = (seriesData: any[] = []) => {
|
|
// Get chart dimensions from chartPanelRef
|
|
const dimensions = getChartDimensions(chartPanelRef);
|
|
|
|
// Calculate available dimensions using our centralized helper function
|
|
const chartDimensions = calculateChartDimensions(
|
|
panelSchema,
|
|
dimensions.chartWidth,
|
|
dimensions.chartHeight,
|
|
seriesData,
|
|
);
|
|
|
|
// Use the optimized pie chart radius calculation
|
|
const radius = calculatePieChartRadius(
|
|
panelSchema,
|
|
chartDimensions.availableWidth,
|
|
chartDimensions.availableHeight,
|
|
dimensions.chartWidth,
|
|
dimensions.chartHeight,
|
|
);
|
|
|
|
return radius;
|
|
};
|
|
|
|
const legendConfig = createBaseLegendConfig(panelSchema, hoveredSeriesState);
|
|
|
|
const isHorizontalChart =
|
|
panelSchema.type === "h-bar" || panelSchema.type === "h-stacked";
|
|
|
|
const defaultGrid = {
|
|
containLabel: true,
|
|
left: "10%",
|
|
right: "10%",
|
|
top: "15%",
|
|
bottom: "15%",
|
|
};
|
|
|
|
const [min, max] = getSQLMinMaxValue(yAxisKeys, missingValueData);
|
|
|
|
const getFinalAxisValue = (
|
|
configValue: number | null | undefined,
|
|
dataValue: number,
|
|
isMin: boolean,
|
|
) => {
|
|
if (configValue === null || configValue === undefined) {
|
|
return undefined;
|
|
}
|
|
return isMin
|
|
? Math.min(configValue, dataValue)
|
|
: Math.max(configValue, dataValue);
|
|
};
|
|
|
|
const hasXAxisName = panelSchema.queries[0]?.fields?.x[0]?.label;
|
|
|
|
const hasYAxisName =
|
|
panelSchema.queries[0]?.fields?.y?.length == 1 &&
|
|
panelSchema.queries[0]?.fields?.y[0]?.label;
|
|
|
|
// Check if x-axis will be time-based by looking for timestamp fields
|
|
const hasTimestampField = panelSchema.queries[0].fields?.x?.some(
|
|
(it: any) =>
|
|
it?.args?.[0]?.value?.field == store.state?.zoConfig?.timestamp_column ||
|
|
["histogram", "date_bin"].includes(it.aggregationFunction),
|
|
);
|
|
|
|
// Calculate additional spacing needed for rotated labels
|
|
// Skip rotation for time-based x-axis and horizontal chart types (h-bar, h-stacked)
|
|
// For horizontal charts, labels should always be at 0 degrees (not rotated)
|
|
let labelRotation =
|
|
hasTimestampField || isHorizontalChart
|
|
? 0
|
|
: panelSchema.config?.axis_label_rotate || 0;
|
|
let labelWidth =
|
|
hasTimestampField || isHorizontalChart
|
|
? 0
|
|
: panelSchema.config?.axis_label_truncate_width || 0;
|
|
|
|
// If truncate width is not set and not time-based/horizontal, calculate the actual max width from data
|
|
if (
|
|
!hasTimestampField &&
|
|
!isHorizontalChart &&
|
|
labelWidth === 0 &&
|
|
xAxisKeys.length > 0
|
|
) {
|
|
const longestLabelStr = largestLabel(getAxisDataFromKey(xAxisKeys[0]));
|
|
labelWidth = calculateWidthText(longestLabelStr, "12px");
|
|
} else if (!hasTimestampField && !isHorizontalChart && labelWidth === 0) {
|
|
labelWidth = 120; // Fallback
|
|
}
|
|
|
|
const labelFontSize = 12;
|
|
const labelMargin = 10;
|
|
|
|
// Calculate the section height (nameGap) upfront so we can use it for bottom spacing
|
|
// Skip rotation-based calculations for horizontal charts and time-based axes
|
|
const dynamicXAxisNameGap =
|
|
hasTimestampField || isHorizontalChart
|
|
? 25
|
|
: calculateDynamicNameGap(
|
|
labelRotation,
|
|
labelWidth,
|
|
labelFontSize,
|
|
25,
|
|
labelMargin,
|
|
);
|
|
|
|
// Additional bottom space is only needed for non-horizontal, non-time-based charts
|
|
const additionalBottomSpace =
|
|
hasTimestampField || isHorizontalChart
|
|
? 0
|
|
: calculateRotatedLabelBottomSpace(
|
|
labelRotation,
|
|
labelWidth,
|
|
labelFontSize,
|
|
!!hasXAxisName,
|
|
dynamicXAxisNameGap,
|
|
);
|
|
|
|
const options: any = {
|
|
backgroundColor: "transparent",
|
|
legend: legendConfig,
|
|
grid: {
|
|
containLabel: panelSchema.config?.axis_width == null ? true : false,
|
|
left: hasYAxisName ? (panelSchema.config?.axis_width ?? 30) : 5,
|
|
right: 20,
|
|
top: "15",
|
|
bottom: hasXAxisName
|
|
? (() => {
|
|
const baseBottom =
|
|
legendConfig.orient === "horizontal" &&
|
|
panelSchema.config?.show_legends
|
|
? panelSchema.config?.axis_width == null
|
|
? 50
|
|
: 60
|
|
: panelSchema.config?.axis_width == null
|
|
? 35
|
|
: 40;
|
|
// When an x-axis name is present, `nameGap` already reserves space
|
|
// between rotated labels and the axis name. Adding `additionalBottomSpace`
|
|
// here causes double-counting and extra blank space beneath the axis
|
|
// name. Only return the base bottom in this case.
|
|
return baseBottom;
|
|
})()
|
|
: (() => {
|
|
const baseBottom =
|
|
legendConfig.orient === "vertical" &&
|
|
panelSchema.config?.show_legends
|
|
? 0
|
|
: breakDownKeys.length > 0
|
|
? 25
|
|
: 0;
|
|
return baseBottom + additionalBottomSpace;
|
|
})(),
|
|
},
|
|
tooltip: {
|
|
trigger: "axis",
|
|
textStyle: {
|
|
color: store.state.theme === "dark" ? "#fff" : "#000",
|
|
fontSize: 12,
|
|
},
|
|
enterable: true,
|
|
backgroundColor:
|
|
store.state.theme === "dark" ? "rgba(0,0,0,1)" : "rgba(255,255,255,1)",
|
|
extraCssText:
|
|
"max-height: 200px; overflow: auto; max-width: 400px; user-select: text; scrollbar-width: thin; scrollbar-color: rgba(128,128,128,0.5) transparent;",
|
|
axisPointer: {
|
|
type: "cross",
|
|
label: {
|
|
show: true,
|
|
fontsize: 12,
|
|
precision: panelSchema.config?.decimals,
|
|
backgroundColor: store.state.theme === "dark" ? "#333" : "",
|
|
formatter: function (params: any) {
|
|
try {
|
|
let lineBreaks = "";
|
|
if (
|
|
panelSchema.type === "h-bar" ||
|
|
panelSchema.type === "h-stacked"
|
|
) {
|
|
if (params?.axisDimension == "x")
|
|
return formatUnitValue(
|
|
getUnitValue(
|
|
params?.value,
|
|
panelSchema.config?.unit,
|
|
panelSchema.config?.unit_custom,
|
|
panelSchema.config?.decimals,
|
|
),
|
|
);
|
|
|
|
//we does not required any linebreaks for h-stacked because we only use one x axis
|
|
if (panelSchema.type === "h-stacked")
|
|
return params?.value?.toString();
|
|
for (
|
|
let i = 0;
|
|
i <
|
|
xAxisKeys.length +
|
|
breakDownKeys.length -
|
|
params?.axisIndex -
|
|
1;
|
|
i++
|
|
) {
|
|
lineBreaks += " \n \n";
|
|
}
|
|
params.value = params?.value?.toString();
|
|
return `${lineBreaks} ${params?.value}`;
|
|
}
|
|
if (params?.axisDimension == "y")
|
|
return formatUnitValue(
|
|
getUnitValue(
|
|
params?.value,
|
|
panelSchema.config?.unit,
|
|
panelSchema.config?.unit_custom,
|
|
panelSchema.config?.decimals,
|
|
),
|
|
);
|
|
for (
|
|
let i = 0;
|
|
i <
|
|
xAxisKeys.length + breakDownKeys.length - params?.axisIndex - 1;
|
|
i++
|
|
) {
|
|
lineBreaks += " \n \n";
|
|
}
|
|
params.value = params?.value?.toString();
|
|
return `${lineBreaks} ${params?.value}`;
|
|
} catch (error) {
|
|
return params?.value?.toString() ?? "";
|
|
}
|
|
},
|
|
},
|
|
},
|
|
formatter: function (name: any) {
|
|
try {
|
|
// show tooltip for hovered panel only for other we only need axis so just return empty string
|
|
if (
|
|
hoveredSeriesState?.value &&
|
|
panelSchema.id &&
|
|
hoveredSeriesState?.value?.panelId != panelSchema.id
|
|
)
|
|
return "";
|
|
if (name?.length == 0) return "";
|
|
|
|
// sort tooltip array based on value
|
|
name?.sort((a: any, b: any) => {
|
|
return (b.value ?? 0) - (a.value ?? 0);
|
|
});
|
|
|
|
// if hovered series name is not null then move it to first position
|
|
if (hoveredSeriesState?.value?.hoveredSeriesName) {
|
|
// get the current series index from name
|
|
const currentSeriesIndex = name?.findIndex(
|
|
(it: any) =>
|
|
it.seriesName == hoveredSeriesState?.value?.hoveredSeriesName,
|
|
);
|
|
|
|
// if hovered series index is not -1 then take it to very first position
|
|
if (currentSeriesIndex != -1) {
|
|
// shift all series to next position and place current series at first position
|
|
const temp = name?.[currentSeriesIndex];
|
|
for (let i = currentSeriesIndex; i > 0; i--) {
|
|
name[i] = name?.[i - 1];
|
|
}
|
|
name[0] = temp;
|
|
}
|
|
}
|
|
|
|
const hoverText: string[] = [];
|
|
name?.forEach((it: any) => {
|
|
// if value is not null, show in tooltip
|
|
if (it.value != null) {
|
|
// check if the series is the current series being hovered
|
|
// if have than bold it
|
|
if (
|
|
it?.seriesName == hoveredSeriesState?.value?.hoveredSeriesName
|
|
)
|
|
hoverText.push(
|
|
`<strong>${it.marker} ${it.seriesName} : ${formatUnitValue(
|
|
getUnitValue(
|
|
it.value,
|
|
panelSchema.config?.unit,
|
|
panelSchema.config?.unit_custom,
|
|
panelSchema.config?.decimals,
|
|
),
|
|
)} </strong>`,
|
|
);
|
|
// else normal text
|
|
else
|
|
hoverText.push(
|
|
`${it.marker} ${it.seriesName} : ${formatUnitValue(
|
|
getUnitValue(
|
|
it.value,
|
|
panelSchema.config?.unit,
|
|
panelSchema.config?.unit_custom,
|
|
panelSchema.config?.decimals,
|
|
),
|
|
)}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
return `${name?.[0]?.name} <br/> ${hoverText.join("<br/>")}`;
|
|
} catch (error) {
|
|
return "";
|
|
}
|
|
},
|
|
},
|
|
xAxis: [...xAxisKeys, ...breakDownKeys]?.map((key: any, index: number) => {
|
|
const data = getAxisDataFromKey(key);
|
|
|
|
//unique value index array
|
|
const arr: any = [];
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i == 0 || data[i] != data[i - 1]) arr.push(i);
|
|
}
|
|
|
|
// Use 0 for rotation and width if time-based field or horizontal chart
|
|
const labelRotation =
|
|
hasTimestampField || isHorizontalChart
|
|
? 0
|
|
: panelSchema.config?.axis_label_rotate || 0;
|
|
const labelWidth =
|
|
hasTimestampField || isHorizontalChart
|
|
? 120
|
|
: panelSchema.config?.axis_label_truncate_width || 120;
|
|
const labelFontSize = 12;
|
|
const labelMargin = 10;
|
|
|
|
return {
|
|
type: "category",
|
|
position: panelSchema.type == "h-bar" ? "left" : "bottom",
|
|
// inverse data for h-stacked and h-bar
|
|
inverse: ["h-stacked", "h-bar"].includes(panelSchema.type),
|
|
name: index == 0 ? panelSchema.queries[0]?.fields?.x[index]?.label : "",
|
|
label: {
|
|
show: panelSchema.config?.label_option?.position != null,
|
|
position: panelSchema.config?.label_option?.position || "None",
|
|
rotate: panelSchema.config?.label_option?.rotate || 0,
|
|
},
|
|
nameLocation: "middle",
|
|
nameGap: dynamicXAxisNameGap,
|
|
nameTextStyle: {
|
|
fontWeight: "bold",
|
|
fontSize: 14,
|
|
},
|
|
axisLabel: {
|
|
interval:
|
|
panelSchema.type == "h-stacked"
|
|
? "auto"
|
|
: index == xAxisKeys.length + breakDownKeys.length - 1
|
|
? "auto"
|
|
: function (i: any) {
|
|
return arr.includes(i);
|
|
},
|
|
overflow:
|
|
index == xAxisKeys.length + breakDownKeys.length - 1
|
|
? "none"
|
|
: "truncate",
|
|
// hide axis label if overlaps
|
|
hideOverlap: true,
|
|
width: labelWidth,
|
|
margin: labelMargin,
|
|
rotate: labelRotation,
|
|
},
|
|
splitLine: {
|
|
show: showGridlines,
|
|
},
|
|
axisLine: {
|
|
show: searchQueryData?.every((it: any) => it.length == 0)
|
|
? true
|
|
: (panelSchema.config?.axis_border_show ?? false),
|
|
},
|
|
axisTick: {
|
|
show: xAxisKeys.length + breakDownKeys.length == 1 ? false : true,
|
|
align: "left",
|
|
alignWithLabel: false,
|
|
length: 5,
|
|
interval:
|
|
panelSchema.type == "h-stacked"
|
|
? "auto"
|
|
: function (i: any) {
|
|
return arr.includes(i);
|
|
},
|
|
},
|
|
data: data,
|
|
};
|
|
}),
|
|
yAxis: {
|
|
type: "value",
|
|
name:
|
|
panelSchema.queries[0]?.fields?.y?.length == 1
|
|
? panelSchema.queries[0]?.fields?.y[0]?.label
|
|
: "",
|
|
nameLocation: "middle",
|
|
min: getFinalAxisValue(panelSchema.config.y_axis_min, min, true),
|
|
max: getFinalAxisValue(panelSchema.config.y_axis_max, max, false),
|
|
nameGap:
|
|
calculateWidthText(
|
|
panelSchema.type == "h-bar" || panelSchema.type == "h-stacked"
|
|
? largestLabel(getAxisDataFromKey(yAxisKeys[0]))
|
|
: formatUnitValue(
|
|
getUnitValue(
|
|
getLargestLabel(),
|
|
panelSchema.config?.unit,
|
|
panelSchema.config?.unit_custom,
|
|
panelSchema.config?.decimals,
|
|
),
|
|
),
|
|
) + 8,
|
|
nameTextStyle: {
|
|
fontWeight: "bold",
|
|
fontSize: 14,
|
|
},
|
|
axisLabel: {
|
|
formatter: function (value: any) {
|
|
try {
|
|
return formatUnitValue(
|
|
getUnitValue(
|
|
value,
|
|
panelSchema.config?.unit,
|
|
panelSchema.config?.unit_custom,
|
|
panelSchema.config?.decimals,
|
|
),
|
|
);
|
|
} catch (error) {
|
|
return "";
|
|
}
|
|
},
|
|
},
|
|
splitLine: {
|
|
show: showGridlines,
|
|
},
|
|
axisLine: {
|
|
show: searchQueryData?.every((it: any) => it.length == 0)
|
|
? true
|
|
: (panelSchema.config?.axis_border_show ?? false),
|
|
},
|
|
},
|
|
toolbox: {
|
|
orient: "vertical",
|
|
show: !["pie", "donut", "metric", "gauge"].includes(panelSchema.type),
|
|
showTitle: false,
|
|
tooltip: {
|
|
show: false,
|
|
},
|
|
itemSize: 0,
|
|
itemGap: 0,
|
|
// it is used to hide toolbox buttons
|
|
bottom: "100%",
|
|
feature: {
|
|
dataZoom: {
|
|
yAxisIndex: panelSchema.config?.dataZoom?.hasOwnProperty("yAxisIndex")
|
|
? panelSchema.config?.dataZoom.yAxisIndex
|
|
: "none",
|
|
},
|
|
},
|
|
},
|
|
series: [],
|
|
};
|
|
|
|
// Ensure gridlines visibility is set for all xAxis and yAxis (handles both array and object cases)
|
|
if (options.xAxis) {
|
|
(Array.isArray(options.xAxis) ? options.xAxis : [options.xAxis]).forEach(
|
|
(axis) => {
|
|
axis.splitLine.show = showGridlines;
|
|
},
|
|
);
|
|
}
|
|
if (options.yAxis) {
|
|
(Array.isArray(options.yAxis) ? options.yAxis : [options.yAxis]).forEach(
|
|
(axis) => {
|
|
// if (!axis.splitLine) axis.splitLine = {};
|
|
axis.splitLine.show = showGridlines;
|
|
},
|
|
);
|
|
}
|
|
|
|
const defaultSeriesProps = getPropsByChartTypeForSeries(panelSchema);
|
|
|
|
// if color type is shades, continuous then required to calculate min and max for chart.
|
|
let chartMin: any = Infinity;
|
|
let chartMax: any = -Infinity;
|
|
if (
|
|
!Object.values(ColorModeWithoutMinMax).includes(
|
|
panelSchema.config?.color?.mode,
|
|
)
|
|
) {
|
|
// if heatmap then get min and max from z axis sql data
|
|
if (panelSchema.type == "heatmap") {
|
|
// NOTE: Currently we do not support color options for heatmap
|
|
// [chartMin, chartMax] = getSQLMinMaxValue(zAxisKeys, searchQueryData);
|
|
chartMin = null;
|
|
chartMax = null;
|
|
} else {
|
|
[chartMin, chartMax] = getSQLMinMaxValue(yAxisKeys, missingValueData);
|
|
}
|
|
}
|
|
|
|
// Build the lookup map once for O(1) series data access
|
|
const dataLookupMap = buildDataLookupMap(
|
|
missingValueData,
|
|
breakDownKeys,
|
|
xAxisKeys,
|
|
);
|
|
|
|
// Build series helpers (they close over options, missingValueData, etc.)
|
|
const seriesHelpers = createSeriesBuilders({
|
|
options,
|
|
panelSchema,
|
|
store,
|
|
missingValueData,
|
|
xAxisKeys,
|
|
yAxisKeys,
|
|
breakDownKeys,
|
|
defaultSeriesProps,
|
|
chartMin,
|
|
chartMax,
|
|
dataLookupMap,
|
|
annotations,
|
|
getMarkLineData,
|
|
getAxisDataFromKey,
|
|
});
|
|
|
|
// Build trellis helpers (they close over options, chartPanelRef, etc.)
|
|
const trellisHelpers = createTrellisHelpers({
|
|
options,
|
|
panelSchema,
|
|
chartPanelRef,
|
|
chartPanelStyle,
|
|
yAxisKeys,
|
|
xAxisKeys,
|
|
isHorizontalChart,
|
|
convertedTimeStampToDataFormat,
|
|
defaultGrid,
|
|
getAxisDataFromKey,
|
|
getAnnotationMarkLine: seriesHelpers.getAnnotationMarkLine,
|
|
getSeriesMarkArea: seriesHelpers.getSeriesMarkArea,
|
|
largestLabel,
|
|
});
|
|
|
|
const ctx: SQLContext = {
|
|
// Raw inputs
|
|
panelSchema,
|
|
searchQueryData,
|
|
store,
|
|
chartPanelRef,
|
|
hoveredSeriesState,
|
|
chartPanelStyle,
|
|
annotations,
|
|
metadata,
|
|
resultMetaData,
|
|
// Axis keys
|
|
xAxisKeys,
|
|
yAxisKeys,
|
|
zAxisKeys,
|
|
breakDownKeys,
|
|
// Processed data
|
|
missingValueData,
|
|
extras,
|
|
// Derived scalars
|
|
showGridlines,
|
|
hasTimestampField,
|
|
isHorizontalChart,
|
|
dynamicXAxisNameGap,
|
|
convertedTimeStampToDataFormat,
|
|
defaultSeriesProps,
|
|
chartMin,
|
|
chartMax,
|
|
defaultGrid,
|
|
// Mutable ECharts options
|
|
options,
|
|
// Closure-based helpers
|
|
getAxisDataFromKey,
|
|
getSeries: seriesHelpers.getSeries,
|
|
getAnnotationMarkLine: seriesHelpers.getAnnotationMarkLine,
|
|
getSeriesMarkArea: seriesHelpers.getSeriesMarkArea,
|
|
getPieChartRadius,
|
|
updateTrellisConfig: trellisHelpers.updateTrellisConfig,
|
|
};
|
|
|
|
return ctx;
|
|
}
|