mirror of
https://github.com/apache/superset.git
synced 2026-06-26 18:09:21 +00:00
Compare commits
3 Commits
chore/ci/s
...
fix/force-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b19eb54d | ||
|
|
66ccb94c29 | ||
|
|
66158aa477 |
@@ -238,11 +238,16 @@ export const xAxisForceCategoricalControl = {
|
||||
return state?.form_data?.x_axis_sort !== undefined || control.value;
|
||||
},
|
||||
renderTrigger: true,
|
||||
// Expose the toggle for numeric and temporal x-axes. Temporal columns
|
||||
// default to a continuous time scale, where ECharts places ticks at "nice"
|
||||
// intervals that don't align with the actual buckets (e.g. weekly grain
|
||||
// markers landing between month ticks). Treating the axis as categorical
|
||||
// lets each bucket map to a discrete, tick-aligned category.
|
||||
visibility: ({ controls }: { controls: ControlStateMapping }) =>
|
||||
checkColumnType(
|
||||
getColumnLabel(controls?.x_axis?.value as QueryFormColumn),
|
||||
controls?.datasource?.datasource,
|
||||
[GenericDataType.Numeric],
|
||||
[GenericDataType.Numeric, GenericDataType.Temporal],
|
||||
),
|
||||
shouldMapStateToProps: () => true,
|
||||
},
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { xAxisForceCategoricalControl } from '../../src/shared-controls/customControls';
|
||||
import { checkColumnType } from '../../src/utils/checkColumnType';
|
||||
import type { ControlState } from '@superset-ui/chart-controls';
|
||||
import type {
|
||||
ControlPanelState,
|
||||
ControlState,
|
||||
ControlStateMapping,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
jest.mock('../../src/utils/checkColumnType');
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
@@ -39,12 +43,12 @@ test('xAxisForceCategoricalControl should not treat temporal columns as categori
|
||||
controls: {
|
||||
x_axis: { value: 'date_column' },
|
||||
datasource: { datasource: {} },
|
||||
},
|
||||
};
|
||||
} as unknown as ControlStateMapping,
|
||||
} as unknown as ControlPanelState;
|
||||
|
||||
const result = xAxisForceCategoricalControl.config.initialValue!(
|
||||
control,
|
||||
state as any,
|
||||
state,
|
||||
);
|
||||
|
||||
// Verify: should return control value (false) for non-numeric columns
|
||||
@@ -55,3 +59,27 @@ test('xAxisForceCategoricalControl should not treat temporal columns as categori
|
||||
|
||||
mockCheckColumnType.mockClear();
|
||||
});
|
||||
|
||||
test('xAxisForceCategoricalControl is visible for numeric and temporal x-axes', () => {
|
||||
const mockCheckColumnType = jest.mocked(checkColumnType);
|
||||
mockCheckColumnType.mockReturnValue(true);
|
||||
|
||||
const controls = {
|
||||
x_axis: { value: 'date_column' },
|
||||
datasource: { datasource: {} },
|
||||
} as unknown as ControlStateMapping;
|
||||
|
||||
const visible = xAxisForceCategoricalControl.config.visibility!({
|
||||
controls,
|
||||
});
|
||||
|
||||
expect(visible).toBe(true);
|
||||
// Temporal columns must be included so the toggle is exposed for time-grain
|
||||
// charts (e.g. weekly grain), where the time scale misaligns ticks/markers.
|
||||
expect(mockCheckColumnType).toHaveBeenCalledWith('date_column', {}, [
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Temporal,
|
||||
]);
|
||||
|
||||
mockCheckColumnType.mockClear();
|
||||
});
|
||||
|
||||
@@ -1569,6 +1569,47 @@ test('xAxisForceCategorical forces Category axis regardless of Numeric coltype',
|
||||
expect(xAxis.type).toBe(AxisType.Category);
|
||||
});
|
||||
|
||||
test('temporal x coltype forced categorical yields a Category axis with date labels', () => {
|
||||
// Issue #28204: with a temporal x-axis (e.g. weekly grain) the default Time
|
||||
// scale places ticks at "nice" intervals that don't line up with the buckets.
|
||||
// Forcing categorical maps each bucket to a discrete, tick-aligned category
|
||||
// while still formatting the labels as dates rather than raw timestamps.
|
||||
const ts1 = 1745784000000;
|
||||
const ts2 = 1745870400000;
|
||||
const chartProps = createTestChartProps({
|
||||
formData: {
|
||||
metrics: ['metric'],
|
||||
granularity_sqla: 'ds',
|
||||
x_axis: '__timestamp',
|
||||
xAxisForceCategorical: true,
|
||||
},
|
||||
queriesData: [
|
||||
createTestQueryData(
|
||||
[
|
||||
{ __timestamp: ts1, metric: 10 },
|
||||
{ __timestamp: ts2, metric: 20 },
|
||||
],
|
||||
{
|
||||
colnames: ['__timestamp', 'metric'],
|
||||
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const { echartOptions } = transformProps(chartProps);
|
||||
const xAxis = echartOptions.xAxis as {
|
||||
type: string;
|
||||
axisLabel: { formatter: (v: Date) => string };
|
||||
};
|
||||
|
||||
expect(xAxis.type).toBe(AxisType.Category);
|
||||
const label = xAxis.axisLabel.formatter(new Date(ts1));
|
||||
expect(typeof label).toBe('string');
|
||||
expect(label).not.toMatch(/NaN/);
|
||||
expect(label).not.toBe(String(ts1));
|
||||
});
|
||||
|
||||
test('temporal x coltype wires the time formatter and Time axis', () => {
|
||||
// Regression guard: the happy path for time-series charts. Ensures that
|
||||
// Temporal coltype keeps routing through the TimeFormatter so a refactor
|
||||
|
||||
Reference in New Issue
Block a user