Skip to content

Conversation

jrieke
Copy link
Collaborator

@jrieke jrieke commented Aug 27, 2025

Describe your changes

This PR adds a sort parameter to st.bar_chart to sort the bars by a column (sort="col1" for ascending and sort="-col1" for descending order).

The default is sort=True for now, which keeps Altair's default sorting. We might change this later on to disable sorting by default but want to discuss it first.

GitHub Issue Link (if applicable)

Closes #385
Closes #7111

Testing Plan

  • Added a few Python unit tests.
  • Added various snapshot tests for sorting.

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

Copy link
Contributor

snyk-io bot commented Aug 27, 2025

🎉 Snyk checks have passed. No issues have been found so far.

security/snyk check is complete. No issues have been found. (View Details)

license/snyk check is complete. No issues have been found. (View Details)

Copy link
Contributor

github-actions bot commented Aug 27, 2025

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-12339/streamlit-1.49.0-py3-none-any.whl
🕹️ Preview app pr-12339.streamlit.app (☁️ Deploy here if not accessible)

@sfc-gh-jrieke sfc-gh-jrieke changed the title Disable sorting of bars in st.bar_chart Add sort parameter to st.bar_chart + disable default sorting Aug 27, 2025
Copy link
Contributor

github-actions bot commented Aug 27, 2025

📈 Python coverage change detected

The Python unit test coverage has increased by 0.0165%

  • Current PR: 92.6835% (18465 statements, 1351 missed)
  • Latest develop: 92.6669% (18437 statements, 1352 missed)

✅ Coverage change is within normal range.

