streamlit 1.49.0__py3-none-any.whl → 1.50.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. streamlit/column_config.py +2 -0
  2. streamlit/commands/navigation.py +3 -1
  3. streamlit/components/v1/custom_component.py +17 -42
  4. streamlit/config.py +306 -0
  5. streamlit/connections/base_connection.py +4 -2
  6. streamlit/dataframe_util.py +3 -2
  7. streamlit/delta_generator.py +2 -3
  8. streamlit/elements/arrow.py +63 -43
  9. streamlit/elements/deck_gl_json_chart.py +1 -0
  10. streamlit/elements/form.py +6 -6
  11. streamlit/elements/graphviz_chart.py +23 -6
  12. streamlit/elements/iframe.py +0 -2
  13. streamlit/elements/image.py +10 -9
  14. streamlit/elements/layouts.py +58 -11
  15. streamlit/elements/lib/built_in_chart_utils.py +95 -29
  16. streamlit/elements/lib/column_config_utils.py +5 -0
  17. streamlit/elements/lib/column_types.py +563 -144
  18. streamlit/elements/lib/dialog.py +1 -0
  19. streamlit/elements/lib/layout_utils.py +4 -4
  20. streamlit/elements/lib/pandas_styler_utils.py +30 -14
  21. streamlit/elements/lib/utils.py +17 -5
  22. streamlit/elements/map.py +1 -3
  23. streamlit/elements/media.py +2 -0
  24. streamlit/elements/metric.py +10 -32
  25. streamlit/elements/plotly_chart.py +17 -9
  26. streamlit/elements/pyplot.py +6 -6
  27. streamlit/elements/vega_charts.py +110 -44
  28. streamlit/elements/widgets/audio_input.py +48 -0
  29. streamlit/elements/widgets/button.py +27 -25
  30. streamlit/elements/widgets/button_group.py +1 -0
  31. streamlit/elements/widgets/camera_input.py +1 -0
  32. streamlit/elements/widgets/chat.py +1 -0
  33. streamlit/elements/widgets/checkbox.py +1 -0
  34. streamlit/elements/widgets/color_picker.py +1 -0
  35. streamlit/elements/widgets/data_editor.py +6 -5
  36. streamlit/elements/widgets/file_uploader.py +1 -0
  37. streamlit/elements/widgets/multiselect.py +10 -0
  38. streamlit/elements/widgets/number_input.py +3 -0
  39. streamlit/elements/widgets/radio.py +1 -0
  40. streamlit/elements/widgets/select_slider.py +1 -0
  41. streamlit/elements/widgets/selectbox.py +4 -0
  42. streamlit/elements/widgets/slider.py +1 -0
  43. streamlit/elements/widgets/text_widgets.py +6 -0
  44. streamlit/elements/widgets/time_widgets.py +9 -0
  45. streamlit/elements/write.py +1 -17
  46. streamlit/git_util.py +65 -43
  47. streamlit/material_icon_names.py +1 -1
  48. streamlit/proto/Arrow_pb2.py +10 -8
  49. streamlit/proto/Arrow_pb2.pyi +31 -2
  50. streamlit/proto/AudioInput_pb2.py +2 -2
  51. streamlit/proto/AudioInput_pb2.pyi +6 -2
  52. streamlit/proto/Block_pb2.py +11 -11
  53. streamlit/proto/Block_pb2.pyi +5 -0
  54. streamlit/proto/NewSession_pb2.py +18 -16
  55. streamlit/proto/NewSession_pb2.pyi +135 -2
  56. streamlit/runtime/app_session.py +18 -5
  57. streamlit/runtime/theme_util.py +148 -0
  58. streamlit/static/index.html +2 -2
  59. streamlit/static/manifest.json +221 -221
  60. streamlit/static/static/css/index.CHEnSPGk.css +1 -0
  61. streamlit/static/static/css/{index.C8X8rNzw.css → index.CIiu7Ygf.css} +1 -1
  62. streamlit/static/static/js/{ErrorOutline.esm.u9XvzxL8.js → ErrorOutline.esm.DUpR0_Ka.js} +1 -1
  63. streamlit/static/static/js/{FileDownload.esm.CaRyZ-b2.js → FileDownload.esm.CN4j9-1w.js} +1 -1
  64. streamlit/static/static/js/{FileHelper.Dk2SwIi3.js → FileHelper.CaIUKG91.js} +1 -1
  65. streamlit/static/static/js/{FormClearHelper.l_UPPvkg.js → FormClearHelper.DTcdrasw.js} +1 -1
  66. streamlit/static/static/js/{Hooks.BxrVEftw.js → Hooks.BRba_Own.js} +1 -1
  67. streamlit/static/static/js/InputInstructions.xnSDuYeQ.js +1 -0
  68. streamlit/static/static/js/{Particles.DkY6FDnc.js → Particles.CElH0XX2.js} +1 -1
  69. streamlit/static/static/js/{ProgressBar.BPtSM82n.js → ProgressBar.DetlP5aY.js} +2 -2
  70. streamlit/static/static/js/Toolbar.C77ar7rq.js +1 -0
  71. streamlit/static/static/js/{base-input.egUI4LjJ.js → base-input.BQft14La.js} +3 -3
  72. streamlit/static/static/js/{checkbox.ButpszcE.js → checkbox.yZOfXCeX.js} +1 -1
  73. streamlit/static/static/js/{createSuper.DYJA5xa6.js → createSuper.Dh9w1cs8.js} +1 -1
  74. streamlit/static/static/js/data-grid-overlay-editor.DcuHuCyW.js +1 -0
  75. streamlit/static/static/js/{downloader.B3TjsSPZ.js → downloader.MeHtkq8r.js} +1 -1
  76. streamlit/static/static/js/{es6.BYSNuG4D.js → es6.VpBPGCnM.js} +2 -2
  77. streamlit/static/static/js/{iframeResizer.contentWindow.CNPHJsF2.js → iframeResizer.contentWindow.yMw_ARIL.js} +1 -1
  78. streamlit/static/static/js/{index.DgpIMUsr.js → index.64ejlaaT.js} +1 -1
  79. streamlit/static/static/js/{index.BBnWuh07.js → index.6xX1278W.js} +90 -91
  80. streamlit/static/static/js/index.B-hiXRzw.js +1 -0
  81. streamlit/static/static/js/{index.DtwkPJs5.js → index.B0H9IXUJ.js} +47 -47
  82. streamlit/static/static/js/{index.0tDq1WXk.js → index.B4cAbHP6.js} +1 -1
  83. streamlit/static/static/js/{index.CFjU0x00.js → index.B4dUQfni.js} +1 -1
  84. streamlit/static/static/js/{index.BrD9sbpx.js → index.BPQo7BKk.js} +1 -1
  85. streamlit/static/static/js/index.Baqa90pe.js +2 -0
  86. streamlit/static/static/js/{index.uInpwWAP.js → index.Bj9JgOEC.js} +1 -1
  87. streamlit/static/static/js/index.BjCwMzj4.js +3 -0
  88. streamlit/static/static/js/{index.C3EXAI-u.js → index.Bm3VbPB5.js} +1 -1
  89. streamlit/static/static/js/{index.DGcW849X.js → index.Bxz2yX3P.js} +1 -1
  90. streamlit/static/static/js/{index.CeXLlclc.js → index.BycLveZ4.js} +1 -1
  91. streamlit/static/static/js/{index.CjQnYKID.js → index.C9BdUqTi.js} +1 -1
  92. streamlit/static/static/js/index.CFMf5_ez.js +197 -0
  93. streamlit/static/static/js/index.CGYqqs6j.js +1 -0
  94. streamlit/static/static/js/{index.CFePF7s4.js → index.CH1tqnSs.js} +1 -1
  95. streamlit/static/static/js/{index.BoJaJReB.js → index.CMItVsFA.js} +1 -1
  96. streamlit/static/static/js/{index.CuEFSQ-o.js → index.CTBk8Vk2.js} +1 -1
  97. streamlit/static/static/js/index.CiAQIz1H.js +7 -0
  98. streamlit/static/static/js/index.Cj7DSzVR.js +73 -0
  99. streamlit/static/static/js/index.Ck8rQ9OL.js +1 -0
  100. streamlit/static/static/js/{index.CqSRo6zQ.js → index.ClELlchS.js} +2 -2
  101. streamlit/static/static/js/{index.DP1rDFP0.js → index.Cnpi3o3E.js} +1 -1
  102. streamlit/static/static/js/{index.Cl_966eE.js → index.Ctn27_AE.js} +1 -1
  103. streamlit/static/static/js/{index.D4jR1m1z.js → index.D2QEXQq_.js} +1 -1
  104. streamlit/static/static/js/index.DH71Ezyj.js +1 -0
  105. streamlit/static/static/js/{index.CfiZGqj3.js → index.DHh-U0dK.js} +2 -2
  106. streamlit/static/static/js/{index.Bp1Of6L8.js → index.DK7hD7_w.js} +1 -1
  107. streamlit/static/static/js/{index.MQLQLR5Z.js → index.DKv_lNO7.js} +1 -1
  108. streamlit/static/static/js/index.DNLrMXgm.js +12 -0
  109. streamlit/static/static/js/index.DW0Grddz.js +1 -0
  110. streamlit/static/static/js/{index.Cb9gN2T2.js → index.Dbe-Q3C-.js} +1 -1
  111. streamlit/static/static/js/index.DcPNYEUo.js +1 -0
  112. streamlit/static/static/js/index.DuxqVQpd.js +1 -0
  113. streamlit/static/static/js/{index.BH79B25f.js → index.FFOzOWzC.js} +1 -1
  114. streamlit/static/static/js/{index.DWedOrkQ.js → index.GRUzrudl.js} +1 -1
  115. streamlit/static/static/js/{input.CbP5ZuQ7.js → input.s6pjQ49A.js} +1 -1
  116. streamlit/static/static/js/{memory.BuacVo2L.js → memory.Cuvsdfrl.js} +1 -1
  117. streamlit/static/static/js/{number-overlay-editor.BZb9zRl_.js → number-overlay-editor.DdgVR5m3.js} +1 -1
  118. streamlit/static/static/js/{possibleConstructorReturn.DSM84rOS.js → possibleConstructorReturn.CqidKeei.js} +1 -1
  119. streamlit/static/static/js/{sandbox.C480llMG.js → sandbox.CCQREcJx.js} +1 -1
  120. streamlit/static/static/js/{timepicker.BunxCVp7.js → timepicker.mkJF97Bb.js} +4 -4
  121. streamlit/static/static/js/{toConsumableArray.B4o8rEx1.js → toConsumableArray.De7I7KVR.js} +1 -1
  122. streamlit/static/static/js/{uniqueId.tii0yosY.js → uniqueId.RI1LJdtz.js} +1 -1
  123. streamlit/static/static/js/{useBasicWidgetState.Bnm4FD6K.js → useBasicWidgetState.CedkNjUW.js} +1 -1
  124. streamlit/static/static/js/{useTextInputAutoExpand.Dgtwc1m0.js → useTextInputAutoExpand.Ca7w8dVs.js} +2 -2
  125. streamlit/static/static/js/{useUpdateUiValue.DjXdMFGw.js → useUpdateUiValue.DeXelfRH.js} +1 -1
  126. streamlit/static/static/js/withFullScreenWrapper.C3561XxJ.js +1 -0
  127. streamlit/static/static/media/MaterialSymbols-Rounded.DeCZgS-4.woff2 +0 -0
  128. streamlit/string_util.py +58 -1
  129. streamlit/web/bootstrap.py +0 -31
  130. streamlit/web/server/routes.py +17 -4
  131. streamlit/web/server/server.py +1 -0
  132. {streamlit-1.49.0.dist-info → streamlit-1.50.0.dist-info}/METADATA +1 -1
  133. {streamlit-1.49.0.dist-info → streamlit-1.50.0.dist-info}/RECORD +137 -136
  134. streamlit/static/static/css/index.COe1010n.css +0 -1
  135. streamlit/static/static/js/InputInstructions.C254RU9X.js +0 -1
  136. streamlit/static/static/js/Toolbar.BO_3WBaS.js +0 -1
  137. streamlit/static/static/js/data-grid-overlay-editor.C9gQLEnU.js +0 -1
  138. streamlit/static/static/js/index.BDZorv41.js +0 -1
  139. streamlit/static/static/js/index.BeTC4Yl-.js +0 -197
  140. streamlit/static/static/js/index.BnOd05Ko.js +0 -2
  141. streamlit/static/static/js/index.Bpe4-O2W.js +0 -1
  142. streamlit/static/static/js/index.C1qCS-sd.js +0 -1
  143. streamlit/static/static/js/index.C77g9sAQ.js +0 -3
  144. streamlit/static/static/js/index.Ca3y4ztK.js +0 -1
  145. streamlit/static/static/js/index.CbwuUwu4.js +0 -12
  146. streamlit/static/static/js/index.DKb-BAE2.js +0 -1
  147. streamlit/static/static/js/index.DStzYLqM.js +0 -73
  148. streamlit/static/static/js/index.DVKQKDLu.js +0 -1
  149. streamlit/static/static/js/index.DYbRPmVF.js +0 -1
  150. streamlit/static/static/js/index.z992t-BQ.js +0 -7
  151. streamlit/static/static/js/withFullScreenWrapper.0cy2pVf5.js +0 -1
  152. streamlit/static/static/media/MaterialSymbols-Rounded.CBxVaFdk.woff2 +0 -0
  153. {streamlit-1.49.0.data → streamlit-1.50.0.data}/scripts/streamlit.cmd +0 -0
  154. {streamlit-1.49.0.dist-info → streamlit-1.50.0.dist-info}/WHEEL +0 -0
  155. {streamlit-1.49.0.dist-info → streamlit-1.50.0.dist-info}/entry_points.txt +0 -0
  156. {streamlit-1.49.0.dist-info → streamlit-1.50.0.dist-info}/top_level.txt +0 -0
