diff --git a/pyi_hashes.json b/pyi_hashes.json index ec9bbff8850..b3727031f38 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -3,22 +3,22 @@ "reflex/components/__init__.pyi": "ac05995852baa81062ba3d18fbc489fb", "reflex/components/base/__init__.pyi": "16e47bf19e0d62835a605baa3d039c5a", "reflex/components/base/app_wrap.pyi": "22e94feaa9fe675bcae51c412f5b67f1", - "reflex/components/base/body.pyi": "e8ab029a730824bab6d4211203609e6a", + "reflex/components/base/body.pyi": "b711b2324634fb6950b0f18ddc6d6357", "reflex/components/base/document.pyi": "311c53c90a60587a82e760103758a3cf", "reflex/components/base/error_boundary.pyi": "a678cceea014cb16048647257cd24ba6", "reflex/components/base/fragment.pyi": "745f1be02c23a0b25d7c52d7423ec76a", - "reflex/components/base/link.pyi": "0bc1d26ee29d8864aed14a12991bd47d", - "reflex/components/base/meta.pyi": "129aecf65ab53f756c4d1cbe1d0b188d", - "reflex/components/base/script.pyi": "e5f506d1d0d6712cb9e597a781eb3941", + "reflex/components/base/link.pyi": "5675330b0afd94e4af9ef0f52218dd5d", + "reflex/components/base/meta.pyi": "9dabed857da042e25d13da918db64eea", + "reflex/components/base/script.pyi": "8f35f06436d8e5181a0ef414e86dc74b", "reflex/components/base/strict_mode.pyi": "6b72e16caadf7158ab744a0ab751b010", "reflex/components/core/__init__.pyi": "007170b97e58bdf28b2aee381d91c0c7", - "reflex/components/core/auto_scroll.pyi": "18068d22aca7244a08cd0c5a897c0950", - "reflex/components/core/banner.pyi": "fd93e7a92961de8524718ad32135c37c", + "reflex/components/core/auto_scroll.pyi": "f360e3f9b8de450459f4eb0d1b0e912f", + "reflex/components/core/banner.pyi": "197e93bb0096b72b56b326d6b4edcb55", "reflex/components/core/clipboard.pyi": "a844eb927d9bc2a43f5e88161b258539", "reflex/components/core/debounce.pyi": "055da7aa890f44fb4d48bd5978f1a874", "reflex/components/core/helmet.pyi": "43f8497c8fafe51e29dca1dd535d143a", - "reflex/components/core/html.pyi": "86eb9d4c1bb4807547b2950d9a32e9fd", - "reflex/components/core/sticky.pyi": "cb763b986a9b0654d1a3f33440dfcf60", + "reflex/components/core/html.pyi": "025087c029f5220955e58ed454684544", + "reflex/components/core/sticky.pyi": "3149961127317a0dabbea867bf2eff89", "reflex/components/core/upload.pyi": "6dc28804a6dddf903e31162e87c1b023", "reflex/components/core/window_events.pyi": "af33ccec866b9540ee7fbec6dbfbd151", "reflex/components/datadisplay/__init__.pyi": "52755871369acbfd3a96b46b9a11d32e", @@ -28,90 +28,90 @@ "reflex/components/el/__init__.pyi": "0adfd001a926a2a40aee94f6fa725ecc", "reflex/components/el/element.pyi": "c5974a92fbc310e42d0f6cfdd13472f4", "reflex/components/el/elements/__init__.pyi": "29512d7a6b29c6dc5ff68d3b31f26528", - "reflex/components/el/elements/base.pyi": "3f74c7ea573ea29b055b0cd48b040d2c", - "reflex/components/el/elements/forms.pyi": "8b6bb2fbaf4bad828b076e2f7c8444d0", - "reflex/components/el/elements/inline.pyi": "3549cd6ad45217aa6387800911b641c3", - "reflex/components/el/elements/media.pyi": "9b97220aa99783d402b6e278c4069043", - "reflex/components/el/elements/metadata.pyi": "24448004b7aa07f1225028a85bd49fef", - "reflex/components/el/elements/other.pyi": "0c4d5d0b955d8596bf6cf4a48d7decdb", - "reflex/components/el/elements/scripts.pyi": "d33df9f21f7e838376b2b5024beef7c9", - "reflex/components/el/elements/sectioning.pyi": "3c5a7e4caa9c25da0ae788f02466eac4", - "reflex/components/el/elements/tables.pyi": "686eb70ea7d8c4dafb0cc5c284e76184", - "reflex/components/el/elements/typography.pyi": "684e83dde887dba12badd0fb75c87c04", + "reflex/components/el/elements/base.pyi": "3b3ddc9f080410f2c482e7234d147137", + "reflex/components/el/elements/forms.pyi": "4f4b3a7d9322969b5433952bc37c4494", + "reflex/components/el/elements/inline.pyi": "fba23b08aea01b9f0fca9e46eac47221", + "reflex/components/el/elements/media.pyi": "e7faa24a345ec76ec7ab098509fb6bad", + "reflex/components/el/elements/metadata.pyi": "eca0c61cc55933425726130ecf292baa", + "reflex/components/el/elements/other.pyi": "f34e66c4779e0182e4a410944cfade9d", + "reflex/components/el/elements/scripts.pyi": "6d5db9fa99b4f5e547fcfcf8bbe77f16", + "reflex/components/el/elements/sectioning.pyi": "bf5cc36a2b2a37b71056d5b649d338e5", + "reflex/components/el/elements/tables.pyi": "a9ea7ee769109e55ba5d4c1d4be37cc1", + "reflex/components/el/elements/typography.pyi": "f087c042d2e559099c5a218b0097202d", "reflex/components/gridjs/datatable.pyi": "98a7e1b3f3b60cafcdfcd8879750ee42", - "reflex/components/lucide/icon.pyi": "dcb8773ef162f3ec5759efe11374cf5e", - "reflex/components/markdown/markdown.pyi": "dd74e8e9665b2a813ff799a7aa190b44", - "reflex/components/moment/moment.pyi": "e1952f1c2c82cef85d91e970d1be64ab", - "reflex/components/plotly/plotly.pyi": "4311a0aae2abcc9226abb6a273f96372", + "reflex/components/lucide/icon.pyi": "f5c75bd6c0c44c7dc2ae1bd7ca777beb", + "reflex/components/markdown/markdown.pyi": "db4efc4508acb16664db909d6aff26d6", + "reflex/components/moment/moment.pyi": "46dca271b3a717ecb4cba529917d251e", + "reflex/components/plotly/plotly.pyi": "3fb14bc7cabc520b40861c6c5035b976", "reflex/components/radix/__init__.pyi": "5d8e3579912473e563676bfc71f29191", "reflex/components/radix/primitives/__init__.pyi": "01c388fe7a1f5426a16676404344edf6", - "reflex/components/radix/primitives/accordion.pyi": "19484eca0ad53f538f5db04c09921738", - "reflex/components/radix/primitives/base.pyi": "9ef34884fb6028dc017df5e2db639c81", - "reflex/components/radix/primitives/dialog.pyi": "9ee73362bb59619c482b6b0d07033f37", - "reflex/components/radix/primitives/drawer.pyi": "921e45dfaf5b9131ef27c561c3acca2e", - "reflex/components/radix/primitives/form.pyi": "78055e820703c98c3b838aa889566365", - "reflex/components/radix/primitives/progress.pyi": "c917952d57ddb3e138a40c4005120d5e", - "reflex/components/radix/primitives/slider.pyi": "4ff06f0025d47f166132909b09ab96f8", + "reflex/components/radix/primitives/accordion.pyi": "9192299dac04a5172edd7b2f6b125f53", + "reflex/components/radix/primitives/base.pyi": "8918eb415aabd66736db1ba680de5eb9", + "reflex/components/radix/primitives/dialog.pyi": "13ec2d6d93e925427c68aa17adb1fdb1", + "reflex/components/radix/primitives/drawer.pyi": "c6ad2f60217fe25952f3a1ba88fbd72a", + "reflex/components/radix/primitives/form.pyi": "5ae87aa4e9e15c100645cf070963ebdb", + "reflex/components/radix/primitives/progress.pyi": "b26c99c1d827c0f599fff344746aeec3", + "reflex/components/radix/primitives/slider.pyi": "be343dcd0558917b054683c99d539a43", "reflex/components/radix/themes/__init__.pyi": "582b4a7ead62b2ae8605e17fa084c063", "reflex/components/radix/themes/base.pyi": "3e1ccd5ce5fef0b2898025193ee3d069", - "reflex/components/radix/themes/color_mode.pyi": "dda570583355d8c0d8f607be457ba7a1", + "reflex/components/radix/themes/color_mode.pyi": "66f0749351e21648cfee85b01ae4e477", "reflex/components/radix/themes/components/__init__.pyi": "efa279ee05479d7bb8a64d49da808d03", - "reflex/components/radix/themes/components/alert_dialog.pyi": "eed422fcc1ff5ccf3dbf6934699bd0b1", - "reflex/components/radix/themes/components/aspect_ratio.pyi": "71de4160d79840561c48b570197a4152", - "reflex/components/radix/themes/components/avatar.pyi": "e40c2f0fda6d2c028d83681a27f3fb96", - "reflex/components/radix/themes/components/badge.pyi": "58fd1a9c5d2f8762e2a0370311731ff5", - "reflex/components/radix/themes/components/button.pyi": "50f0b08ad5d1d1054ab537152f0f5c43", - "reflex/components/radix/themes/components/callout.pyi": "547f2570ffbd10db36b745566e9f1b17", - "reflex/components/radix/themes/components/card.pyi": "f7adb83f7b001a11bdd7fd6791fb3ffb", - "reflex/components/radix/themes/components/checkbox.pyi": "8eabb6887a5d0849a43e086a284814c2", - "reflex/components/radix/themes/components/checkbox_cards.pyi": "1d567fd04b4425abd5cc5aad10108aa9", - "reflex/components/radix/themes/components/checkbox_group.pyi": "8638582a623036f8893a3fa6080f2672", - "reflex/components/radix/themes/components/context_menu.pyi": "b9499d8bdd2c5565621fea5fe7d7a25a", - "reflex/components/radix/themes/components/data_list.pyi": "6f8d9c582e084c23966b992158193b72", - "reflex/components/radix/themes/components/dialog.pyi": "d2615f1a68c80ff930444d054b598c13", - "reflex/components/radix/themes/components/dropdown_menu.pyi": "43f8770c9adf93c73398d68f79048424", - "reflex/components/radix/themes/components/hover_card.pyi": "a96f4433237f9994decf935deff9f269", - "reflex/components/radix/themes/components/icon_button.pyi": "e930911d8ecbe61e5447e61c76a28ab6", - "reflex/components/radix/themes/components/inset.pyi": "bd7a2186b553bd4c86d83ff50c784066", - "reflex/components/radix/themes/components/popover.pyi": "91f8edefeb232cc6d48690b1838144c2", - "reflex/components/radix/themes/components/progress.pyi": "0e59587d5b3c8fe0d0067587f144e5b0", - "reflex/components/radix/themes/components/radio.pyi": "f375aa5ac746679618ea7dad257e3224", - "reflex/components/radix/themes/components/radio_cards.pyi": "9dc34a1ce2a1924eb1f41438ef84e80b", + "reflex/components/radix/themes/components/alert_dialog.pyi": "5aa411233f5f6dcf4a5b9462fd1e2c9b", + "reflex/components/radix/themes/components/aspect_ratio.pyi": "5d3266b64c0634630ef57b743ed7f846", + "reflex/components/radix/themes/components/avatar.pyi": "4fccc79784a38d13e2c3c75f824be8c7", + "reflex/components/radix/themes/components/badge.pyi": "abee5065dc6a20ec47cef413543c581d", + "reflex/components/radix/themes/components/button.pyi": "bf344b51e5b69b06ea8fde883ef4600e", + "reflex/components/radix/themes/components/callout.pyi": "0040f8fbff4b0f6e9c76d7bd35c1c48b", + "reflex/components/radix/themes/components/card.pyi": "518ffa82875221b67849e738b1ba6eb4", + "reflex/components/radix/themes/components/checkbox.pyi": "80733028f0d6d1a5d2a6e1b737ee70ee", + "reflex/components/radix/themes/components/checkbox_cards.pyi": "d787691fcae86c2ec27da4371c72de81", + "reflex/components/radix/themes/components/checkbox_group.pyi": "b233e9fbc189dee7794ba7ad49c1c82f", + "reflex/components/radix/themes/components/context_menu.pyi": "cab85b6ac7564b649bfc98acf972176f", + "reflex/components/radix/themes/components/data_list.pyi": "72615c11a8df2fd700ea1196848b8b27", + "reflex/components/radix/themes/components/dialog.pyi": "211827ca8d7c88526f811214fb182b25", + "reflex/components/radix/themes/components/dropdown_menu.pyi": "9c034dace9fdb2c889017ec54be29fe6", + "reflex/components/radix/themes/components/hover_card.pyi": "d049caf91d2b17f7a3604be513d42170", + "reflex/components/radix/themes/components/icon_button.pyi": "53b3c86b374a7562bd88a616630b9997", + "reflex/components/radix/themes/components/inset.pyi": "c50589a6c5bad98cf89e69784200598e", + "reflex/components/radix/themes/components/popover.pyi": "74fa0640ca9045b0d43469ec4b8ac706", + "reflex/components/radix/themes/components/progress.pyi": "bca3898da78f0ddd3f47bce92063bd10", + "reflex/components/radix/themes/components/radio.pyi": "a9a5b6fa46da99be40e2ff38fbdb6c6d", + "reflex/components/radix/themes/components/radio_cards.pyi": "59f7ef9153525a05b4ea1ae8ff85f9e1", "reflex/components/radix/themes/components/radio_group.pyi": "173254cf91908bcf6aa4fa21a747e2cf", - "reflex/components/radix/themes/components/scroll_area.pyi": "2e3539b0f6895dda127ee96e9864dbf9", + "reflex/components/radix/themes/components/scroll_area.pyi": "4e1f86c71e83e5aefcfc0ccce6f00dbb", "reflex/components/radix/themes/components/segmented_control.pyi": "1776f1ad936bae402007802b1ee98906", - "reflex/components/radix/themes/components/select.pyi": "2c7aee592972ff5f05da08154aa981c8", - "reflex/components/radix/themes/components/separator.pyi": "79e550cc10ee455f35d75d0e236fedd2", - "reflex/components/radix/themes/components/skeleton.pyi": "a25d3ceb56f99f736ea463579845c454", + "reflex/components/radix/themes/components/select.pyi": "f7196bf70b506b21ccce95301679b8b5", + "reflex/components/radix/themes/components/separator.pyi": "4db0b99e3ffc63c59a8d6d973c25495a", + "reflex/components/radix/themes/components/skeleton.pyi": "4bae01522e3f4efe5d04262c53f23571", "reflex/components/radix/themes/components/slider.pyi": "305a34c14ca8656ca9267e4c31aaa388", - "reflex/components/radix/themes/components/spinner.pyi": "b7e689e7d75635e379242fd113a1ea9a", - "reflex/components/radix/themes/components/switch.pyi": "f1ba948750a74126cda990e89a3ec7ef", - "reflex/components/radix/themes/components/table.pyi": "eefbbd1904deae3d166fcad28b20fd4a", + "reflex/components/radix/themes/components/spinner.pyi": "476098d606a9271ff49d7bbbae285acd", + "reflex/components/radix/themes/components/switch.pyi": "1edf811e25894d44e7ec5a8f031ff86a", + "reflex/components/radix/themes/components/table.pyi": "a1cab0e0602092e2571567e1b6136e0b", "reflex/components/radix/themes/components/tabs.pyi": "a533d2509a6798fe0ab7275b0152519d", - "reflex/components/radix/themes/components/text_area.pyi": "4af55e5d18a5b9d56717bf31b23ea543", - "reflex/components/radix/themes/components/text_field.pyi": "232618b744076db98d861ea1b9eb3192", - "reflex/components/radix/themes/components/tooltip.pyi": "2b8366200ce92ec4784ca3ec4152e676", + "reflex/components/radix/themes/components/text_area.pyi": "51f2c3f5bead1a41e19615580d9bdde0", + "reflex/components/radix/themes/components/text_field.pyi": "e32dd462113888b4e1591cc8a73549cf", + "reflex/components/radix/themes/components/tooltip.pyi": "9fa0019cf2c399bdd8c5f8751304f3b8", "reflex/components/radix/themes/layout/__init__.pyi": "73eefc509a49215b1797b5b5d28d035e", "reflex/components/radix/themes/layout/base.pyi": "5be31d7dadd23ab544e53762423d123e", - "reflex/components/radix/themes/layout/box.pyi": "dbaed1c50c668805fc7b71d22f878254", - "reflex/components/radix/themes/layout/center.pyi": "17323694217e8ad7611adb683f8d96ce", - "reflex/components/radix/themes/layout/container.pyi": "24222fd7ffa2dc05f709eab6c7b9643c", - "reflex/components/radix/themes/layout/flex.pyi": "0307e9dbe6a5784140121d77c8f67a86", - "reflex/components/radix/themes/layout/grid.pyi": "95c9edb8bdd4e39dc1bd6bc2a8ca0933", - "reflex/components/radix/themes/layout/list.pyi": "049ecf827ef0ba8de2d76dbf7b1c562c", - "reflex/components/radix/themes/layout/section.pyi": "a51952b9b5c8227aa3024373dedcad5d", - "reflex/components/radix/themes/layout/spacer.pyi": "c35accf0f2f742c90a23675ff1fb960d", - "reflex/components/radix/themes/layout/stack.pyi": "271d3315c6196356d3ced759520d4e7d", + "reflex/components/radix/themes/layout/box.pyi": "bc7da679cd170d1d0da0cac3f199e448", + "reflex/components/radix/themes/layout/center.pyi": "a1053d2c3d928a89a139ad31f1006bc5", + "reflex/components/radix/themes/layout/container.pyi": "f8714454df7a4611bd361956824ed64c", + "reflex/components/radix/themes/layout/flex.pyi": "4313b4d9a57038165ba260027cc97d1c", + "reflex/components/radix/themes/layout/grid.pyi": "f46fe59758a2077f85fac1622cfbc1ad", + "reflex/components/radix/themes/layout/list.pyi": "ef99a03bd30f3c68930f1685f9210455", + "reflex/components/radix/themes/layout/section.pyi": "9e2ff3e6c9bbfa3bcc30624565cdecc1", + "reflex/components/radix/themes/layout/spacer.pyi": "64daf05799ebdb6f63fc02f68434f003", + "reflex/components/radix/themes/layout/stack.pyi": "70fad9c2b420a005e4fa5309697e4bdd", "reflex/components/radix/themes/typography/__init__.pyi": "b8ef970530397e9984004961f3aaee62", - "reflex/components/radix/themes/typography/blockquote.pyi": "080c71899532f5dbf4cf143e7a5ad3bf", - "reflex/components/radix/themes/typography/code.pyi": "7ffe785d55979cf8ff97ea040f3e2b64", - "reflex/components/radix/themes/typography/heading.pyi": "0ebb38915cd0521fd59c569e04d288bb", - "reflex/components/radix/themes/typography/link.pyi": "e88c5d880a54548b6808c097ac62505b", - "reflex/components/radix/themes/typography/text.pyi": "50f9ca15a941e4b77ddd12e77aa3c03e", + "reflex/components/radix/themes/typography/blockquote.pyi": "8c71dc81f6d906f5ce8adfa810784f58", + "reflex/components/radix/themes/typography/code.pyi": "2b2b80772e93333f4019b7e1247d5819", + "reflex/components/radix/themes/typography/heading.pyi": "563629a1dcd8e710d6c606a80b14e0f3", + "reflex/components/radix/themes/typography/link.pyi": "ab2467b1f52317c49d25869ac3b6b50f", + "reflex/components/radix/themes/typography/text.pyi": "0fe931d9e6d988e4b8cb98f0e159b189", "reflex/components/react_player/audio.pyi": "0e1690ff1f1f39bc748278d292238350", "reflex/components/react_player/react_player.pyi": "5ccd373b94ed1d3934ae6afc46bd6fe4", "reflex/components/react_player/video.pyi": "998671c06103d797c554d9278eb3b2a0", - "reflex/components/react_router/dom.pyi": "3042fa630b7e26a7378fe045d7fbf4af", + "reflex/components/react_router/dom.pyi": "32d771821244da75c0e10eb74194634f", "reflex/components/recharts/__init__.pyi": "6ee7f1ca2c0912f389ba6f3251a74d99", "reflex/components/recharts/cartesian.pyi": "d138261ab8259d5208c2f028b9f708bd", "reflex/components/recharts/charts.pyi": "013036b9c00ad85a570efdb813c1bc40", diff --git a/reflex/components/plotly/plotly.py b/reflex/components/plotly/plotly.py index ecd9cbd56b0..b6ee9912431 100644 --- a/reflex/components/plotly/plotly.py +++ b/reflex/components/plotly/plotly.py @@ -72,8 +72,12 @@ class Plotly(NoSSRComponent): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js@3.3.1", + "plotly.js-locales@3.3.1", + ] + # tag stays as "Plot" — _render overrides the rendered name to _RxPlotLocale. tag = "Plot" is_default = True @@ -93,6 +97,11 @@ class Plotly(NoSSRComponent): # If true, the graph will resize when the window is resized. use_resize_handler: Var[bool] = LiteralVar.create(True) + # The BCP-47 locale code for chart formatting (e.g. "de", "zh-CN", "fr", "pt-BR"). + # When set, Plotly number separators, date formats, and modebar tooltips follow + # the chosen locale. Defaults to "" which means Plotly's built-in "en" locale. + locale: Var[str] = LiteralVar.create("") + # Fired after the plot is redrawn. on_after_plot: EventHandler[no_args_event_spec] @@ -153,24 +162,35 @@ class Plotly(NoSSRComponent): # Fired when a hovered element is no longer hovered. on_unhover: EventHandler[_event_points_data_signature] - def add_imports(self) -> dict[str, str]: + def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly component. Returns: The imports for the plotly component. """ - return { + return [ # For merging plotly data/layout/templates. - "mergician@v2.0.2": "mergician" - } + {"mergician@v2.0.2": "mergician"}, + # React hooks used inside _RxPlotLocale. + { + "react": [ + ImportVar(tag="React", is_default=True), + ImportVar(tag="useState"), + ImportVar(tag="useEffect"), + ] + }, + ] def add_custom_code(self) -> list[str]: - """Add custom codes for processing the plotly points data. + """Add custom codes for processing the plotly points data and locale support. Returns: Custom code snippets for the module level. """ return [ + # ------------------------------------------------------------------ # + # Existing helpers # + # ------------------------------------------------------------------ # "const removeUndefined = (obj) => {Object.keys(obj).forEach(key => obj[key] === undefined && delete obj[key]); return obj}", """ const extractPoints = (points) => { @@ -200,6 +220,78 @@ def add_custom_code(self) -> list[str]: }) }) } +""", + # ------------------------------------------------------------------ # + # Locale loading + _RxPlotLocale wrapper # + # # + # Key insight: plotly.js accepts inline locale data via the config # + # prop: config={{ locale: "de", locales: { de: localeData } }} # + # This avoids needing a Plotly instance / Plotly.register() entirely. # + # # + # _rxLocaleCache – resolved locale data objects keyed by locale code. # + # _rxLoadLocale – fetches and parses a CJS locale file via fetch + # + # new Function sandbox. Returns Promise. # + # _RxPlotLocale – wraps , loads locale data, injects it into # + # the config prop before forwarding to Plotly. # + # ------------------------------------------------------------------ # + """ +const _rxLocaleCache = {}; + +function _rxLoadLocale(locale) { + const key = locale.toLowerCase(); + if (_rxLocaleCache[key]) return Promise.resolve(_rxLocaleCache[key]); + const url = `/node_modules/plotly.js-locales/${key}.js`; + return fetch(url) + .then(r => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }) + .then(code => { + const mod = { exports: {} }; + new Function("module", "exports", code)(mod, mod.exports); + _rxLocaleCache[key] = mod.exports; + return mod.exports; + }) + .catch(e => { + console.warn( + "[rx.plotly] Locale \\"" + locale + "\\" could not be loaded: " + e.message + + ". Check https://www.npmjs.com/package/plotly.js-locales for supported codes." + ); + return null; + }); +} + +function _RxPlotLocale({ locale, config, ...rest }) { + const isEnglish = !locale || locale === "en"; + const [localeData, setLocaleData] = useState(null); + const [localeReady, setLocaleReady] = useState(isEnglish); + + useEffect(() => { + if (isEnglish) { + setLocaleData(null); + setLocaleReady(true); + return; + } + setLocaleReady(false); + _rxLoadLocale(locale).then(data => { + setLocaleData(data); + setLocaleReady(true); + }); + }, [locale]); + + if (!localeReady) return null; + + const key = locale ? locale.toLowerCase() : "en"; + const mergedConfig = (!isEnglish && localeData) + ? { + ...(config || {}), + locale: key, + locales: { [key]: localeData }, + } + : (config || {}); + + return React.createElement(Plot, { ...rest, config: mergedConfig }); +} """, ] @@ -216,6 +308,13 @@ def create(cls, *children, **props) -> Component: """ from plotly.graph_objs.layout import Template from plotly.io import templates + from reflex.config import get_config + + # Apply global plotly_locale from rxconfig.py if no per-chart locale given. + if not props.get("locale"): + global_locale = get_config().plotly_locale + if global_locale: + props["locale"] = global_locale responsive_template = color_mode_cond( light=LiteralVar.create(templates["plotly"]), @@ -228,11 +327,17 @@ def create(cls, *children, **props) -> Component: return super().create(*children, **props) def _exclude_props(self) -> set[str]: - # These props are handled specially in the _render function + # These props are handled specially in the _render function. + # `locale` is intentionally NOT excluded — it passes through as a normal + # prop to _RxPlotLocale which reads and handles it. return {"data", "layout", "template"} def _render(self): tag = super()._render() + # Render through _RxPlotLocale wrapper which handles locale loading. + # `tag = "Plot"` above tells Reflex to auto-import Plot from react-plotly.js; + # we override the rendered element name here so the JSX uses _RxPlotLocale. + tag = tag.set(name="_RxPlotLocale") figure = self.data.to(dict) if self.data is not None else Var.create({}) merge_dicts = [] # Data will be merged and spread from these dict Vars if self.layout is not None: @@ -303,7 +408,10 @@ class PlotlyBasic(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-basic-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-basic-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly basic component. @@ -329,7 +437,10 @@ class PlotlyCartesian(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-cartesian-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-cartesian-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly cartesian component. @@ -355,7 +466,10 @@ class PlotlyGeo(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-geo-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-geo-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly geo component. @@ -381,7 +495,10 @@ class PlotlyGl3d(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-gl3d-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-gl3d-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly 3d component. @@ -407,7 +524,10 @@ class PlotlyGl2d(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-gl2d-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-gl2d-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly 2d component. @@ -433,7 +553,10 @@ class PlotlyMapbox(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-mapbox-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-mapbox-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly mapbox component. @@ -459,7 +582,10 @@ class PlotlyFinance(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-finance-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-finance-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly finance component. @@ -485,7 +611,10 @@ class PlotlyStrict(Plotly): library = "react-plotly.js@2.6.0" - lib_dependencies: list[str] = ["plotly.js-strict-dist-min@3.3.1"] + lib_dependencies: list[str] = [ + "plotly.js-strict-dist-min@3.3.1", + "plotly.js-locales@3.3.1", + ] def add_imports(self) -> ImportDict | list[ImportDict]: """Add imports for the plotly strict component. diff --git a/reflex/config.py b/reflex/config.py index 47fd969ac49..4e0c6878a27 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -262,6 +262,8 @@ class BaseConfig: # List of plugin types to disable in the app. disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list) + plotly_locale: str = "" + # The transport method for client-server communication. transport: Literal["websocket", "polling"] = "websocket" diff --git a/tests/components/plotly/__init__.py b/tests/components/plotly/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/plotly/test_plotly.py b/tests/components/plotly/test_plotly.py new file mode 100644 index 00000000000..139e0bad12e --- /dev/null +++ b/tests/components/plotly/test_plotly.py @@ -0,0 +1,47 @@ +"""Tests for rx.plotly locale support.""" +from types import SimpleNamespace +from unittest.mock import patch +from reflex.components.plotly.plotly import Plotly +from reflex.vars.base import LiteralVar + + +def make_plotly(**props): + """Instantiate Plotly directly, bypassing create() which needs plotly installed.""" + component = Plotly.__new__(Plotly) + for k, v in props.items(): + setattr(component, k, LiteralVar.create(v)) + return component + + +def test_plotly_locale_default(): + """locale defaults to empty string.""" + c = make_plotly(locale="") + assert str(c.locale) == '""' + + +def test_plotly_locale_de(): + """locale prop stores German locale code.""" + c = make_plotly(locale="de") + assert str(c.locale) == '"de"' + + +def test_plotly_locale_zh(): + """locale prop stores Chinese locale code.""" + c = make_plotly(locale="zh-CN") + assert str(c.locale) == '"zh-CN"' + + +def test_config_has_plotly_locale(): + """plotly_locale field exists on Config.""" + from reflex.config import BaseConfig + import dataclasses + fields = {f.name for f in dataclasses.fields(BaseConfig)} + assert "plotly_locale" in fields + + +def test_config_plotly_locale_default(): + """plotly_locale defaults to empty string.""" + from reflex.config import BaseConfig + import dataclasses + field = next(f for f in dataclasses.fields(BaseConfig) if f.name == "plotly_locale") + assert field.default == ""