mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
HeatmapNG: cell value filtering and color clamping (#50204)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"fiscalYearStartMonth": 0,
|
"fiscalYearStartMonth": 0,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"liveNow": false,
|
"liveNow": false,
|
||||||
"panels": [
|
"panels": [
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
"color": "rgba(255,0,255,0.7)"
|
"color": "rgba(255,0,255,0.7)"
|
||||||
},
|
},
|
||||||
"filterValues": {
|
"filterValues": {
|
||||||
"min": 1e-9
|
"le": 1e-9
|
||||||
},
|
},
|
||||||
"legend": {
|
"legend": {
|
||||||
"show": true
|
"show": true
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
"color": "rgba(255,0,255,0.7)"
|
"color": "rgba(255,0,255,0.7)"
|
||||||
},
|
},
|
||||||
"filterValues": {
|
"filterValues": {
|
||||||
"min": 1e-9
|
"le": 1e-9
|
||||||
},
|
},
|
||||||
"legend": {
|
"legend": {
|
||||||
"show": true
|
"show": true
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
"color": "rgba(255,0,255,0.7)"
|
"color": "rgba(255,0,255,0.7)"
|
||||||
},
|
},
|
||||||
"filterValues": {
|
"filterValues": {
|
||||||
"min": 1e-9
|
"le": 1e-9
|
||||||
},
|
},
|
||||||
"legend": {
|
"legend": {
|
||||||
"show": true
|
"show": true
|
||||||
@@ -417,7 +417,7 @@
|
|||||||
"color": "rgba(255,0,255,0.7)"
|
"color": "rgba(255,0,255,0.7)"
|
||||||
},
|
},
|
||||||
"filterValues": {
|
"filterValues": {
|
||||||
"min": 1e-9
|
"le": 1e-9
|
||||||
},
|
},
|
||||||
"legend": {
|
"legend": {
|
||||||
"show": true
|
"show": true
|
||||||
@@ -502,7 +502,7 @@
|
|||||||
"color": "rgba(255,0,255,0.7)"
|
"color": "rgba(255,0,255,0.7)"
|
||||||
},
|
},
|
||||||
"filterValues": {
|
"filterValues": {
|
||||||
"min": 1e-9
|
"le": 1e-9
|
||||||
},
|
},
|
||||||
"legend": {
|
"legend": {
|
||||||
"show": true
|
"show": true
|
||||||
@@ -548,6 +548,6 @@
|
|||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Heatmap calculate (log)",
|
"title": "Heatmap calculate (log)",
|
||||||
"uid": "ZXYQTA97ZZ",
|
"uid": "ZXYQTA97ZZ",
|
||||||
"version": 4,
|
"version": 1,
|
||||||
"weekStart": ""
|
"weekStart": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const GRADIENT_STOPS = 10;
|
|||||||
export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useStopsPercentage }: Props) => {
|
export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useStopsPercentage }: Props) => {
|
||||||
const [colors, setColors] = useState<string[]>([]);
|
const [colors, setColors] = useState<string[]>([]);
|
||||||
const [scaleHover, setScaleHover] = useState<HoverState>({ isShown: false, value: 0 });
|
const [scaleHover, setScaleHover] = useState<HoverState>({ isShown: false, value: 0 });
|
||||||
const [percent, setPercent] = useState<number | null>(null);
|
const [percent, setPercent] = useState<number | null>(null); // 0-100 for CSS percentage
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, colors);
|
const styles = getStyles(theme, colors);
|
||||||
@@ -50,15 +50,12 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useSto
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hoverValue != null) {
|
setPercent(hoverValue == null ? null : clampPercent100((hoverValue - min) / (max - min)));
|
||||||
const percent = hoverValue / (max - min);
|
|
||||||
setPercent(percent * 100);
|
|
||||||
}
|
|
||||||
}, [hoverValue, min, max]);
|
}, [hoverValue, min, max]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.scaleWrapper}>
|
<div className={styles.scaleWrapper} onMouseMove={onScaleMouseMove} onMouseLeave={onScaleMouseLeave}>
|
||||||
<div className={styles.scaleGradient} onMouseMove={onScaleMouseMove} onMouseLeave={onScaleMouseLeave}>
|
<div className={styles.scaleGradient}>
|
||||||
{display && (scaleHover.isShown || hoverValue !== undefined) && (
|
{display && (scaleHover.isShown || hoverValue !== undefined) && (
|
||||||
<div className={styles.followerContainer}>
|
<div className={styles.followerContainer}>
|
||||||
<div className={styles.follower} style={{ left: `${percent}%` }} />
|
<div className={styles.follower} style={{ left: `${percent}%` }} />
|
||||||
@@ -121,10 +118,19 @@ const getGradientStops = ({
|
|||||||
return [...gradientStops];
|
return [...gradientStops];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function clampPercent100(v: number) {
|
||||||
|
if (v > 1) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (v < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return v * 100;
|
||||||
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
||||||
scaleWrapper: css`
|
scaleWrapper: css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 300px;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
`,
|
`,
|
||||||
@@ -138,7 +144,7 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
|
|||||||
`,
|
`,
|
||||||
hoverValue: css`
|
hoverValue: css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding-top: 5px;
|
padding-top: 4px;
|
||||||
`,
|
`,
|
||||||
followerContainer: css`
|
followerContainer: css`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ const logModeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
|
|||||||
value: HeatmapCalculationMode.Size,
|
value: HeatmapCalculationMode.Size,
|
||||||
description: 'Split the buckets based on size',
|
description: 'Split the buckets based on size',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Count',
|
|
||||||
value: HeatmapCalculationMode.Count,
|
|
||||||
description: 'Split the buckets based on count',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AxisEditor: React.FC<StandardEditorProps<HeatmapCalculationBucketConfig, any>> = ({
|
export const AxisEditor: React.FC<StandardEditorProps<HeatmapCalculationBucketConfig, any>> = ({
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe('Heatmap transformer', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const heatmap = bucketsToScanlines({ frame, name: 'Speed' });
|
const heatmap = bucketsToScanlines({ frame, value: 'Speed' });
|
||||||
expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(`
|
expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function readHeatmapScanlinesCustomMeta(frame?: DataFrame): HeatmapScanli
|
|||||||
|
|
||||||
export interface BucketsOptions {
|
export interface BucketsOptions {
|
||||||
frame: DataFrame;
|
frame: DataFrame;
|
||||||
name?: string;
|
value?: string; // the field value name
|
||||||
layout?: HeatmapBucketLayout;
|
layout?: HeatmapBucketLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ export function bucketsToScanlines(opts: BucketsOptions): DataFrame {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: opts.name?.length ? opts.name : 'Value',
|
name: opts.value?.length ? opts.value : 'Value',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: new ArrayVector(counts2),
|
values: new ArrayVector(counts2),
|
||||||
config: yFields[0].config,
|
config: yFields[0].config,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
let exemplarsyFacet: number[] = [];
|
let exemplarsyFacet: number[] = [];
|
||||||
|
|
||||||
const meta = readHeatmapScanlinesCustomMeta(info.heatmap);
|
const meta = readHeatmapScanlinesCustomMeta(info.heatmap);
|
||||||
if (info.exemplars && meta.yMatchWithLabel) {
|
if (info.exemplars?.length && meta.yMatchWithLabel) {
|
||||||
exemplarsXFacet = info.exemplars?.fields[0].values.toArray();
|
exemplarsXFacet = info.exemplars?.fields[0].values.toArray();
|
||||||
|
|
||||||
// ordinal/labeled heatmap-buckets?
|
// ordinal/labeled heatmap-buckets?
|
||||||
@@ -126,7 +126,10 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
getTimeRange: () => timeRangeRef.current,
|
getTimeRange: () => timeRangeRef.current,
|
||||||
palette,
|
palette,
|
||||||
cellGap: options.cellGap,
|
cellGap: options.cellGap,
|
||||||
hideThreshold: options.filterValues?.min, // eventually a better range
|
hideLE: options.filterValues?.le,
|
||||||
|
hideGE: options.filterValues?.ge,
|
||||||
|
valueMin: options.color.min,
|
||||||
|
valueMax: options.color.max,
|
||||||
exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
|
exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)',
|
||||||
yAxisConfig: options.yAxis,
|
yAxisConfig: options.yAxis,
|
||||||
ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
|
ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1,
|
||||||
@@ -143,7 +146,17 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
let countFieldIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3;
|
let countFieldIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3;
|
||||||
const countField = info.heatmap.fields[countFieldIdx];
|
const countField = info.heatmap.fields[countFieldIdx];
|
||||||
|
|
||||||
const { min, max } = reduceField({ field: countField, reducers: [ReducerID.min, ReducerID.max] });
|
// TODO -- better would be to get the range from the real color scale!
|
||||||
|
let { min, max } = options.color;
|
||||||
|
if (min == null || max == null) {
|
||||||
|
const calc = reduceField({ field: countField, reducers: [ReducerID.min, ReducerID.max] });
|
||||||
|
if (min == null) {
|
||||||
|
min = calc[ReducerID.min];
|
||||||
|
}
|
||||||
|
if (max == null) {
|
||||||
|
max = calc[ReducerID.max];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let hoverValue: number | undefined = undefined;
|
let hoverValue: number | undefined = undefined;
|
||||||
// seriesIdx: 1 is heatmap layer; 2 is exemplar layer
|
// seriesIdx: 1 is heatmap layer; 2 is exemplar layer
|
||||||
@@ -154,7 +167,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
|
|||||||
return (
|
return (
|
||||||
<VizLayout.Legend placement="bottom" maxHeight="20%">
|
<VizLayout.Legend placement="bottom" maxHeight="20%">
|
||||||
<div className={styles.colorScaleWrapper}>
|
<div className={styles.colorScaleWrapper}>
|
||||||
<ColorScale hoverValue={hoverValue} colorPalette={palette} min={min} max={max} display={info.display} />
|
<ColorScale hoverValue={hoverValue} colorPalette={palette} min={min!} max={max!} display={info.display} />
|
||||||
</div>
|
</div>
|
||||||
</VizLayout.Legend>
|
</VizLayout.Legend>
|
||||||
);
|
);
|
||||||
@@ -209,5 +222,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
colorScaleWrapper: css`
|
colorScaleWrapper: css`
|
||||||
margin-left: 25px;
|
margin-left: 25px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
max-width: 300px;
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getHeatmapData(bucketsToScanlines({ ...options.bucket, frame: bucketHeatmap }), exemplars, theme);
|
return getHeatmapData(bucketsToScanlines({ ...options.bucketFrame, frame: bucketHeatmap }), exemplars, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSparseHeatmapData = (
|
const getSparseHeatmapData = (
|
||||||
@@ -139,7 +139,7 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them
|
|||||||
|
|
||||||
const data: HeatmapData = {
|
const data: HeatmapData = {
|
||||||
heatmap: frame,
|
heatmap: frame,
|
||||||
exemplars,
|
exemplars: exemplars?.length ? exemplars : undefined,
|
||||||
xBucketSize: xBinIncr,
|
xBucketSize: xBinIncr,
|
||||||
yBucketSize: yBinIncr,
|
yBucketSize: yBinIncr,
|
||||||
xBucketCount: xBinQty,
|
xBucketCount: xBinQty,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -21,11 +21,14 @@ describe('Heatmap Migrations', () => {
|
|||||||
expect(panel).toMatchInlineSnapshot(`
|
expect(panel).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"fieldConfig": Object {
|
"fieldConfig": Object {
|
||||||
"defaults": Object {},
|
"defaults": Object {
|
||||||
|
"decimals": 6,
|
||||||
|
"unit": "short",
|
||||||
|
},
|
||||||
"overrides": Array [],
|
"overrides": Array [],
|
||||||
},
|
},
|
||||||
"options": Object {
|
"options": Object {
|
||||||
"bucket": Object {
|
"bucketFrame": Object {
|
||||||
"layout": "auto",
|
"layout": "auto",
|
||||||
},
|
},
|
||||||
"calculate": true,
|
"calculate": true,
|
||||||
@@ -44,7 +47,7 @@ describe('Heatmap Migrations', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"cellGap": 2,
|
"cellGap": 2,
|
||||||
"cellSize": 10,
|
"cellRadius": 10,
|
||||||
"color": Object {
|
"color": Object {
|
||||||
"exponent": 0.5,
|
"exponent": 0.5,
|
||||||
"fill": "dark-orange",
|
"fill": "dark-orange",
|
||||||
@@ -59,7 +62,7 @@ describe('Heatmap Migrations', () => {
|
|||||||
"color": "rgba(255,0,255,0.7)",
|
"color": "rgba(255,0,255,0.7)",
|
||||||
},
|
},
|
||||||
"filterValues": Object {
|
"filterValues": Object {
|
||||||
"min": 1e-9,
|
"le": 1e-9,
|
||||||
},
|
},
|
||||||
"legend": Object {
|
"legend": Object {
|
||||||
"show": true,
|
"show": true,
|
||||||
@@ -72,6 +75,8 @@ describe('Heatmap Migrations', () => {
|
|||||||
"yAxis": Object {
|
"yAxis": Object {
|
||||||
"axisPlacement": "left",
|
"axisPlacement": "left",
|
||||||
"axisWidth": 400,
|
"axisWidth": 400,
|
||||||
|
"max": 22,
|
||||||
|
"min": 7,
|
||||||
"reverse": false,
|
"reverse": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -133,11 +138,11 @@ const oldHeatmap = {
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
show: true,
|
show: true,
|
||||||
format: 'short',
|
format: 'short',
|
||||||
decimals: null,
|
decimals: 6,
|
||||||
logBase: 2,
|
logBase: 2,
|
||||||
splitFactor: 3,
|
splitFactor: 3,
|
||||||
min: null,
|
min: 7,
|
||||||
max: null,
|
max: 22,
|
||||||
width: '400',
|
width: '400',
|
||||||
},
|
},
|
||||||
xBucketSize: null,
|
xBucketSize: null,
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldConfig.defaults.unit = oldYAxis.format;
|
||||||
|
fieldConfig.defaults.decimals = oldYAxis.decimals;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: PanelOptions = {
|
const options: PanelOptions = {
|
||||||
@@ -69,14 +72,16 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
|
|||||||
...defaultPanelOptions.color,
|
...defaultPanelOptions.color,
|
||||||
steps: 128, // best match with existing colors
|
steps: 128, // best match with existing colors
|
||||||
},
|
},
|
||||||
cellGap: asNumber(angular.cards?.cardPadding),
|
cellGap: asNumber(angular.cards?.cardPadding, 2),
|
||||||
cellSize: asNumber(angular.cards?.cardRound),
|
cellRadius: asNumber(angular.cards?.cardRound), // just to keep it
|
||||||
yAxis: {
|
yAxis: {
|
||||||
axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left,
|
axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left,
|
||||||
reverse: Boolean(angular.reverseYBuckets),
|
reverse: Boolean(angular.reverseYBuckets),
|
||||||
axisWidth: oldYAxis.width ? +oldYAxis.width : undefined,
|
axisWidth: oldYAxis.width ? +oldYAxis.width : undefined,
|
||||||
|
min: oldYAxis.min,
|
||||||
|
max: oldYAxis.max,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucketFrame: {
|
||||||
layout: getHeatmapBucketLayout(angular.yBucketBound),
|
layout: getHeatmapBucketLayout(angular.yBucketBound),
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
@@ -134,9 +139,12 @@ function getHeatmapBucketLayout(v?: string): HeatmapBucketLayout {
|
|||||||
return HeatmapBucketLayout.auto;
|
return HeatmapBucketLayout.auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asNumber(v: any): number | undefined {
|
function asNumber(v: any, defaultValue?: number): number | undefined {
|
||||||
|
if (v == null || v === '') {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
const num = +v;
|
const num = +v;
|
||||||
return isNaN(num) ? undefined : num;
|
return isNaN(num) ? defaultValue : num;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const heatmapMigrationHandler = (panel: PanelModel): Partial<PanelOptions> => {
|
export const heatmapMigrationHandler = (panel: PanelModel): Partial<PanelOptions> => {
|
||||||
|
|||||||
@@ -34,11 +34,14 @@ export interface YAxisConfig extends AxisConfig {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
reverse?: boolean;
|
reverse?: boolean;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
|
// Only used when the axis is not ordinal
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterValueRange {
|
export interface FilterValueRange {
|
||||||
min?: number;
|
le?: number;
|
||||||
max?: number;
|
ge?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeatmapTooltip {
|
export interface HeatmapTooltip {
|
||||||
@@ -53,8 +56,8 @@ export interface ExemplarConfig {
|
|||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BucketOptions {
|
export interface BucketFrameOptions {
|
||||||
name?: string;
|
value?: string; // value field name
|
||||||
layout?: HeatmapBucketLayout;
|
layout?: HeatmapBucketLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,11 +67,11 @@ export interface PanelOptions {
|
|||||||
|
|
||||||
color: HeatmapColorOptions;
|
color: HeatmapColorOptions;
|
||||||
filterValues?: FilterValueRange; // was hideZeroBuckets
|
filterValues?: FilterValueRange; // was hideZeroBuckets
|
||||||
bucket?: BucketOptions;
|
bucketFrame?: BucketFrameOptions;
|
||||||
showValue: VisibilityMode;
|
showValue: VisibilityMode;
|
||||||
|
|
||||||
cellGap?: number; // was cardPadding
|
cellGap?: number; // was cardPadding
|
||||||
cellSize?: number; // was cardRadius
|
cellRadius?: number; // was cardRadius (not used, but migrated from angular)
|
||||||
|
|
||||||
yAxis: YAxisConfig;
|
yAxis: YAxisConfig;
|
||||||
legend: HeatmapLegend;
|
legend: HeatmapLegend;
|
||||||
@@ -87,7 +90,7 @@ export const defaultPanelOptions: PanelOptions = {
|
|||||||
exponent: 0.5,
|
exponent: 0.5,
|
||||||
steps: 64,
|
steps: 64,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucketFrame: {
|
||||||
layout: HeatmapBucketLayout.auto,
|
layout: HeatmapBucketLayout.auto,
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
@@ -105,7 +108,7 @@ export const defaultPanelOptions: PanelOptions = {
|
|||||||
color: 'rgba(255,0,255,0.7)',
|
color: 'rgba(255,0,255,0.7)',
|
||||||
},
|
},
|
||||||
filterValues: {
|
filterValues: {
|
||||||
min: 1e-9,
|
le: 1e-9,
|
||||||
},
|
},
|
||||||
cellGap: 1,
|
cellGap: 1,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -63,33 +63,10 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
|
|
||||||
if (opts.calculate) {
|
if (opts.calculate) {
|
||||||
addHeatmapCalculationOptions('calculation.', builder, opts.calculation, category);
|
addHeatmapCalculationOptions('calculation.', builder, opts.calculation, category);
|
||||||
} else {
|
|
||||||
builder.addTextInput({
|
|
||||||
path: 'bucket.name',
|
|
||||||
name: 'Cell value name',
|
|
||||||
defaultValue: defaultPanelOptions.bucket?.name,
|
|
||||||
settings: {
|
|
||||||
placeholder: 'Value',
|
|
||||||
},
|
|
||||||
category,
|
|
||||||
});
|
|
||||||
builder.addRadio({
|
|
||||||
path: 'bucket.layout',
|
|
||||||
name: 'Layout',
|
|
||||||
defaultValue: defaultPanelOptions.bucket?.layout ?? HeatmapBucketLayout.auto,
|
|
||||||
category,
|
|
||||||
settings: {
|
|
||||||
options: [
|
|
||||||
{ label: 'Auto', value: HeatmapBucketLayout.auto },
|
|
||||||
{ label: 'Middle', value: HeatmapBucketLayout.unknown },
|
|
||||||
{ label: 'Lower (LE)', value: HeatmapBucketLayout.le },
|
|
||||||
{ label: 'Upper (GE)', value: HeatmapBucketLayout.ge },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
category = ['Y Axis'];
|
category = ['Y Axis'];
|
||||||
|
|
||||||
builder.addRadio({
|
builder.addRadio({
|
||||||
path: 'yAxis.axisPlacement',
|
path: 'yAxis.axisPlacement',
|
||||||
name: 'Placement',
|
name: 'Placement',
|
||||||
@@ -104,6 +81,27 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: support clamping the min/max range when there is a real axis
|
||||||
|
if (false && opts.calculate) {
|
||||||
|
builder
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'yAxis.min',
|
||||||
|
name: 'Min value',
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
.addTextInput({
|
||||||
|
path: 'yAxis.max',
|
||||||
|
name: 'Max value',
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.addNumberInput({
|
.addNumberInput({
|
||||||
path: 'yAxis.axisWidth',
|
path: 'yAxis.axisWidth',
|
||||||
@@ -123,14 +121,31 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
placeholder: 'Auto',
|
placeholder: 'Auto',
|
||||||
},
|
},
|
||||||
category,
|
category,
|
||||||
})
|
|
||||||
.addBooleanSwitch({
|
|
||||||
path: 'yAxis.reverse',
|
|
||||||
name: 'Reverse',
|
|
||||||
defaultValue: defaultPanelOptions.yAxis.reverse === true,
|
|
||||||
category,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!opts.calculate) {
|
||||||
|
builder.addRadio({
|
||||||
|
path: 'bucketFrame.layout',
|
||||||
|
name: 'Tick alignment',
|
||||||
|
defaultValue: defaultPanelOptions.bucketFrame?.layout ?? HeatmapBucketLayout.auto,
|
||||||
|
category,
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ label: 'Auto', value: HeatmapBucketLayout.auto },
|
||||||
|
{ label: 'Top (LE)', value: HeatmapBucketLayout.le },
|
||||||
|
{ label: 'Middle', value: HeatmapBucketLayout.unknown },
|
||||||
|
{ label: 'Bottom (GE)', value: HeatmapBucketLayout.ge },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
builder.addBooleanSwitch({
|
||||||
|
path: 'yAxis.reverse',
|
||||||
|
name: 'Reverse',
|
||||||
|
defaultValue: defaultPanelOptions.yAxis.reverse === true,
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
|
||||||
category = ['Colors'];
|
category = ['Colors'];
|
||||||
|
|
||||||
builder.addRadio({
|
builder.addRadio({
|
||||||
@@ -225,6 +240,26 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'color.min',
|
||||||
|
name: 'Start color scale from value',
|
||||||
|
defaultValue: defaultPanelOptions.color.min,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto (min)',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'color.max',
|
||||||
|
name: 'End color scale at value',
|
||||||
|
defaultValue: defaultPanelOptions.color.max,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Auto (max)',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
|
||||||
category = ['Display'];
|
category = ['Display'];
|
||||||
|
|
||||||
builder
|
builder
|
||||||
@@ -241,12 +276,6 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
// ],
|
// ],
|
||||||
// },
|
// },
|
||||||
// })
|
// })
|
||||||
.addNumberInput({
|
|
||||||
path: 'filterValues.min',
|
|
||||||
name: 'Hide cell counts <=',
|
|
||||||
defaultValue: defaultPanelOptions.filterValues?.min,
|
|
||||||
category,
|
|
||||||
})
|
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
name: 'Cell gap',
|
name: 'Cell gap',
|
||||||
path: 'cellGap',
|
path: 'cellGap',
|
||||||
@@ -256,6 +285,24 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 25,
|
max: 25,
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'filterValues.le',
|
||||||
|
name: 'Hide cells with values <=',
|
||||||
|
defaultValue: defaultPanelOptions.filterValues?.le,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'None',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'filterValues.ge',
|
||||||
|
name: 'Hide cells with values >=',
|
||||||
|
defaultValue: defaultPanelOptions.filterValues?.ge,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'None',
|
||||||
|
},
|
||||||
|
category,
|
||||||
});
|
});
|
||||||
// .addSliderInput({
|
// .addSliderInput({
|
||||||
// name: 'Cell radius',
|
// name: 'Cell radius',
|
||||||
@@ -277,6 +324,18 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
|
|||||||
category,
|
category,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!opts.calculate) {
|
||||||
|
builder.addTextInput({
|
||||||
|
path: 'bucketFrame.value',
|
||||||
|
name: 'Cell value name',
|
||||||
|
defaultValue: defaultPanelOptions.bucketFrame?.value,
|
||||||
|
settings: {
|
||||||
|
placeholder: 'Value',
|
||||||
|
},
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
builder.addBooleanSwitch({
|
builder.addBooleanSwitch({
|
||||||
path: 'tooltip.yHistogram',
|
path: 'tooltip.yHistogram',
|
||||||
name: 'Show histogram (Y axis)',
|
name: 'Show histogram (Y axis)',
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
"state": "alpha",
|
"state": "alpha",
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
"description": "Next generation heatmap visualization",
|
"description": "Like a histogram over time",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Grafana Labs",
|
"name": "Grafana Labs",
|
||||||
"url": "https://grafana.com"
|
"url": "https://grafana.com"
|
||||||
},
|
},
|
||||||
"logos": {
|
"logos": {
|
||||||
"small": "img/heatmap.svg",
|
"small": "img/icn-heatmap-panel.svg",
|
||||||
"large": "img/heatmap.svg"
|
"large": "img/icn-heatmap-panel.svg"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { MutableRefObject, RefObject } from 'react';
|
import { MutableRefObject, RefObject } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import { DataFrameType, GrafanaTheme2, incrRoundDn, incrRoundUp, TimeRange } from '@grafana/data';
|
import {
|
||||||
|
DataFrameType,
|
||||||
|
formattedValueToString,
|
||||||
|
getValueFormat,
|
||||||
|
GrafanaTheme2,
|
||||||
|
incrRoundDn,
|
||||||
|
incrRoundUp,
|
||||||
|
TimeRange,
|
||||||
|
} from '@grafana/data';
|
||||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema';
|
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema';
|
||||||
import { UPlotConfigBuilder } from '@grafana/ui';
|
import { UPlotConfigBuilder } from '@grafana/ui';
|
||||||
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
|
||||||
@@ -15,7 +23,8 @@ import { PanelFieldConfig, YAxisConfig } from './models.gen';
|
|||||||
interface PathbuilderOpts {
|
interface PathbuilderOpts {
|
||||||
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
||||||
gap?: number | null;
|
gap?: number | null;
|
||||||
hideThreshold?: number;
|
hideLE?: number;
|
||||||
|
hideGE?: number;
|
||||||
xAlign?: -1 | 0 | 1;
|
xAlign?: -1 | 0 | 1;
|
||||||
yAlign?: -1 | 0 | 1;
|
yAlign?: -1 | 0 | 1;
|
||||||
ySizeDivisor?: number;
|
ySizeDivisor?: number;
|
||||||
@@ -55,7 +64,10 @@ interface PrepConfigOpts {
|
|||||||
palette: string[];
|
palette: string[];
|
||||||
exemplarColor: string;
|
exemplarColor: string;
|
||||||
cellGap?: number | null; // in css pixels
|
cellGap?: number | null; // in css pixels
|
||||||
hideThreshold?: number;
|
hideLE?: number;
|
||||||
|
hideGE?: number;
|
||||||
|
valueMin?: number;
|
||||||
|
valueMax?: number;
|
||||||
yAxisConfig: YAxisConfig;
|
yAxisConfig: YAxisConfig;
|
||||||
ySizeDivisor?: number;
|
ySizeDivisor?: number;
|
||||||
}
|
}
|
||||||
@@ -72,7 +84,10 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
getTimeRange,
|
getTimeRange,
|
||||||
palette,
|
palette,
|
||||||
cellGap,
|
cellGap,
|
||||||
hideThreshold,
|
hideLE,
|
||||||
|
hideGE,
|
||||||
|
valueMin,
|
||||||
|
valueMax,
|
||||||
yAxisConfig,
|
yAxisConfig,
|
||||||
ySizeDivisor,
|
ySizeDivisor,
|
||||||
} = opts;
|
} = opts;
|
||||||
@@ -289,12 +304,12 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
// how to expand scale range if inferred non-regular or log buckets?
|
// how to expand scale range if inferred non-regular or log buckets?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [dataMin, dataMax];
|
return [dataMin, dataMax];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasLabeledY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null;
|
const isOrdianalY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null;
|
||||||
|
const disp = dataRef.current?.heatmap?.fields[1].display ?? getValueFormat('short');
|
||||||
|
|
||||||
builder.addAxis({
|
builder.addAxis({
|
||||||
scaleKey: 'y',
|
scaleKey: 'y',
|
||||||
@@ -303,35 +318,51 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
size: yAxisConfig.axisWidth || null,
|
size: yAxisConfig.axisWidth || null,
|
||||||
label: yAxisConfig.axisLabel,
|
label: yAxisConfig.axisLabel,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
splits: hasLabeledY
|
formatValue: (v: any) => formattedValueToString(disp(v)),
|
||||||
? () => {
|
splits: isOrdianalY
|
||||||
const ys = dataRef.current?.heatmap?.fields[1].values.toArray()!;
|
? (self: uPlot) => {
|
||||||
const splits = ys.slice(0, ys.length - ys.lastIndexOf(ys[0]));
|
const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap);
|
||||||
|
if (!meta.yOrdinalDisplay) {
|
||||||
|
return [0, 1]; //?
|
||||||
|
}
|
||||||
|
let splits = meta.yOrdinalDisplay.map((v, idx) => idx);
|
||||||
|
|
||||||
const bucketSize = dataRef.current?.yBucketSize!;
|
switch (dataRef.current?.yLayout) {
|
||||||
|
case HeatmapBucketLayout.le:
|
||||||
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
splits.unshift(-1);
|
||||||
splits.unshift(ys[0] - bucketSize);
|
break;
|
||||||
} else {
|
case HeatmapBucketLayout.ge:
|
||||||
splits.push(ys[ys.length - 1] + bucketSize);
|
splits.push(splits.length);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip labels when the height is too small
|
||||||
|
if (self.height < 60) {
|
||||||
|
splits = [splits[0], splits[splits.length - 1]];
|
||||||
|
} else {
|
||||||
|
while (splits.length > 3 && (self.height - 15) / splits.length < 10) {
|
||||||
|
splits = splits.filter((v, idx) => idx % 2 === 0); // remove half the items
|
||||||
|
}
|
||||||
|
}
|
||||||
return splits;
|
return splits;
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
values: hasLabeledY
|
values: isOrdianalY
|
||||||
? () => {
|
? (self: uPlot, splits) => {
|
||||||
const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap);
|
const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap);
|
||||||
const yAxisValues = meta.yOrdinalDisplay?.slice()!;
|
if (meta.yOrdinalDisplay) {
|
||||||
const isFromBuckets = meta.yOrdinalDisplay?.length && !('le' === meta.yMatchWithLabel);
|
return splits.map((v) => {
|
||||||
|
const txt = meta.yOrdinalDisplay[v];
|
||||||
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
|
if (!txt && v < 0) {
|
||||||
yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish
|
// Check prometheus style labels
|
||||||
} else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) {
|
if ('le' === meta.yMatchWithLabel) {
|
||||||
yAxisValues.push(isFromBuckets ? '' : '+Inf');
|
return '0.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return txt;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return splits.map((v) => `${v}`);
|
||||||
return yAxisValues;
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
@@ -363,7 +394,8 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
gap: cellGap,
|
gap: cellGap,
|
||||||
hideThreshold,
|
hideLE,
|
||||||
|
hideGE,
|
||||||
xAlign:
|
xAlign:
|
||||||
dataRef.current?.xLayout === HeatmapBucketLayout.le
|
dataRef.current?.xLayout === HeatmapBucketLayout.le
|
||||||
? -1
|
? -1
|
||||||
@@ -380,7 +412,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
fill: {
|
fill: {
|
||||||
values: (u, seriesIdx) => {
|
values: (u, seriesIdx) => {
|
||||||
let countFacetIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3;
|
let countFacetIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3;
|
||||||
return countsToFills(u.data[seriesIdx][countFacetIdx] as unknown as number[], palette);
|
return valuesToFills(u.data[seriesIdx][countFacetIdx] as unknown as number[], palette, valueMin, valueMax);
|
||||||
},
|
},
|
||||||
index: palette,
|
index: palette,
|
||||||
},
|
},
|
||||||
@@ -465,7 +497,7 @@ export function prepConfig(opts: PrepConfigOpts) {
|
|||||||
const CRISP_EDGES_GAP_MIN = 4;
|
const CRISP_EDGES_GAP_MIN = 4;
|
||||||
|
|
||||||
export function heatmapPathsDense(opts: PathbuilderOpts) {
|
export function heatmapPathsDense(opts: PathbuilderOpts) {
|
||||||
const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts;
|
const { disp, each, gap = 1, hideLE = -Infinity, hideGE = Infinity, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts;
|
||||||
|
|
||||||
const pxRatio = devicePixelRatio;
|
const pxRatio = devicePixelRatio;
|
||||||
|
|
||||||
@@ -549,14 +581,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < dlen; i++) {
|
for (let i = 0; i < dlen; i++) {
|
||||||
// filter out 0 counts and out of view
|
if (counts[i] > hideLE && counts[i] < hideGE) {
|
||||||
if (
|
|
||||||
counts[i] > hideThreshold &&
|
|
||||||
xs[i] + xBinIncr >= scaleX.min! &&
|
|
||||||
xs[i] - xBinIncr <= scaleX.max! &&
|
|
||||||
ys[i] + yBinIncr >= scaleY.min! &&
|
|
||||||
ys[i] - yBinIncr <= scaleY.max!
|
|
||||||
) {
|
|
||||||
let cx = cxs[~~(i / yBinQty)];
|
let cx = cxs[~~(i / yBinQty)];
|
||||||
let cy = cys[i % yBinQty];
|
let cy = cys[i % yBinQty];
|
||||||
|
|
||||||
@@ -646,7 +671,7 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin
|
|||||||
// accepts xMax, yMin, yMax, count
|
// accepts xMax, yMin, yMax, count
|
||||||
// xbinsize? x tile sizes are uniform?
|
// xbinsize? x tile sizes are uniform?
|
||||||
export function heatmapPathsSparse(opts: PathbuilderOpts) {
|
export function heatmapPathsSparse(opts: PathbuilderOpts) {
|
||||||
const { disp, each, gap = 1, hideThreshold = 0 } = opts;
|
const { disp, each, gap = 1, hideLE = -Infinity, hideGE = Infinity } = opts;
|
||||||
|
|
||||||
const pxRatio = devicePixelRatio;
|
const pxRatio = devicePixelRatio;
|
||||||
|
|
||||||
@@ -717,7 +742,7 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
|
|||||||
let xSizeUniform = xOffs.get(xMaxs.find((v) => v !== xMaxs[0])) - xOffs.get(xMaxs[0]);
|
let xSizeUniform = xOffs.get(xMaxs.find((v) => v !== xMaxs[0])) - xOffs.get(xMaxs[0]);
|
||||||
|
|
||||||
for (let i = 0; i < dlen; i++) {
|
for (let i = 0; i < dlen; i++) {
|
||||||
if (counts[i] <= hideThreshold) {
|
if (counts[i] <= hideLE || counts[i] >= hideGE) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,19 +764,11 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
|
|||||||
let x = xMaxPx;
|
let x = xMaxPx;
|
||||||
let y = yMinPx;
|
let y = yMinPx;
|
||||||
|
|
||||||
// filter out 0 counts and out of view
|
|
||||||
// if (
|
|
||||||
// xs[i] + xBinIncr >= scaleX.min! &&
|
|
||||||
// xs[i] - xBinIncr <= scaleX.max! &&
|
|
||||||
// ys[i] + yBinIncr >= scaleY.min! &&
|
|
||||||
// ys[i] - yBinIncr <= scaleY.max!
|
|
||||||
// ) {
|
|
||||||
let fillPath = fillPaths[fills[i]];
|
let fillPath = fillPaths[fills[i]];
|
||||||
|
|
||||||
rect(fillPath, x, y, xSize, ySize);
|
rect(fillPath, x, y, xSize, ySize);
|
||||||
|
|
||||||
each(u, 1, i, x, y, xSize, ySize);
|
each(u, 1, i, x, y, xSize, ySize);
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u.ctx.save();
|
u.ctx.save();
|
||||||
@@ -772,29 +789,36 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const countsToFills = (counts: number[], palette: string[]) => {
|
export const valuesToFills = (values: number[], palette: string[], minValue?: number, maxValue?: number) => {
|
||||||
// TODO: integrate 1e-9 hideThreshold?
|
if (minValue == null) {
|
||||||
const hideThreshold = 0;
|
minValue = Infinity;
|
||||||
|
|
||||||
let minCount = Infinity;
|
for (let i = 0; i < values.length; i++) {
|
||||||
let maxCount = -Infinity;
|
minValue = Math.min(minValue, values[i]);
|
||||||
|
|
||||||
for (let i = 0; i < counts.length; i++) {
|
|
||||||
if (counts[i] > hideThreshold) {
|
|
||||||
minCount = Math.min(minCount, counts[i]);
|
|
||||||
maxCount = Math.max(maxCount, counts[i]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let range = maxCount - minCount;
|
if (maxValue == null) {
|
||||||
|
maxValue = -Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
maxValue = Math.max(maxValue, values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = maxValue - minValue;
|
||||||
|
|
||||||
let paletteSize = palette.length;
|
let paletteSize = palette.length;
|
||||||
|
|
||||||
let indexedFills = Array(counts.length);
|
let indexedFills = Array(values.length);
|
||||||
|
|
||||||
for (let i = 0; i < counts.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
indexedFills[i] =
|
indexedFills[i] =
|
||||||
counts[i] === 0 ? -1 : Math.min(paletteSize - 1, Math.floor((paletteSize * (counts[i] - minCount)) / range));
|
values[i] < minValue
|
||||||
|
? 0
|
||||||
|
: values[i] > maxValue
|
||||||
|
? paletteSize - 1
|
||||||
|
: Math.min(paletteSize - 1, Math.floor((paletteSize * (values[i] - minValue)) / range));
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexedFills;
|
return indexedFills;
|
||||||
|
|||||||
Reference in New Issue
Block a user