Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,9 @@ export default function transformProps(
nameGap: xAxisTitleMarginPx,
nameLocation: 'middle',
axisLabel: {
hideOverlap: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
hideOverlap: showMaxLabel
? false
: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
formatter: deduplicatedFormatter,
rotate: xAxisLabelRotation,
interval: xAxisLabelInterval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1256,10 +1256,12 @@ export default function transformProps(
// When rotation is applied on time axes, hideOverlap can
// aggressively hide the last label. Rotated labels already
// have less overlap, so disabling hideOverlap is safe.
// At 0° rotation, keep hideOverlap to prevent long labels
// from overlapping each other, with showMaxLabel to ensure
// the last data point label stays visible (#37181).
hideOverlap: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
// At 0° rotation, also disable hideOverlap when showMaxLabel
// is active so the forced boundary label is never suppressed
// by ECharts' overlap detection (#39899).
hideOverlap: showMaxLabel
? false
: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0),
formatter: deduplicatedFormatter,
rotate: xAxisLabelRotation,
interval: xAxisLabelInterval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,110 @@ test('x-axis dedup keeps the forced min label when the endpoints format identica
expect(formatter(min)).toBe('May');
});

test('#39899 - x-axis dates do not overlap and last label stays visible at 0° rotation (mixed)', () => {
// When showMaxLabel is active on a time axis with 0° rotation,
// hideOverlap must be off so ECharts cannot suppress the forced
// max label (the end-of-axis date).
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [
createTestQueryData(
[
{
__timestamp: Date.UTC(2026, 0, 1),
sum__num: 100,
},
{
__timestamp: Date.UTC(2026, 6, 1),
sum__num: 200,
},
],
{
colnames: ['__timestamp', 'sum__num'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], sum__num: ['sum__num'] },
},
),
createTestQueryData(
[
{
__timestamp: Date.UTC(2026, 0, 1),
sum__num: 100,
},
{
__timestamp: Date.UTC(2026, 6, 1),
sum__num: 200,
},
],
{
colnames: ['__timestamp', 'sum__num'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], sum__num: ['sum__num'] },
},
),
],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['sum__num'],
metricsB: ['sum__num'],
groupby: [],
groupbyB: [],
xAxisLabelRotation: 0,
// showMaxLabel (and therefore hideOverlap: false) only activates when
// a time grain resolves, so this needs one set to actually exercise
// the #39899 fix rather than silently no-op.
timeGrainSqla: TimeGranularity.MONTH,
},
queriesData: [
createTestQueryData(
[
{
__timestamp: Date.UTC(2026, 0, 1),
sum__num: 100,
},
{
__timestamp: Date.UTC(2026, 6, 1),
sum__num: 200,
},
],
{
colnames: ['__timestamp', 'sum__num'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], sum__num: ['sum__num'] },
},
),
createTestQueryData(
[
{
__timestamp: Date.UTC(2026, 0, 1),
sum__num: 100,
},
{
__timestamp: Date.UTC(2026, 6, 1),
sum__num: 200,
},
],
{
colnames: ['__timestamp', 'sum__num'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], sum__num: ['sum__num'] },
},
),
],
});

const { echartOptions } = transformProps(chartProps);
const { axisLabel } = echartOptions.xAxis as Record<string, any>;

expect(axisLabel.showMaxLabel).toBe(true);
expect(axisLabel.alignMaxLabel).toBe('right');
expect(axisLabel.hideOverlap).toBe(false);
});

test('regression #37921: multi-metric Query A with groupby does not duplicate first metric in series names', () => {
// Regression test for https://github.com/apache/superset/issues/37921
// ("Residual" follow-up to #37055).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,15 +341,15 @@ test('should configure time axis labels to show max label for last month visibil
);
});

