rusackas commented on code in PR #36214:
URL: https://github.com/apache/superset/pull/36214#discussion_r4356692012


##########
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts:
##########
@@ -546,6 +585,84 @@ export default function transformProps(
     xAxisDataType,
     seriesType,
   );
+  // sortOperator (buildQuery.ts) already orders the backend rows by a
+  // chosen metric when x_axis_sort names one other than the x-axis column
+  // itself, for every series type here (not just Bar) - but only when
+  // groupby is empty, the same condition it checks before emitting that
+  // sort. Natural-sorting by x-value on top of that would silently discard
+  // the explicit "sort categories by metric" choice, so skip it whenever
+  // the backend would actually have applied that sort.
+  const xAxisSortedByMetric =
+    isDefined(xAxisSort) &&
+    isDefined(xAxisSortAsc) &&
+    xAxisSort !== rawXAxisLabel &&

Review Comment:
   Good catch, and this is already handled on the current head: the skip 
condition now also covers `x_axis_sort` naming the x-axis column itself (same 
condition `sortOperator` uses), so an explicit descending order is kept. 
There's a test for it (`skips the natural sort when x_axis_sort explicitly 
orders the x-axis column itself, descending`).



##########
superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts:
##########
@@ -4637,3 +4637,207 @@ test('tooltip keeps the metric format on a Difference 
time comparison', () => {
   expect(result).toContain('$ 100');
   expect(result).toContain('$ 25');
 });
+
+describe('natural sort of a category x-axis (#35853)', () => {
+  function categoryChartProps(
+    records: { dim: string; metric: number }[],
+    formDataOverrides: Partial<EchartsTimeseriesFormData> = {},
+  ) {
+    return createTestChartProps({
+      formData: {
+        metrics: ['metric'],
+        groupby: [],
+        x_axis: 'dim',
+        ...formDataOverrides,
+      },
+      queriesData: [
+        createTestQueryData(records, {
+          colnames: ['dim', 'metric'],
+          coltypes: [GenericDataType.String, GenericDataType.Numeric],
+        }),
+      ],
+    });
+  }
+
+  test('reorders numeric-like string dimension values numerically, not 
lexicographically', () => {
+    // Backend returned these out of order; lexicographic order would put

Review Comment:
   Agreed, and the current head already has this: variable-width `['10', '2', 
'1']` asserting `['1', '2', '10']`, plus the descending `x_axis_sort` on the x 
column case.



##########
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts:
##########
@@ -546,6 +585,85 @@ export default function transformProps(
     xAxisDataType,
     seriesType,
   );
+  // sortOperator (buildQuery.ts) already orders the backend rows, for every
+  // series type here (not just Bar), whenever x_axis_sort/x_axis_sort_asc
+  // are both set, groupby is empty, and x_axis_sort names either a metric
+  // or the x-axis column itself - the latter via `is_sort_index`, honoring
+  // x_axis_sort_asc's direction. Natural-sorting by x-value on top of
+  // either of those would silently discard that explicit choice (including
+  // an explicit descending order on the x-axis column itself), so skip it
+  // under the exact same condition sortOperator uses to decide whether it
+  // applied a sort.
+  const xAxisSortAppliedByBackend =
+    isDefined(xAxisSort) &&
+    isDefined(xAxisSortAsc) &&
+    isEmpty(groupBy) &&
+    [
+      rawXAxisLabel,
+      ...ensureIsArray(metrics).map(getMetricLabel),
+      ...extractExtraMetrics(chartProps.rawFormData).map(getMetricLabel),
+    ].includes(xAxisSort);
+  // A category axis renders points in data-array order, not by sorted
+  // x-value: the backend can return string (and numeric-like-string, e.g.
+  // "202401") dimensions in an arbitrary order, which drew line segments
+  // that jumped between non-adjacent categories (#35853). Natural-sorting
+  // here, before extractSeries builds any per-series data and before
+  // rebaseToPercentChange below picks its baseline row, is the single
+  // point of truth for every consumer downstream (series data in every
+  // shape extractSeries/transformSeries can produce, stacked totals, the
+  // legend, and the percent-change baseline) rather than re-sorting each
+  // series' already-shaped data separately later. Time axes are
+  // unaffected: they already carry a meaningful numeric order and
+  // getAxisType never returns Category for them.
+  // Bar is excluded: discrete bars have no line-connection artifact to fix,
+  // and Bar's category order is already a deliberate, source-preserving
+  // contract independent of legend display sorting (see "should preserve
+  // source order for color-by-primary-axis legends when label sorting is
+  // enabled" in Bar/transformProps.test.ts) - forcing a natural sort here
+  // would silently override that.
+  const shouldNaturalSort =
+    xAxisType === AxisType.Category &&
+    seriesType !== EchartsTimeseriesSeriesType.Bar &&
+    !xAxisSortAppliedByBackend;
+  const naturalSortMode = classifyNaturalSortMode(
+    forecastRebasedData.map(row => row[xAxisLabel]),
+  );
+  const sortedForecastRebasedData = shouldNaturalSort

Review Comment:
   Yep, that's a real problem. Pushed d5f499a52: the natural sort is now 
skipped when `rolling_type` is set (cumsum or a rolling window), so the 
backend's order-dependent results line up with what's drawn. Added a test with 
a cumsum on `'1'`, `'10'`, `'2'`. I haven't been able to run jest locally on 
this one, so leaning on CI.



##########
superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts:
##########
@@ -546,6 +585,85 @@ export default function transformProps(
     xAxisDataType,
     seriesType,
   );
+  // sortOperator (buildQuery.ts) already orders the backend rows, for every
+  // series type here (not just Bar), whenever x_axis_sort/x_axis_sort_asc
+  // are both set, groupby is empty, and x_axis_sort names either a metric
+  // or the x-axis column itself - the latter via `is_sort_index`, honoring
+  // x_axis_sort_asc's direction. Natural-sorting by x-value on top of
+  // either of those would silently discard that explicit choice (including
+  // an explicit descending order on the x-axis column itself), so skip it
+  // under the exact same condition sortOperator uses to decide whether it
+  // applied a sort.
+  const xAxisSortAppliedByBackend =
+    isDefined(xAxisSort) &&
+    isDefined(xAxisSortAsc) &&
+    isEmpty(groupBy) &&
+    [
+      rawXAxisLabel,
+      ...ensureIsArray(metrics).map(getMetricLabel),
+      ...extractExtraMetrics(chartProps.rawFormData).map(getMetricLabel),
+    ].includes(xAxisSort);
+  // A category axis renders points in data-array order, not by sorted
+  // x-value: the backend can return string (and numeric-like-string, e.g.
+  // "202401") dimensions in an arbitrary order, which drew line segments
+  // that jumped between non-adjacent categories (#35853). Natural-sorting
+  // here, before extractSeries builds any per-series data and before
+  // rebaseToPercentChange below picks its baseline row, is the single
+  // point of truth for every consumer downstream (series data in every
+  // shape extractSeries/transformSeries can produce, stacked totals, the
+  // legend, and the percent-change baseline) rather than re-sorting each
+  // series' already-shaped data separately later. Time axes are
+  // unaffected: they already carry a meaningful numeric order and
+  // getAxisType never returns Category for them.
+  // Bar is excluded: discrete bars have no line-connection artifact to fix,
+  // and Bar's category order is already a deliberate, source-preserving
+  // contract independent of legend display sorting (see "should preserve
+  // source order for color-by-primary-axis legends when label sorting is
+  // enabled" in Bar/transformProps.test.ts) - forcing a natural sort here
+  // would silently override that.
+  const shouldNaturalSort =
+    xAxisType === AxisType.Category &&
+    seriesType !== EchartsTimeseriesSeriesType.Bar &&
+    !xAxisSortAppliedByBackend;
+  const naturalSortMode = classifyNaturalSortMode(
+    forecastRebasedData.map(row => row[xAxisLabel]),
+  );
+  const sortedForecastRebasedData = shouldNaturalSort
+    ? [...forecastRebasedData].sort((row1, row2) =>
+        naturalCompare(row1[xAxisLabel], row2[xAxisLabel], naturalSortMode),
+      )
+    : forecastRebasedData;
+  // rebaseToPercentChange picks each series' 0%-baseline as its first row
+  // in whatever order it receives - running it after the natural sort
+  // above means that baseline matches the sorted, on-screen leftmost
+  // category, consistent with the interactive drag-to-rebase
+  // (rebaseSeriesData in this plugin's percentChange.ts), which rebases
+  // from the displayed x-position too.
+  const rebasedData = rebasePercentChange

Review Comment:
   Right, `rebaseSeriesData` was comparing x as strings. Pushed d5f499a52: when 
the baseline x exactly matches a point (always the case for category axes), it 
now finds the baseline by position in the displayed data instead of `<=`; the 
old comparison stays as the fallback for continuous axes. Added a test with 
`'1'`, `'2'`, `'10'` rebased to `'2'` giving `-0.5, 0, 4`. I didn't add the 
full `transformProps` to `rebaseSeriesData` boundary test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to