@@ -19,14 +19,7 @@ from __future__ import annotations
19
19
  from dataclasses import dataclass
20
20
  from datetime import date
21
21
  from enum import Enum
22
- from typing import (
23
- TYPE_CHECKING,
24
- Any,
25
- Final,
26
- Literal,
27
- TypedDict,
28
- cast,
29
- )
22
+ from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict, cast
30
23
 
31
24
  from typing_extensions import TypeAlias
32
25
 
@@ -47,6 +40,10 @@ if TYPE_CHECKING:
47
40
  import pandas as pd
48
41
 
49
42
  from streamlit.dataframe_util import Data
43
+ from streamlit.elements.lib.layout_utils import (
44
+ Height,
45
+ Width,
46
+ )
50
47
 
51
48
  VegaLiteType: TypeAlias = Literal["quantitative", "ordinal", "temporal", "nominal"]
52
49
  ChartStackType: TypeAlias = Literal["normalize", "center", "layered"]
@@ -59,6 +56,7 @@ class PrepDataColumns(TypedDict):
59
56
  y_column_list: list[str]
60
57
  color_column: str | None
61
58
  size_column: str | None
59
+ sort_column: str | None
62
60
 
63
61
 
64
62
  @dataclass
@@ -73,13 +71,14 @@ class AddRowsMetadata:
73
71
  columns: PrepDataColumns