test('x-axis dates do not overlap and last label stays visible at 0° rotation', () => {
test('#39899 - x-axis dates do not overlap and last label stays visible at 0° rotation', () => {
const result = transformProps(buildTimeseriesChartProps());
const { axisLabel } = result.echartOptions.xAxis as Record<string, any>;

expect(axisLabel.hideOverlap).toBe(true);
// showMaxLabel forces the last data point label to render even
// when hideOverlap is active, preventing the #37181 regression.
// showMaxLabel forces the last data point label to render
expect(axisLabel.showMaxLabel).toBe(true);
expect(axisLabel.alignMaxLabel).toBe('right');
// hideOverlap must be OFF so ECharts cannot suppress the forced max label
expect(axisLabel.hideOverlap).toBe(false);
});

test('last x-axis date is visible and not cut off when rotated -45°', () => {
Expand Down
31 changes: 31 additions & 0 deletions superset-frontend/src/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 {
URL_PARAMS,
RESERVED_CHART_URL_PARAMS,
RESERVED_DASHBOARD_URL_PARAMS,
} from 'src/constants';

test('permalinkKey is reserved on both the chart and dashboard URL param lists', () => {
// Dashboard and explore permalinks resolve against different backend
// KV resources/salts, so a key from one must never leak into the other's
// URL via the reserved-params passthrough logic.
expect(RESERVED_DASHBOARD_URL_PARAMS).toContain(URL_PARAMS.permalinkKey.name);
expect(RESERVED_CHART_URL_PARAMS).toContain(URL_PARAMS.permalinkKey.name);
});
1 change: 1 addition & 0 deletions superset-frontend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export const RESERVED_CHART_URL_PARAMS: string[] = [
URL_PARAMS.datasourceId.name,
URL_PARAMS.datasourceType.name,
URL_PARAMS.datasetId.name,
URL_PARAMS.permalinkKey.name,
URL_PARAMS.versionHistory.name,
];
export const RESERVED_DASHBOARD_URL_PARAMS: string[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,24 @@ describe('VizTypeControl', () => {
).not.toBeInTheDocument();
});

test('Thumbnail labels expose the full chart name via a title tooltip', async () => {
// Labels are clamped to a fixed two-line block so every tile is the same
// height; the full (possibly truncated) name must stay discoverable through
// the title attribute.
await waitForRenderWrapper();
userEvent.click(screen.getByRole('tab', { name: 'All charts' }));

const visualizations = screen.getByTestId(getTestId('viz-row'));
const labels = await within(visualizations).findAllByTestId(
getTestId('viztype-label'),
);

expect(labels.length).toBeGreaterThan(0);
labels.forEach(label => {
expect(label).toHaveAttribute('title', label.textContent ?? '');
});
});

test('Submit on viz type double-click', async () => {
await waitForRenderWrapper();
userEvent.click(screen.getByRole('tab', { name: 'All charts' }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ const IconsPane = styled.div`
justify-content: space-evenly;
grid-gap: ${({ theme }) => theme.sizeUnit * 2}px;
justify-items: center;
/* top-align every tile so a longer chart name never pushes the thumbnails
of the other tiles in the same row upward */
align-items: start;
/* for some reason this padding doesn't seem to apply at the bottom of the container. Why is a mystery. */
padding: ${({ theme }) => theme.sizeUnit * 2}px;
`;
Expand Down Expand Up @@ -274,7 +277,6 @@ const thumbnailContainerCss = (theme: SupersetTheme) => css`
font: inherit;
cursor: pointer;
width: ${theme.sizeUnit * THUMBNAIL_GRID_UNITS}px;
position: relative;
outline: none; /* Remove focus outline to show only selected state */

img {
Expand All @@ -297,6 +299,16 @@ const thumbnailContainerCss = (theme: SupersetTheme) => css`
.viztype-label {
margin-top: ${theme.sizeUnit * 2}px;
text-align: center;
/* reserve a fixed two-line block so every tile is the same height,
regardless of how long the chart name is. Longer names are clamped
with an ellipsis; the full name stays available via the title tooltip. */
line-height: ${theme.sizeUnit * 4}px;
height: ${theme.sizeUnit * 8}px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
}
`;

Expand All @@ -320,10 +332,19 @@ const HighlightLabel = styled.div`
`}
`;

// Wraps the thumbnail image so the "Featured" badge can be anchored to the
// image itself rather than to the whole tile (whose height varies with the
// chart-name length). line-height: 0 removes the inline-image descender gap.
const ThumbnailImageWrapper = styled.div`
position: relative;
width: ${({ theme }) => theme.sizeUnit * THUMBNAIL_GRID_UNITS}px;
line-height: 0;
`;

const ThumbnailLabelWrapper = styled.div`
position: absolute;
right: ${({ theme }) => theme.sizeUnit}px;
top: ${({ theme }) => theme.sizeUnit * 19}px;
top: ${({ theme }) => theme.sizeUnit}px;
`;

const TitleLabelWrapper = styled.div`
Expand Down Expand Up @@ -367,27 +388,32 @@ const Thumbnail: FC<ThumbnailProps> = ({
onFocus={handleFocus}
data-test="viztype-selector-container"
>
<img
alt={type.name}
width="100%"
className={`viztype-selector ${isSelected ? 'selected' : ''}`}
src={
isDarkMode && type.thumbnailDark ? type.thumbnailDark : type.thumbnail
}
/>
<ThumbnailImageWrapper>
<img
alt={type.name}
width="100%"
className={`viztype-selector ${isSelected ? 'selected' : ''}`}
src={
isDarkMode && type.thumbnailDark
? type.thumbnailDark
: type.thumbnail
}
/>
{type.label && (
<ThumbnailLabelWrapper>
<HighlightLabel>
<div>{t(type.label)}</div>
</HighlightLabel>
</ThumbnailLabelWrapper>
)}
</ThumbnailImageWrapper>
<div
className="viztype-label"
data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__viztype-label`}
title={type.name}
>
{type.name}
</div>
{type.label && (
<ThumbnailLabelWrapper>
<HighlightLabel>
<div>{t(type.label)}</div>
</HighlightLabel>
</ThumbnailLabelWrapper>
)}
</button>
);
};
Expand Down
6 changes: 6 additions & 0 deletions superset/charts/data/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,12 @@ def data_from_cache(self, cache_key: str) -> Response:
# for async queries with jinja context
set_form_data(cached_data)
query_context = self._create_query_context_from_form(cached_data)
# Mark as a cache replay so _sql_filters_modified skips the
# SQL-extras check. The original request already passed the
# full security check, cache keys are opaque SHA-256 hashes
# (unguessable), and force_cached only serves pre-computed
# data — no new SQL is executed.
query_context._from_cache_replay = True
command = ChartDataCommand(query_context)
command.validate()
except ChartDataCacheLoadError:
Expand Down
Loading
Loading