Coverage by files
Name Stmts Miss Cover
streamlit/__init__.py 138 0 100%
streamlit/__main__.py 3 3 0%
streamlit/auth_util.py 100 25 75%
streamlit/cli_util.py 39 6 85%
streamlit/column_config.py 3 0 100%
streamlit/commands/__init__.py 0 0 100%
streamlit/commands/echo.py 54 11 80%
streamlit/commands/execution_control.py 59 10 83%
streamlit/commands/experimental_query_params.py 40 2 95%
streamlit/commands/logo.py 41 6 85%
streamlit/commands/navigation.py 107 2 98%
streamlit/commands/page_config.py 102 4 96%
streamlit/components/__init__.py 0 0 100%
streamlit/components/lib/__init__.py 0 0 100%
streamlit/components/lib/local_component_registry.py 35 2 94%
streamlit/components/types/__init__.py 0 0 100%
streamlit/components/types/base_component_registry.py 14 0 100%
streamlit/components/types/base_custom_component.py 49 6 88%
streamlit/components/v1/__init__.py 5 0 100%
streamlit/components/v1/component_arrow.py 33 8 76%
streamlit/components/v1/component_registry.py 41 3 93%
streamlit/components/v1/components.py 4 4 0%
streamlit/components/v1/custom_component.py 92 7 92%
streamlit/config.py 372 13 97%
streamlit/config_option.py 79 3 96%
streamlit/config_util.py 81 1 99%
streamlit/connections/__init__.py 6 0 100%
streamlit/connections/base_connection.py 45 0 100%
streamlit/connections/snowflake_connection.py 60 13 78%
streamlit/connections/snowpark_connection.py 44 3 93%
streamlit/connections/sql_connection.py 56 6 89%
streamlit/connections/util.py 33 0 100%
streamlit/cursor.py 82 2 98%
streamlit/dataframe_util.py 498 45 91%
streamlit/delta_generator.py 204 6 97%
streamlit/delta_generator_singletons.py 76 8 89%
streamlit/deprecation_util.py 57 4 93%
streamlit/development.py 1 0 100%
streamlit/elements/__init__.py 0 0 100%
streamlit/elements/alert.py 60 0 100%
streamlit/elements/arrow.py 195 15 92%
streamlit/elements/balloons.py 10 0 100%
streamlit/elements/bokeh_chart.py 27 0 100%
streamlit/elements/code.py 20 1 95%
streamlit/elements/deck_gl_json_chart.py 93 6 94%
streamlit/elements/dialog_decorator.py 37 0 100%
streamlit/elements/doc_string.py 227 9 96%
streamlit/elements/empty.py 16 4 75%
streamlit/elements/exception.py 101 10 90%
streamlit/elements/form.py 54 2 96%
streamlit/elements/graphviz_chart.py 35 1 97%
streamlit/elements/heading.py 57 0 100%
streamlit/elements/html.py 48 0 100%
streamlit/elements/iframe.py 29 0 100%
streamlit/elements/image.py 33 0 100%
streamlit/elements/json.py 39 2 95%
streamlit/elements/layouts.py 138 2 99%
streamlit/elements/lib/__init__.py 0 0 100%
streamlit/elements/lib/built_in_chart_utils.py 388 26 93%
streamlit/elements/lib/color_util.py 102 4 96%
streamlit/elements/lib/column_config_utils.py 169 1 99%
streamlit/elements/lib/column_types.py 169 1 99%
streamlit/elements/lib/dialog.py 67 1 99%
streamlit/elements/lib/dicttools.py 39 2 95%
streamlit/elements/lib/file_uploader_utils.py 30 0 100%
streamlit/elements/lib/form_utils.py 26 0 100%
streamlit/elements/lib/image_utils.py 177 21 88%
streamlit/elements/lib/js_number.py 28 3 89%
streamlit/elements/lib/layout_utils.py 95 1 99%
streamlit/elements/lib/mutable_status_container.py 73 4 95%
streamlit/elements/lib/options_selector_utils.py 90 0 100%
streamlit/elements/lib/pandas_styler_utils.py 71 1 99%
streamlit/elements/lib/policies.py 56 1 98%
streamlit/elements/lib/streamlit_plotly_theme.py 49 0 100%
streamlit/elements/lib/subtitle_utils.py 76 13 83%
streamlit/elements/lib/utils.py 77 5 94%
streamlit/elements/map.py 106 1 99%
streamlit/elements/markdown.py 63 2 97%
streamlit/elements/media.py 182 8 96%
streamlit/elements/metric.py 100 5 95%
streamlit/elements/pdf.py 50 2 96%
streamlit/elements/plotly_chart.py 91 3 97%
streamlit/elements/progress.py 37 0 100%
streamlit/elements/pyplot.py 39 2 95%
streamlit/elements/snow.py 10 0 100%
streamlit/elements/spinner.py 34 0 100%
streamlit/elements/text.py 16 0 100%
streamlit/elements/toast.py 26 0 100%
streamlit/elements/vega_charts.py 219 4 98%
streamlit/elements/widgets/__init__.py 0 0 100%
streamlit/elements/widgets/audio_input.py 63 11 83%
streamlit/elements/widgets/button.py 215 3 99%
streamlit/elements/widgets/button_group.py 159 0 100%
streamlit/elements/widgets/camera_input.py 63 10 84%
streamlit/elements/widgets/chat.py 169 40 76%
streamlit/elements/widgets/checkbox.py 52 0 100%
streamlit/elements/widgets/color_picker.py 56 3 95%
streamlit/elements/widgets/data_editor.py 240 14 94%
streamlit/elements/widgets/file_uploader.py 104 18 83%
streamlit/elements/widgets/multiselect.py 105 5 95%
streamlit/elements/widgets/number_input.py 144 6 96%
streamlit/elements/widgets/radio.py 83 6 93%
streamlit/elements/widgets/select_slider.py 98 1 99%
streamlit/elements/widgets/selectbox.py 91 3 97%
streamlit/elements/widgets/slider.py 242 9 96%
streamlit/elements/widgets/text_widgets.py 130 7 95%
streamlit/elements/widgets/time_widgets.py 249 17 93%
streamlit/elements/write.py 170 30 82%
streamlit/emojis.py 4 0 100%
streamlit/env_util.py 21 3 86%
streamlit/error_util.py 33 2 94%
streamlit/errors.py 159 24 85%
streamlit/external/__init__.py 0 0 100%
streamlit/external/langchain/__init__.py 2 0 100%
streamlit/external/langchain/streamlit_callback_handler.py 141 82 42%
streamlit/file_util.py 84 8 90%
streamlit/git_util.py 100 63 37%
streamlit/logger.py 54 0 100%
streamlit/material_icon_names.py 1 0 100%
streamlit/navigation/__init__.py 0 0 100%
streamlit/navigation/page.py 78 2 97%
streamlit/net_util.py 55 3 95%
streamlit/platform.py 10 1 90%
streamlit/runtime/__init__.py 8 0 100%
streamlit/runtime/app_session.py 442 93 79%
streamlit/runtime/caching/__init__.py 19 0 100%
streamlit/runtime/caching/cache_data_api.py 164 3 98%
streamlit/runtime/caching/cache_errors.py 45 1 98%
streamlit/runtime/caching/cache_resource_api.py 121 0 100%
streamlit/runtime/caching/cache_type.py 11 1 91%
streamlit/runtime/caching/cache_utils.py 165 9 95%
streamlit/runtime/caching/cached_message_replay.py 108 1 99%
streamlit/runtime/caching/hashing.py 311 25 92%
streamlit/runtime/caching/legacy_cache_api.py 13 0 100%
streamlit/runtime/caching/storage/__init__.py 2 0 100%
streamlit/runtime/caching/storage/cache_storage_protocol.py 31 2 94%
streamlit/runtime/caching/storage/dummy_cache_storage.py 21 0 100%
streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py 60 0 100%
streamlit/runtime/caching/storage/local_disk_cache_storage.py 86 4 95%
streamlit/runtime/connection_factory.py 85 9 89%
streamlit/runtime/context.py 140 0 100%
streamlit/runtime/context_util.py 18 0 100%
streamlit/runtime/credentials.py 139 4 97%
streamlit/runtime/forward_msg_cache.py 23 2 91%
streamlit/runtime/forward_msg_queue.py 63 4 94%
streamlit/runtime/fragment.py 111 2 98%
streamlit/runtime/media_file_manager.py 69 7 90%
streamlit/runtime/media_file_storage.py 15 0 100%
streamlit/runtime/memory_media_file_storage.py 68 0 100%
streamlit/runtime/memory_session_storage.py 15 0 100%
streamlit/runtime/memory_uploaded_file_manager.py 41 1 98%
streamlit/runtime/metrics_util.py 190 12 94%
streamlit/runtime/pages_manager.py 59 2 97%
streamlit/runtime/runtime.py 241 18 93%
streamlit/runtime/runtime_util.py 30 1 97%
streamlit/runtime/script_data.py 16 0 100%
streamlit/runtime/scriptrunner/__init__.py 5 0 100%
streamlit/runtime/scriptrunner/exec_code.py 49 5 90%
streamlit/runtime/scriptrunner/magic.py 83 1 99%
streamlit/runtime/scriptrunner/magic_funcs.py 10 1 90%
streamlit/runtime/scriptrunner/script_cache.py 27 0 100%
streamlit/runtime/scriptrunner/script_runner.py 230 27 88%
streamlit/runtime/scriptrunner_utils/__init__.py 0 0 100%
streamlit/runtime/scriptrunner_utils/exceptions.py 11 1 91%
streamlit/runtime/scriptrunner_utils/script_requests.py 106 5 95%
streamlit/runtime/scriptrunner_utils/script_run_context.py 136 2 99%
streamlit/runtime/secrets.py 242 25 90%
streamlit/runtime/session_manager.py 60 1 98%
streamlit/runtime/state/__init__.py 7 0 100%
streamlit/runtime/state/common.py 49 2 96%
streamlit/runtime/state/query_params.py 110 3 97%
streamlit/runtime/state/query_params_proxy.py 71 0 100%
streamlit/runtime/state/safe_session_state.py 77 11 86%
streamlit/runtime/state/session_state.py 361 13 96%
streamlit/runtime/state/session_state_proxy.py 62 8 87%
streamlit/runtime/state/widgets.py 12 1 92%
streamlit/runtime/stats.py 42 0 100%
streamlit/runtime/theme_util.py 46 1 98%
streamlit/runtime/uploaded_file_manager.py 39 3 92%
streamlit/runtime/websocket_session_manager.py 66 0 100%
streamlit/source_util.py 36 1 97%
streamlit/string_util.py 74 2 97%
streamlit/temporary_directory.py 18 1 94%
streamlit/testing/__init__.py 0 0 100%
streamlit/testing/v1/__init__.py 2 0 100%
streamlit/testing/v1/app_test.py 239 6 97%
streamlit/testing/v1/element_tree.py 1319 84 94%
streamlit/testing/v1/local_script_runner.py 71 2 97%
streamlit/testing/v1/util.py 17 0 100%
streamlit/time_util.py 28 1 96%
streamlit/type_util.py 139 12 91%
streamlit/url_util.py 40 5 88%
streamlit/user_info.py 87 8 91%
streamlit/util.py 38 1 97%
streamlit/version.py 3 0 100%
streamlit/watcher/__init__.py 3 0 100%
streamlit/watcher/event_based_path_watcher.py 175 24 86%
streamlit/watcher/folder_black_list.py 14 1 93%
streamlit/watcher/local_sources_watcher.py 127 9 93%
streamlit/watcher/path_watcher.py 43 3 93%
streamlit/watcher/polling_path_watcher.py 55 2 96%
streamlit/watcher/util.py 49 1 98%
streamlit/web/__init__.py 0 0 100%
streamlit/web/bootstrap.py 151 18 88%
streamlit/web/cache_storage_manager_config.py 5 0 100%
streamlit/web/cli.py 177 17 90%
streamlit/web/server/__init__.py 5 0 100%
streamlit/web/server/app_static_file_handler.py 29 3 90%
streamlit/web/server/authlib_tornado_integration.py 18 1 94%
streamlit/web/server/browser_websocket_handler.py 115 31 73%
streamlit/web/server/component_request_handler.py 64 6 91%
streamlit/web/server/media_file_handler.py 65 9 86%
streamlit/web/server/oauth_authlib_routes.py 118 18 85%
streamlit/web/server/oidc_mixin.py 44 0 100%
streamlit/web/server/routes.py 87 7 92%
streamlit/web/server/server.py 185 11 94%
streamlit/web/server/server_util.py 67 5 93%
streamlit/web/server/stats_request_handler.py 53 4 92%
streamlit/web/server/upload_file_request_handler.py 53 9 83%
streamlit/web/server/websocket_headers.py 19 1 95%
TOTAL 18465 1351 93%

