Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions components/src/utils/axis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { getFormattedAxis, getFormattedMultipleYAxes } from './axis';

interface AxisLabelShape {
showMinLabel?: boolean;
showMaxLabel?: boolean;
hideOverlap?: boolean;
}
interface AxisShape {
boundaryGap?: unknown;
axisLabel?: AxisLabelShape;
}

describe('getFormattedAxis', () => {
it('should always render the extreme (min/max) labels and thin only overlapping middle ticks', () => {
const [axis] = getFormattedAxis({}, { unit: 'percent', decimalPlaces: 0 }) as AxisShape[];

expect(axis?.axisLabel?.showMinLabel).toBe(true);
expect(axis?.axisLabel?.showMaxLabel).toBe(true);
expect(axis?.axisLabel?.hideOverlap).toBe(true);
});

it('should not apply the fixed 10% top padding so ECharts can round the axis max to a nice number', () => {
const [axis] = getFormattedAxis({}, { unit: 'percent', decimalPlaces: 0 }) as AxisShape[];

expect(axis?.boundaryGap).toBeUndefined();
});

it('should preserve caller-supplied axis options when merging', () => {
const [axis] = getFormattedAxis({ splitNumber: 4 } as never, { unit: 'decimal' }) as Array<
AxisShape & { splitNumber?: number }
>;

expect(axis?.splitNumber).toBe(4);
expect(axis?.axisLabel?.showMaxLabel).toBe(true);
});
});

describe('getFormattedMultipleYAxes', () => {
it('should force extreme labels and drop the fixed padding on the base (left) axis', () => {
const [baseAxis] = getFormattedMultipleYAxes(undefined, { unit: 'percent' }, []) as unknown as AxisShape[];

expect(baseAxis?.axisLabel?.showMinLabel).toBe(true);
expect(baseAxis?.axisLabel?.showMaxLabel).toBe(true);
expect(baseAxis?.axisLabel?.hideOverlap).toBe(true);
expect(baseAxis?.boundaryGap).toBeUndefined();
});

it('should force extreme labels and drop the fixed padding on additional (right) axes', () => {
const axes = getFormattedMultipleYAxes(undefined, { unit: 'percent' }, [
{ unit: 'decimal' },
]) as unknown as AxisShape[];
const rightAxis = axes[1];

expect(rightAxis?.axisLabel?.showMinLabel).toBe(true);
expect(rightAxis?.axisLabel?.showMaxLabel).toBe(true);
expect(rightAxis?.axisLabel?.hideOverlap).toBe(true);
expect(rightAxis?.boundaryGap).toBeUndefined();
});
});
17 changes: 14 additions & 3 deletions components/src/utils/axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import merge from 'lodash/merge';
import type { XAXisComponentOption, YAXisComponentOption } from 'echarts';
import { FormatOptions, formatValue } from '../model';

// LOGZ.IO CHANGE START:: Always render the axis extremes (top/bottom); hideOverlap thins only the middle.
const EXTREME_VALUE_AXIS_LABEL = {
showMinLabel: true,
showMaxLabel: true,
hideOverlap: true,
};
// LOGZ.IO CHANGE END::