74
72
  # Chart styling properties
75
73
  color: str | Color | list[Color] | None = None
76
- width: int | None = None
77
- height: int | None = None
78
- use_container_width: bool = True
74
+ width: Width | None = None
75
+ height: Height | None = None
76
+ use_container_width: bool | None = None
79
77
  # Only applicable for bar & area charts
80
78
  stack: bool | ChartStackType | None = None
81
79
  # Only applicable for bar charts
82
80
  horizontal: bool = False
81
+ sort: bool | str = False
83
82
 
84
83
 
85
84
  class ChartType(Enum):
@@ -151,13 +150,14 @@ def generate_chart(
151
150
  y_axis_label: str | None = None,
152
151
  color_from_user: str | Color | list[Color] | None = None,
153
152
  size_from_user: str | float | None = None,
154
- width: int | None = None,
155
- height: int | None = None,
156
- use_container_width: bool = True,
153
+ width: Width | None = None,
154
+ height: Height | None = None,
155
+ use_container_width: bool | None = None,
157
156
  # Bar & Area charts only:
158
157
  stack: bool | ChartStackType | None = None,
159
158
  # Bar charts only:
160
159
  horizontal: bool = False,
160
+ sort_from_user: bool | str = False,
161
161
  ) -> tuple[alt.Chart | alt.LayerChart, AddRowsMetadata]:
