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


##########
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:
   Reordering here happens after the backend has already computed 
order-dependent post-processing (cumulative sum and rolling windows, via 
`rollingWindowOperator`) over the pivot's lexicographic row order. With an 
ad-hoc string x-axis (categories `'1'`, `'10'`, `'2'`, metric `1` each) and 
cumulative sum enabled, the backend returns `('1',1), ('10',2), ('2',3)`; this 
sort now renders `('1',1), ('2',3), ('10',2)`, so a running total of positive 
values visibly decreases and the rolling values no longer correspond to the 
displayed neighbors. Should the reorder be skipped when a rolling/cumulative 
operator is active (or the ordering moved ahead of that computation)?



##########
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 &&

Review Comment:
   `extractSeries` imputes the stacked-area neighbor zero (`fillNeighborValue`) 
by indexing `rows[idx ± 1]` while the output follows `sortedRows` from 
`sortRows`. Previously both orders were lexicographic for string categories, so 
they agreed; now `rows` is naturally sorted while an explicit multi-series 
`x_axis_sort_series` by name re-sorts lexicographically. For stacked area with 
groupby, categories `1, 2, 10`, series A = `[null, 5, null]` with sort by name, 
the zero-fill lands on `'1'` instead of `'10'` (A renders `[0, null, 5]` rather 
than `[null, 0, 5]`), so the area is drawn incorrectly. Should the natural sort 
be skipped when `xAxisSortSeries` is set for a multi-series chart, as it is for 
the backend sort?



##########
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:
   After this sort the displayed category order is natural, but the interactive 
drag-to-rebase (`rebaseSeriesData` in `percentChange.ts`) still finds its 
baseline with a string comparison (`x <= baselineX`). With data `['1',0], 
['2',1], ['10',9]` and a drag to `'2'`, `'10' <= '2'` is also true 
lexicographically, so `'10'` wins as the baseline: `'2'` shows -80% instead of 
0% and `'10'` shows 0% instead of +400%. Before this change the data order 
matched the lexicographic comparison, so it worked. Could the rebase use the 
same natural comparison, and could a test cover the `transformProps` → 
`rebaseSeriesData` boundary with `'1'`, `'2'`, `'10'` (rebasing to `'2'` should 
give `-0.5, 0, 4`)?



-- 
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