export interface YAxisConfig {
format?: FormatOptions;
position?: 'left' | 'right';
Expand Down Expand Up @@ -50,8 +58,9 @@ function estimateLabelWidth(format: FormatOptions | undefined, maxValue: number)
export function getFormattedAxis(axis?: YAXisComponentOption | XAXisComponentOption, unit?: FormatOptions): unknown[] {
const AXIS_DEFAULT = {
type: 'value',
boundaryGap: [0, '10%'],
axisLabel: {
// LOGZ.IO CHANGE:: preserve extreme labels + even spacing
...EXTREME_VALUE_AXIS_LABEL,
formatter: (value: number): string => {
return formatValue(value, unit);
},
Expand Down Expand Up @@ -83,8 +92,9 @@ export function getFormattedMultipleYAxes(
{
type: 'value',
position: 'left',
boundaryGap: [0, '10%'],
axisLabel: {
// LOGZ.IO CHANGE:: preserve extreme labels + even spacing
...EXTREME_VALUE_AXIS_LABEL,
formatter: (value: number): string => {
return formatValue(value, baseFormat);
},
Expand All @@ -106,8 +116,9 @@ export function getFormattedMultipleYAxes(
position: 'right',
// Dynamic offset based on cumulative width of preceding axis labels
offset: cumulativeOffset,
boundaryGap: [0, '10%'],
axisLabel: {
// LOGZ.IO CHANGE:: preserve extreme labels + even spacing
...EXTREME_VALUE_AXIS_LABEL,
formatter: (value: number): string => {
return formatValue(value, format);
},
Expand Down
48 changes: 48 additions & 0 deletions components/src/utils/columnar-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@ describe('buildColumnarTimeChart', () => {
expect(column.filter(Number.isNaN)).toHaveLength(4);
});

it('should report the max of visible values as valueMax', () => {
const { valueMax } = buildColumnarTimeChart(SERIES, TIME_SCALE);

expect(valueMax).toBe(60);
});

it('should exclude out-of-range and null samples from valueMax', () => {
const { valueMax } = buildColumnarTimeChart(
[
{
name: 's',
values: [
[900, 1_000], // before the range — excluded despite being the largest
[1_030, null],
[1_045, 7],
[2_000, 9_000], // after the range — excluded
],
},
],
TIME_SCALE
);

expect(valueMax).toBe(7);
});

it('should return undefined valueMax when there are no visible values', () => {
const { valueMax } = buildColumnarTimeChart([{ name: 'empty', values: [] }], TIME_SCALE);

expect(valueMax).toBeUndefined();
});

it('should exclude NaN values from valueMax', () => {
const { valueMax } = buildColumnarTimeChart(
[
{
name: 's',
values: [
[1_000, 5],
[1_045, Number.NaN],
],
},
],
TIME_SCALE
);

expect(valueMax).toBe(5);
});

it('should expose row-aligned tuple views that read like the old tuple arrays', () => {
const { viewData } = buildColumnarTimeChart(SERIES, TIME_SCALE);
const gappy = viewData[1]?.values ?? [];
Expand Down
7 changes: 6 additions & 1 deletion components/src/utils/columnar-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export interface ColumnarTimeChart {
source: Record<string, Float64Array>;
/** The input series with `values` replaced by row-aligned lazy tuple views. */
viewData: TimeSeries[];
// LOGZ.IO CHANGE:: Max of the visible values, tracked for free inside the build loop, so the Y-axis
// auto-scale/thinning gate doesn't need a second pass over the data. `undefined` when there is none.
valueMax: number | undefined;
}

function createTupleView(time: Float64Array, column: Float64Array): TimeSeriesValueTuple[] {
Expand Down Expand Up @@ -95,6 +98,7 @@ export function buildColumnarTimeChart(data: TimeSeries[], timeScale: TimeScale)

const source: Record<string, Float64Array> = { [TIME_COLUMN_KEY]: time };
const viewData: TimeSeries[] = new Array(data.length);
let valueMax = -Infinity; // LOGZ.IO CHANGE:: track the visible value max in this same loop [unidash-perf]

for (let seriesIndex = 0; seriesIndex < data.length; seriesIndex++) {
const series = data[seriesIndex] as TimeSeries;
Expand All @@ -109,13 +113,14 @@ export function buildColumnarTimeChart(data: TimeSeries[], timeScale: TimeScale)

if (row >= 0 && row < rowCount) {
column[row] = value;
if (value > valueMax) valueMax = value; // LOGZ.IO CHANGE:: free axis max [unidash-perf]
}
}

source[getSeriesColumnKey(seriesIndex)] = column;
viewData[seriesIndex] = { ...series, values: createTupleView(time, column) };
}

return { source, viewData };
return { source, viewData, valueMax: valueMax === -Infinity ? undefined : valueMax };
}
// LOGZ.IO CHANGE END:: columnar chart data [unidash-perf]
Loading