HeatmapNG: cell value filtering and color clamping (#50204)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Leon Sorokin 2022-06-06 02:21:47 -05:00 committed by GitHub
parent fac8db8ff6
commit 8cdfef4796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 265 additions and 151 deletions

View File

@ -23,7 +23,7 @@
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"graphTooltip": 1,
"links": [],
"liveNow": false,
"panels": [
@ -172,7 +172,7 @@
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"min": 1e-9
"le": 1e-9
},
"legend": {
"show": true
@ -257,7 +257,7 @@
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"min": 1e-9
"le": 1e-9
},
"legend": {
"show": true
@ -333,7 +333,7 @@
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"min": 1e-9
"le": 1e-9
},
"legend": {
"show": true
@ -417,7 +417,7 @@
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"min": 1e-9
"le": 1e-9
},
"legend": {
"show": true
@ -502,7 +502,7 @@
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"min": 1e-9
"le": 1e-9
},
"legend": {
"show": true
@ -548,6 +548,6 @@
"timezone": "",
"title": "Heatmap calculate (log)",
"uid": "ZXYQTA97ZZ",
"version": 4,
"version": 1,
"weekStart": ""
}

View File

@ -26,7 +26,7 @@ const GRADIENT_STOPS = 10;
export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useStopsPercentage }: Props) => {
const [colors, setColors] = useState<string[]>([]);
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 styles = getStyles(theme, colors);
@ -50,15 +50,12 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useSto
};
useEffect(() => {
if (hoverValue != null) {
const percent = hoverValue / (max - min);
setPercent(percent * 100);
}
setPercent(hoverValue == null ? null : clampPercent100((hoverValue - min) / (max - min)));
}, [hoverValue, min, max]);
return (
<div className={styles.scaleWrapper}>
<div className={styles.scaleGradient} onMouseMove={onScaleMouseMove} onMouseLeave={onScaleMouseLeave}>
<div className={styles.scaleWrapper} onMouseMove={onScaleMouseMove} onMouseLeave={onScaleMouseLeave}>
<div className={styles.scaleGradient}>
{display && (scaleHover.isShown || hoverValue !== undefined) && (
<div className={styles.followerContainer}>
<div className={styles.follower} style={{ left: `${percent}%` }} />
@ -121,10 +118,19 @@ const getGradientStops = ({
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[]) => ({
scaleWrapper: css`
width: 100%;
max-width: 300px;
font-size: 11px;
opacity: 1;
`,
@ -138,7 +144,7 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({
`,
hoverValue: css`
position: absolute;
padding-top: 5px;
padding-top: 4px;
`,
followerContainer: css`
position: relative;

View File

@ -24,11 +24,6 @@ const logModeOptions: Array<SelectableValue<HeatmapCalculationMode>> = [
value: HeatmapCalculationMode.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>> = ({

View File

@ -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(`
Array [
Object {

View File

@ -63,7 +63,7 @@ export function readHeatmapScanlinesCustomMeta(frame?: DataFrame): HeatmapScanli
export interface BucketsOptions {
frame: DataFrame;
name?: string;
value?: string; // the field value name
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,
values: new ArrayVector(counts2),
config: yFields[0].config,

View File

@ -57,7 +57,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
let exemplarsyFacet: number[] = [];
const meta = readHeatmapScanlinesCustomMeta(info.heatmap);
if (info.exemplars && meta.yMatchWithLabel) {
if (info.exemplars?.length && meta.yMatchWithLabel) {
exemplarsXFacet = info.exemplars?.fields[0].values.toArray();
// ordinal/labeled heatmap-buckets?
@ -126,7 +126,10 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
getTimeRange: () => timeRangeRef.current,
palette,
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)',
yAxisConfig: options.yAxis,
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;
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;
// seriesIdx: 1 is heatmap layer; 2 is exemplar layer
@ -154,7 +167,7 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
return (
<VizLayout.Legend placement="bottom" maxHeight="20%">
<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>
</VizLayout.Legend>
);
@ -209,5 +222,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
colorScaleWrapper: css`
margin-left: 25px;
padding: 10px 0;
max-width: 300px;
`,
});

View File

@ -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 = (
@ -139,7 +139,7 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them
const data: HeatmapData = {
heatmap: frame,
exemplars,
exemplars: exemplars?.length ? exemplars : undefined,
xBucketSize: xBinIncr,
yBucketSize: yBinIncr,
xBucketCount: xBinQty,

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -21,11 +21,14 @@ describe('Heatmap Migrations', () => {
expect(panel).toMatchInlineSnapshot(`
Object {
"fieldConfig": Object {
"defaults": Object {},
"defaults": Object {
"decimals": 6,
"unit": "short",
},
"overrides": Array [],
},
"options": Object {
"bucket": Object {
"bucketFrame": Object {
"layout": "auto",
},
"calculate": true,
@ -44,7 +47,7 @@ describe('Heatmap Migrations', () => {
},
},
"cellGap": 2,
"cellSize": 10,
"cellRadius": 10,
"color": Object {
"exponent": 0.5,
"fill": "dark-orange",
@ -59,7 +62,7 @@ describe('Heatmap Migrations', () => {
"color": "rgba(255,0,255,0.7)",
},
"filterValues": Object {
"min": 1e-9,
"le": 1e-9,
},
"legend": Object {
"show": true,
@ -72,6 +75,8 @@ describe('Heatmap Migrations', () => {
"yAxis": Object {
"axisPlacement": "left",
"axisWidth": 400,
"max": 22,
"min": 7,
"reverse": false,
},
},
@ -133,11 +138,11 @@ const oldHeatmap = {
yAxis: {
show: true,
format: 'short',
decimals: null,
decimals: 6,
logBase: 2,
splitFactor: 3,
min: null,
max: null,
min: 7,
max: 22,
width: '400',
},
xBucketSize: null,

View File

@ -60,6 +60,9 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
},
};
}
fieldConfig.defaults.unit = oldYAxis.format;
fieldConfig.defaults.decimals = oldYAxis.decimals;
}
const options: PanelOptions = {
@ -69,14 +72,16 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS
...defaultPanelOptions.color,
steps: 128, // best match with existing colors
},
cellGap: asNumber(angular.cards?.cardPadding),
cellSize: asNumber(angular.cards?.cardRound),
cellGap: asNumber(angular.cards?.cardPadding, 2),
cellRadius: asNumber(angular.cards?.cardRound), // just to keep it
yAxis: {
axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left,
reverse: Boolean(angular.reverseYBuckets),
axisWidth: oldYAxis.width ? +oldYAxis.width : undefined,
min: oldYAxis.min,
max: oldYAxis.max,
},
bucket: {
bucketFrame: {
layout: getHeatmapBucketLayout(angular.yBucketBound),
},
legend: {
@ -134,9 +139,12 @@ function getHeatmapBucketLayout(v?: string): HeatmapBucketLayout {
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;
return isNaN(num) ? undefined : num;
return isNaN(num) ? defaultValue : num;
}
export const heatmapMigrationHandler = (panel: PanelModel): Partial<PanelOptions> => {

View File

@ -34,11 +34,14 @@ export interface YAxisConfig extends AxisConfig {
unit?: string;
reverse?: boolean;
decimals?: number;
// Only used when the axis is not ordinal
min?: number;
max?: number;
}
export interface FilterValueRange {
min?: number;
max?: number;
le?: number;
ge?: number;
}
export interface HeatmapTooltip {
@ -53,8 +56,8 @@ export interface ExemplarConfig {
color: string;
}
export interface BucketOptions {
name?: string;
export interface BucketFrameOptions {
value?: string; // value field name
layout?: HeatmapBucketLayout;
}
@ -64,11 +67,11 @@ export interface PanelOptions {
color: HeatmapColorOptions;
filterValues?: FilterValueRange; // was hideZeroBuckets
bucket?: BucketOptions;
bucketFrame?: BucketFrameOptions;
showValue: VisibilityMode;
cellGap?: number; // was cardPadding
cellSize?: number; // was cardRadius
cellRadius?: number; // was cardRadius (not used, but migrated from angular)
yAxis: YAxisConfig;
legend: HeatmapLegend;
@ -87,7 +90,7 @@ export const defaultPanelOptions: PanelOptions = {
exponent: 0.5,
steps: 64,
},
bucket: {
bucketFrame: {
layout: HeatmapBucketLayout.auto,
},
yAxis: {
@ -105,7 +108,7 @@ export const defaultPanelOptions: PanelOptions = {
color: 'rgba(255,0,255,0.7)',
},
filterValues: {
min: 1e-9,
le: 1e-9,
},
cellGap: 1,
};

View File

@ -63,33 +63,10 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
if (opts.calculate) {
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'];
builder.addRadio({
path: 'yAxis.axisPlacement',
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
.addNumberInput({
path: 'yAxis.axisWidth',
@ -123,14 +121,31 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
placeholder: 'Auto',
},
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'];
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'];
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({
name: 'Cell gap',
path: 'cellGap',
@ -256,6 +285,24 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
min: 0,
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({
// name: 'Cell radius',
@ -277,6 +324,18 @@ export const plugin = new PanelPlugin<PanelOptions, GraphFieldConfig>(HeatmapPan
category,
});
if (!opts.calculate) {
builder.addTextInput({
path: 'bucketFrame.value',
name: 'Cell value name',
defaultValue: defaultPanelOptions.bucketFrame?.value,
settings: {
placeholder: 'Value',
},
category,
});
}
builder.addBooleanSwitch({
path: 'tooltip.yHistogram',
name: 'Show histogram (Y axis)',

View File

@ -5,14 +5,14 @@
"state": "alpha",
"info": {
"description": "Next generation heatmap visualization",
"description": "Like a histogram over time",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
},
"logos": {
"small": "img/heatmap.svg",
"large": "img/heatmap.svg"
"small": "img/icn-heatmap-panel.svg",
"large": "img/icn-heatmap-panel.svg"
}
}
}

View File

@ -1,7 +1,15 @@
import { MutableRefObject, RefObject } from 'react';
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 { UPlotConfigBuilder } from '@grafana/ui';
import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
@ -15,7 +23,8 @@ import { PanelFieldConfig, YAxisConfig } from './models.gen';
interface PathbuilderOpts {
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
gap?: number | null;
hideThreshold?: number;
hideLE?: number;
hideGE?: number;
xAlign?: -1 | 0 | 1;
yAlign?: -1 | 0 | 1;
ySizeDivisor?: number;
@ -55,7 +64,10 @@ interface PrepConfigOpts {
palette: string[];
exemplarColor: string;
cellGap?: number | null; // in css pixels
hideThreshold?: number;
hideLE?: number;
hideGE?: number;
valueMin?: number;
valueMax?: number;
yAxisConfig: YAxisConfig;
ySizeDivisor?: number;
}
@ -72,7 +84,10 @@ export function prepConfig(opts: PrepConfigOpts) {
getTimeRange,
palette,
cellGap,
hideThreshold,
hideLE,
hideGE,
valueMin,
valueMax,
yAxisConfig,
ySizeDivisor,
} = opts;
@ -289,12 +304,12 @@ export function prepConfig(opts: PrepConfigOpts) {
// how to expand scale range if inferred non-regular or log buckets?
}
}
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({
scaleKey: 'y',
@ -303,35 +318,51 @@ export function prepConfig(opts: PrepConfigOpts) {
size: yAxisConfig.axisWidth || null,
label: yAxisConfig.axisLabel,
theme: theme,
splits: hasLabeledY
? () => {
const ys = dataRef.current?.heatmap?.fields[1].values.toArray()!;
const splits = ys.slice(0, ys.length - ys.lastIndexOf(ys[0]));
formatValue: (v: any) => formattedValueToString(disp(v)),
splits: isOrdianalY
? (self: uPlot) => {
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!;
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
splits.unshift(ys[0] - bucketSize);
} else {
splits.push(ys[ys.length - 1] + bucketSize);
switch (dataRef.current?.yLayout) {
case HeatmapBucketLayout.le:
splits.unshift(-1);
break;
case HeatmapBucketLayout.ge:
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;
}
: undefined,
values: hasLabeledY
? () => {
values: isOrdianalY
? (self: uPlot, splits) => {
const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap);
const yAxisValues = meta.yOrdinalDisplay?.slice()!;
const isFromBuckets = meta.yOrdinalDisplay?.length && !('le' === meta.yMatchWithLabel);
if (dataRef.current?.yLayout === HeatmapBucketLayout.le) {
yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish
} else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) {
yAxisValues.push(isFromBuckets ? '' : '+Inf');
if (meta.yOrdinalDisplay) {
return splits.map((v) => {
const txt = meta.yOrdinalDisplay[v];
if (!txt && v < 0) {
// Check prometheus style labels
if ('le' === meta.yMatchWithLabel) {
return '0.0';
}
}
return txt;
});
}
return yAxisValues;
return splits.map((v) => `${v}`);
}
: undefined,
});
@ -363,7 +394,8 @@ export function prepConfig(opts: PrepConfigOpts) {
});
},
gap: cellGap,
hideThreshold,
hideLE,
hideGE,
xAlign:
dataRef.current?.xLayout === HeatmapBucketLayout.le
? -1
@ -380,7 +412,7 @@ export function prepConfig(opts: PrepConfigOpts) {
fill: {
values: (u, seriesIdx) => {
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,
},
@ -465,7 +497,7 @@ export function prepConfig(opts: PrepConfigOpts) {
const CRISP_EDGES_GAP_MIN = 4;
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;
@ -549,14 +581,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) {
);
for (let i = 0; i < dlen; i++) {
// filter out 0 counts and out of view
if (
counts[i] > hideThreshold &&
xs[i] + xBinIncr >= scaleX.min! &&
xs[i] - xBinIncr <= scaleX.max! &&
ys[i] + yBinIncr >= scaleY.min! &&
ys[i] - yBinIncr <= scaleY.max!
) {
if (counts[i] > hideLE && counts[i] < hideGE) {
let cx = cxs[~~(i / yBinQty)];
let cy = cys[i % yBinQty];
@ -646,7 +671,7 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin
// accepts xMax, yMin, yMax, count
// xbinsize? x tile sizes are uniform?
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;
@ -717,7 +742,7 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
let xSizeUniform = xOffs.get(xMaxs.find((v) => v !== xMaxs[0])) - xOffs.get(xMaxs[0]);
for (let i = 0; i < dlen; i++) {
if (counts[i] <= hideThreshold) {
if (counts[i] <= hideLE || counts[i] >= hideGE) {
continue;
}
@ -739,19 +764,11 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
let x = xMaxPx;
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]];
rect(fillPath, x, y, xSize, ySize);
each(u, 1, i, x, y, xSize, ySize);
// }
}
u.ctx.save();
@ -772,29 +789,36 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) {
};
}
export const countsToFills = (counts: number[], palette: string[]) => {
// TODO: integrate 1e-9 hideThreshold?
const hideThreshold = 0;
export const valuesToFills = (values: number[], palette: string[], minValue?: number, maxValue?: number) => {
if (minValue == null) {
minValue = Infinity;
let minCount = Infinity;
let maxCount = -Infinity;
for (let i = 0; i < counts.length; i++) {
if (counts[i] > hideThreshold) {
minCount = Math.min(minCount, counts[i]);
maxCount = Math.max(maxCount, counts[i]);
for (let i = 0; i < values.length; i++) {
minValue = Math.min(minValue, values[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 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] =
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;