📊 View detailed coverage comparison

assert chart_spec["mark"] in [altair_type, {"type": altair_type}]
assert chart_spec["encoding"]["x"]["type"] == "ordinal"
assert chart_spec["encoding"]["x"]["sort"] == ["c", "b", "a"]
assert chart_spec["encoding"]["x"]["sort"] is None
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note sure if this test still makes sense now or if there's another way to check that the ordered categories are applied properly, given that we're disabling sorting now by setting sort=None.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just ran this manually and it seems like it's ordered b -> c -> a, i.e. in the order of the data. Not sure how before this even worked to get the right order from the Pandas dataframe, given that Altair should sort the categories alphabetically by default?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how before this even worked to get the right order from the Pandas dataframe, given that Altair should sort the categories alphabetically by default?

I believe Altair has some extra treatment if the column is categorical with ordered=True. Also see this old issue and PR related to this aspect:

I'm wondering if we should add some special handling in this case (if the column is categorical + ordered=True)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved this by not doing any special casing but allowing sort=True to get the old behavior with Altair's automatic sorting back. (The drawback of special casing is that then there's no way to disable this sorting...).

@jrieke jrieke marked this pull request as ready for review August 28, 2025 01:00
@sfc-gh-jrieke sfc-gh-jrieke requested a review from Copilot August 28, 2025 01:14
Copilot

This comment was marked as outdated.

@sfc-gh-jrieke sfc-gh-jrieke requested a review from Copilot August 28, 2025 01:52
Copilot

This comment was marked as outdated.

Comment on lines 1168 to 1173
sort : str or None
How to sort the bars. If ``None`` (default), bars are shown in data
order (no sorting). If this is the name of a column (e.g. ``"col1"``),
bars are sorted by that column in ascending order. If this is the
name of a column prefixed with a minus sign (e.g. ``"-col1"``), bars are
sorted by that column in descending order.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One consideration: We could also support bool here with True being the current behaviour.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah pretty neat idea. I changed it now so it uses False by default and allows setting it to True to have Altair's automatic sorting. This also resolves the ordered categorical issue above, since you can just set sort=True to have the same sorting of ordered categorical values as before.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also been thinking about changing the default to True to break fewer existing charts, but I think False by default is much less confusing. Plotly and matplotlib also don't apply any automatic sorting for bar charts by default.

Copy link
Collaborator

@lukasmasuch lukasmasuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 but it might be good to validate if it makes sense to add special treatment for ordered categorical columns, see this comment: #12339 (comment)

@sfc-gh-jrieke sfc-gh-jrieke requested a review from Copilot August 28, 2025 19:32
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a new sort parameter to st.bar_chart to give users explicit control over how bars are ordered, while also disabling Altair's default alphabetical sorting behavior. This addresses user confusion and provides more predictable chart behavior.

Key changes:

  • Adds sort parameter supporting boolean values and column name strings (with optional "-" prefix for descending order)
  • Disables automatic sorting by default (sort=False) for more predictable behavior
  • Updates existing snapshot tests to reflect the new default behavior

Reviewed Changes

Copilot reviewed 6 out of 57 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/streamlit/elements/vega_charts.py Adds the sort parameter to the bar_chart function signature and documentation
lib/streamlit/elements/lib/built_in_chart_utils.py Core implementation of sort functionality with column parsing, validation, and encoding logic
lib/tests/streamlit/elements/vega_charts_test.py Comprehensive unit tests for the new sort parameter behavior
lib/streamlit/elements/arrow.py Updates add_rows functionality to preserve sort parameter
e2e_playwright/st_bar_chart.py Adds test cases for various sort scenarios
e2e_playwright/st_bar_chart_test.py Updates test expectations and adds snapshot tests for sort behavior

Comment on lines +711 to +719
def _parse_sort_column(df: pd.DataFrame, sort_from_user: bool | str) -> str | None:
if sort_from_user is False or sort_from_user is True:
return None

sort_column = sort_from_user.removeprefix("-")
if sort_column not in df.columns:
raise StreamlitColumnNotFoundError(df, sort_column)

return sort_column
Copy link
Preview

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't handle the case where sort_from_user is an empty string. An empty string would pass the boolean checks and then removeprefix('-') would return an empty string, which would likely not be found in df.columns and raise an error. Consider adding an explicit check for empty strings.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would already trigger an error because the empty string wouldn't be in the dataframe columns.

Comment on lines +1008 to +1014
# String: sort by column name (optional '-' prefix for descending)
sort_order: Literal["ascending", "descending"]
if sort_from_user.startswith("-"):
sort_order = "descending"
else:
sort_order = "ascending"
sort_field = sort_from_user.removeprefix("-")
Copy link
Preview

Copilot AI Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to _parse_sort_column, this function doesn't handle empty strings. If sort_from_user is an empty string, it would create a SortField with an empty field name, which could cause issues in Altair. Consider adding validation to ensure the string is not empty.

Suggested change
# String: sort by column name (optional '-' prefix for descending)
sort_order: Literal["ascending", "descending"]
if sort_from_user.startswith("-"):
sort_order = "descending"
else:
sort_order = "ascending"
sort_field = sort_from_user.removeprefix("-")
sort_field = sort_from_user.removeprefix("-")
if not sort_field:
raise StreamlitAPIException(
"Sort column name cannot be empty. Please provide a valid column name for sorting."
)

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already caught by the check above.

Comment on lines +711 to +720
def _parse_sort_column(df: pd.DataFrame, sort_from_user: bool | str) -> str | None:
if sort_from_user is False or sort_from_user is True:
return None

sort_column = sort_from_user.removeprefix("-")
if sort_column not in df.columns:
raise StreamlitColumnNotFoundError(df, sort_column)

return sort_column

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function _parse_sort_column lacks a docstring. Per Python Guide docstring requirements, functions should include docstrings using Numpydoc style. Although this function is prefixed with underscore indicating private scope, it supports chart utilities that users access through st.bar_chart. Add a docstring explaining the function's purpose, parameters, and return value in Numpydoc format.

Suggested change
def _parse_sort_column(df: pd.DataFrame, sort_from_user: bool | str) -> str | None:
if sort_from_user is False or sort_from_user is True:
return None
sort_column = sort_from_user.removeprefix("-")
if sort_column not in df.columns:
raise StreamlitColumnNotFoundError(df, sort_column)
return sort_column
def _parse_sort_column(df: pd.DataFrame, sort_from_user: bool | str) -> str | None:
"""
Parse the sort column parameter for chart utilities.
Parameters
----------
df : pd.DataFrame
The DataFrame containing the data to be plotted.
sort_from_user : bool or str
The sort parameter provided by the user. If a string, it specifies
the column to sort by (with optional "-" prefix for descending order).
If a boolean, it's ignored and None is returned.
Returns
-------
str or None
The name of the column to sort by without any direction prefix,
or None if sort_from_user is a boolean.
Raises
------
StreamlitColumnNotFoundError
If the specified sort column doesn't exist in the DataFrame.
"""
if sort_from_user is False or sort_from_user is True:
return None
sort_column = sort_from_user.removeprefix("-")
if sort_column not in df.columns:
raise StreamlitColumnNotFoundError(df, sort_column)
return sort_column

Spotted by Diamond (based on custom rule: Python Guide)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@jrieke jrieke changed the title Add sort parameter to st.bar_chart + disable default sorting Add sort parameter to st.bar_chart Aug 29, 2025
the chart's Altair spec. As a result this is easier to use for many
"just plot this" scenarios, while being less customizable.
If ``st.line_chart`` does not guess the data specification
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing these lines because they aren't really accurate anymore today given that you can use x and y to specify which columns to use.

@jrieke jrieke merged commit cdc6989 into develop Aug 29, 2025
37 checks passed
@jrieke jrieke deleted the feature/disable-sort-bar-chart branch August 29, 2025 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a sort_by argument to st.bar_chart streamlit.bar_chart should not sort columns.
3 participants