162
162
  """Function to use the chart's type, data columns and indices to figure out the
163
163
  chart's spec.
@@ -181,6 +181,8 @@ def generate_chart(
181
181
  # Get name of column to use for size, or constant value to use. Any/both could
182
182
  # be None.
183
183
  size_column, size_value = _parse_generic_column(df, size_from_user)
184
+ # Get name of column to use for sort.
185
+ sort_column = _parse_sort_column(df, sort_from_user)
184
186
 
185
187
  # Store some info so we can use it in add_rows.
186
188
  add_rows_metadata = AddRowsMetadata(
@@ -194,6 +196,7 @@ def generate_chart(
194
196
  "y_column_list": y_column_list,
195
197
  "color_column": color_column,
196
198
  "size_column": size_column,
199
+ "sort_column": sort_column,
197
200
  },
198
201
  # Chart styling properties
199
202
  color=color_from_user,
@@ -202,13 +205,13 @@ def generate_chart(
202
205
  use_container_width=use_container_width,
203
206
  stack=stack,
204
207
  horizontal=horizontal,
208
+ sort=sort_from_user,
205
209
  )
206
210
 
207
211
  # At this point, all foo_column variables are either None/empty or contain actual
208
212
  # columns that are guaranteed to exist.
209
-
210
- df, x_column, y_column, color_column, size_column = _prep_data(
211
- df, x_column, y_column_list, color_column, size_column
213
+ df, x_column, y_column, color_column, size_column, sort_column = _prep_data(
214
+ df, x_column, y_column_list, color_column, size_column, sort_column
212
215
  )
213
216
 
214
217
  # At this point, x_column is only None if user did not provide one AND df is empty.
@@ -224,14 +227,18 @@ def generate_chart(
224
227
  x_axis_label,
225
228
  y_axis_label,
226
229
  stack,
230
+ sort_from_user,
227
231
  )
228
232
 
233
+ chart_width = width if isinstance(width, int) else None
234
+ chart_height = height if isinstance(height, int) else None
235
+
229
236
  # Create a Chart with x and y encodings.
230
237
  chart = alt.Chart(
231
238
  data=df,
232
239
  mark=chart_type.value["mark_type"],
233
- width=width or 0,
234
- height=height or 0,
240
+ width=chart_width or 0,
241
+ height=chart_height or 0,
235
242
  ).encode(
236
243
  x=x_encoding,
237
244
  y=y_encoding,
@@ -281,7 +288,7 @@ def generate_chart(
281
288
  and is_altair_version_5_or_greater
282
289
  ):
283
290
  return _add_improved_hover_tooltips(
284
- chart, x_column, width, height
291
+ chart, x_column, chart_width, chart_height
285
292
  ).interactive(), add_rows_metadata
286
293
 
287
294
  return chart.interactive(), add_rows_metadata
@@ -359,6 +366,7 @@ def prep_chart_data_for_add_rows(
359
366
  y_column_list=add_rows_metadata.columns["y_column_list"],
360
367
  color_column=add_rows_metadata.columns["color_column"],
361
368
  size_column=add_rows_metadata.columns["size_column"],
369
+ sort_column=add_rows_metadata.columns["sort_column"],
362
370
  )
363
371
 
364
372
  return out_data, add_rows_metadata
@@ -437,7 +445,8 @@ def _prep_data(
437
445
  y_column_list: list[str],
438
446
  color_column: str | None,
439
447
  size_column: str | None,
440
- ) -> tuple[pd.DataFrame, str | None, str | None, str | None, str | None]:
448
+ sort_column: str | None = None,
449
+ ) -> tuple[pd.DataFrame, str | None, str | None, str | None, str | None, str | None]:
441
450
  """Prepares the data for charting. This is also used in add_rows.
442
451
 
443
452
  Returns the prepared dataframe and the new names of the x column (taking the index
@@ -450,7 +459,7 @@ def _prep_data(
450
459
 
451
460
  # Drop columns we're not using.
452
461
  selected_data = _drop_unused_columns(
453
- df, x_column, color_column, size_column, *y_column_list
462
+ df, x_column, color_column, size_column, sort_column, *y_column_list
454
463
  )
455
464
 
456
465
  # Maybe convert color to Vega colors.
@@ -462,17 +471,18 @@ def _prep_data(
462
471
  y_column_list,
463
472
  color_column,
464
473
  size_column,
474
+ sort_column,
465
475
  ) = _convert_col_names_to_str_in_place(
466
- selected_data, x_column, y_column_list, color_column, size_column
476
+ selected_data, x_column, y_column_list, color_column, size_column, sort_column
467
477
  )
468
478
 
469
479
  # Maybe melt data from wide format into long format.
470
480
  melted_data, y_column, color_column = _maybe_melt(
471
- selected_data, x_column, y_column_list, color_column, size_column
481
+ selected_data, x_column, y_column_list, color_column, size_column, sort_column
472
482
  )
473
483
 
474
484
  # Return the data, but also the new names to use for x, y, and color.
475
- return melted_data, x_column, y_column, color_column, size_column
485
+ return melted_data, x_column, y_column, color_column, size_column, sort_column
476
486
 
477
487
 
478
488
  def _last_index_for_melted_dataframes(
@@ -505,7 +515,7 @@ def _is_date_column(df: pd.DataFrame, name: str | None) -> bool:
505
515
  if column.size == 0:
506
516
  return False
507
517
 
508
- return isinstance(column.iloc[0], date)
518
+ return isinstance(column.iat[0], date)
509
519
 
510
520
 
511
521
  def _melt_data(
@@ -636,7 +646,7 @@ def _maybe_convert_color_column_in_place(
636
646
  if color_column is None or len(df[color_column]) == 0:
637
647
  return
638
648
 
639
- first_color_datum = df[color_column].iloc[0]
649
+ first_color_datum = df[color_column].iat[0]
640
650
 
641
651
  if is_hex_color_like(first_color_datum):
642
652
  # Hex is already CSS-valid.
@@ -657,7 +667,8 @@ def _convert_col_names_to_str_in_place(
657
667
  y_column_list: list[str],
658
668
  color_column: str | None,
659
669
  size_column: str | None,
660
- ) -> tuple[str | None, list[str], str | None, str | None]:
670
+ sort_column: str | None,
671
+ ) -> tuple[str | None, list[str], str | None, str | None, str | None]:
661
672
  """Converts column names to strings, since Vega-Lite does not accept ints, etc."""
662
673
  import pandas as pd
663
674
 
@@ -670,6 +681,7 @@ def _convert_col_names_to_str_in_place(
670
681
  [str(c) for c in y_column_list],
671
682
  None if color_column is None else str(color_column),
672
683
  None if size_column is None else str(size_column),
684
+ None if sort_column is None else str(sort_column),
673
685
  )
674
686
 
675
687
 
@@ -703,6 +715,17 @@ def _parse_x_column(df: pd.DataFrame, x_from_user: str | None) -> str | None:
703
715
  )
704
716
 
705
717
 
718
+ def _parse_sort_column(df: pd.DataFrame, sort_from_user: bool | str) -> str | None:
719
+ if sort_from_user is False or sort_from_user is True:
720
+ return None
721
+
722
+ sort_column = sort_from_user.removeprefix("-")
723
+ if sort_column not in df.columns:
724
+ raise StreamlitColumnNotFoundError(df, sort_column)
725
+
726
+ return sort_column
727
+
728
+
706
729
  def _parse_y_columns(
707
730
  df: pd.DataFrame,
708
731
  y_from_user: str | Sequence[str] | None,
@@ -790,6 +813,7 @@ def _maybe_melt(
790
813
  y_column_list: list[str],
791
814
  color_column: str | None,
792
815
  size_column: str | None,
816
+ sort_column: str | None,
793
817
  ) -> tuple[pd.DataFrame, str | None, str | None]:
794
818
  """If multiple columns are set for y, melt the dataframe into long format."""
795
819
  y_column: str | None
@@ -806,6 +830,8 @@ def _maybe_melt(
806
830
  columns_to_leave_alone = [x_column]
807
831
  if size_column:
808
832
  columns_to_leave_alone.append(size_column)
833
+ if sort_column:
834
+ columns_to_leave_alone.append(sort_column)
809
835
 
810
836
  df = _melt_data(
811
837
  df=df,
@@ -828,8 +854,10 @@ def _get_axis_encodings(
828
854
  x_axis_label: str | None,
829
855
  y_axis_label: str | None,
830
856
  stack: bool | ChartStackType | None,
857
+ sort_from_user: bool | str,
831
858
  ) -> tuple[alt.X, alt.Y]:
832
859
  stack_encoding: alt.X | alt.Y
860
+ sort_encoding: alt.X | alt.Y
833
861
  if chart_type == ChartType.HORIZONTAL_BAR:
834
862
  # Handle horizontal bar chart - switches x and y data:
835
863
  x_encoding = _get_x_encoding(
@@ -839,6 +867,7 @@ def _get_axis_encodings(
839
867
  df, x_column, x_from_user, y_axis_label, chart_type
840
868
  )
841
869
  stack_encoding = x_encoding
870
+ sort_encoding = y_encoding
842
871
  else:
843
872
  x_encoding = _get_x_encoding(
844
873
  df, x_column, x_from_user, x_axis_label, chart_type
@@ -847,10 +876,15 @@ def _get_axis_encodings(
847
876
  df, y_column, y_from_user, y_axis_label, chart_type
848
877
  )
849
878
  stack_encoding = y_encoding
879
+ sort_encoding = x_encoding
850
880
 
851
881
  # Handle stacking - only relevant for bar & area charts
852
882
  _update_encoding_with_stack(stack, stack_encoding)
853
883
 
884
+ # Handle sorting - only relevant for bar charts
885
+ if chart_type in (ChartType.VERTICAL_BAR, ChartType.HORIZONTAL_BAR):
886
+ _update_encoding_with_sort(sort_from_user, sort_encoding)
887
+
854
888
  return x_encoding, y_encoding
855
889
 
856
890
 
@@ -957,6 +991,38 @@ def _update_encoding_with_stack(
957
991
  encoding["stack"] = stack
958
992
 
959
993
 
994
+ def _update_encoding_with_sort(
995
+ sort_from_user: bool | str,
996
+ encoding: alt.X | alt.Y,
997
+ ) -> None:
998
+ """Apply sort to the given encoding in-place.
999
+
1000
+ - If sort is False: disable Altair's default sorting on the bar's categorical axis
1001
+ (i.e., set to None).
1002
+ - If sort is True: use Altair's default sorting.
1003
+ - If sort is a column name (optionally starting with '-') set a SortField with the correct order.
1004
+
1005
+ Note: Column validation should be done before calling this function.
1006
+ """
1007
+ import altair as alt
1008
+
1009
+ if sort_from_user is False:
1010
+ # Disable Altair's default sorting
1011
+ encoding["sort"] = None
1012
+ elif sort_from_user is True:
1013
+ # Use Altair's default sorting
1014
+ pass
1015
+ else:
1016
+ # String: sort by column name (optional '-' prefix for descending)
1017
+ sort_order: Literal["ascending", "descending"]
1018
+ if sort_from_user.startswith("-"):
1019
+ sort_order = "descending"
1020
+ else:
1021
+ sort_order = "ascending"
1022
+ sort_field = sort_from_user.removeprefix("-")
1023
+ encoding["sort"] = alt.SortField(field=sort_field, order=sort_order)
1024
+
1025
+
960
1026
  def _get_color_encoding(
961
1027
  df: pd.DataFrame,
962
1028
  color_value: Color | None,
@@ -1022,7 +1088,7 @@ def _get_color_encoding(
1022
1088
 
1023
1089
  # If the 0th element in the color column looks like a color, we'll use the color
1024
1090
  # column's values as the colors in our chart.
1025
- elif len(df[color_column]) and is_color_like(df[color_column].iloc[0]):
1091
+ elif len(df[color_column]) and is_color_like(df[color_column].iat[0]):
1026
1092
  color_range = [to_css_color(c) for c in df[color_column].unique()]
1027
1093
  color_enc["scale"] = alt.Scale(range=color_range)
1028
1094
  # Don't show the color legend, because it will just show text with the
@@ -109,6 +109,11 @@ _EDITING_COMPATIBILITY_MAPPING: Final[dict[ColumnType, list[ColumnDataKind]]] =
109
109
  ColumnDataKind.STRING,
110
110
  ColumnDataKind.EMPTY,
111
111
  ],
112
+ "multiselect": [
113
+ ColumnDataKind.LIST,
114
+ ColumnDataKind.STRING,
115
+ ColumnDataKind.EMPTY,
116
+ ],
112
117
  }
113
118
 
114
119