Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/library/graphing/other-charts/plotly.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ def line_chart():
)
```

## Locale Configuration

Use `locale` to localize Plotly number/date formatting and modebar labels:

```python demo exec
df = px.data.gapminder().query("country=='Canada'")
fig = px.line(df, x="year", y="lifeExp", title="Life expectancy in Canada")


def localized_line_chart():
return rx.center(
rx.plotly(
data=fig,
locale="de",
),
)
```

You can still pass `config`; when both are provided, `locale=` is applied as the final locale value.

## 3D graphing example

Let's create a 3D surface plot of Mount Bruno. This is a slightly more complicated example, but it wraps in Reflex using the same method. In fact, you can wrap any figure using the same approach.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class Plotly(NoSSRComponent):

config: Var[dict] = field(doc="The config of the graph.")

locale: Var[str] = field(
doc="The locale code used for Plotly formatting and modebar labels."
)

use_resize_handler: Var[bool] = field(
default=LiteralVar.create(True),
doc="If true, the graph will resize when the window is resized.",
Expand Down Expand Up @@ -175,15 +179,20 @@ class Plotly(NoSSRComponent):
doc="Fired when a hovered element is no longer hovered."
)

def add_imports(self) -> dict[str, str]:
def add_imports(self) -> ImportDict:
"""Add imports for the plotly component.

Returns:
The imports for the plotly component.
"""
return {
# For merging plotly data/layout/templates.
"mergician@v2.0.2": "mergician"
"mergician@v2.0.2": "mergician",
# For locale dictionaries injected into plot config.locales.
"plotly.js-locales@3.5.0": ImportVar(
tag="plotlyLocales",
is_default=True,
),
}
Comment on lines 188 to 196
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Locale bundle always included regardless of usage

plotly.js-locales@3.5.0 is unconditionally added to the import map, and add_custom_code() always emits the two helper functions that reference it. This means every app that uses rx.plotly (even with no locale prop) will bundle all 70+ locale definitions and include the helper code, adding unnecessary weight for users who never touch the locale feature.

A conditional approach would require add_imports and add_custom_code to only emit locale-related output when the component is actually configured with a locale, which isn't easy at the class level. At minimum, the trade-off should be noted in the docstring or changelog so users are aware of the bundle size impact.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

agree. see how we handle it in moment.py when the user supplies the tz prop. the import should be added conditionally based on the locale prop being provided

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The pattern from moment.py is clear. Here's how to apply it to the plotly add_imports:

Suggested change
return {
# For merging plotly data/layout/templates.
"mergician@v2.0.2": "mergician"
"mergician@v2.0.2": "mergician",
# For locale dictionaries injected into plot config.locales.
"plotly.js-locales@3.5.0": ImportVar(
tag="plotlyLocales",
is_default=True,
),
}
def add_imports(self) -> ImportDict:
"""Add imports for the plotly component.
Returns:
The imports for the plotly component.
"""
imports: ImportDict = {
# For merging plotly data/layout/templates.
"mergician@v2.0.2": "mergician",
}
if self.locale is not None:
# For locale dictionaries injected into plot config.locales.
imports["plotly.js-locales@3.5.0"] = ImportVar(
tag="plotlyLocales",
is_default=True,
)
return imports

Same idea applies to add_custom_code — it should also gate the locale helper functions behind if self.locale is not None so the helper code is only emitted when a locale is actually used.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.


def add_custom_code(self) -> list[str]:
Expand Down Expand Up @@ -222,6 +231,43 @@ def add_custom_code(self) -> list[str]:
})
})
}
""",
"""
const _rxResolvePlotlyLocaleData = (plotlyLocales, locale) => {
if (locale === undefined || locale === null) return null;
const localeString = String(locale).trim();
if (localeString === "") return null;

const normalizedLocale = localeString.toLowerCase().replace(/_/g, "-");
const localesObject = plotlyLocales?.default ?? plotlyLocales;
if (!localesObject || typeof localesObject !== "object") return null;

return (
localesObject[normalizedLocale] ??
localesObject[normalizedLocale.split("-")[0]] ??
null
);
}

const _rxGetPlotlyLocaleConfig = (config, locale, plotlyLocales) => {
const localeData = _rxResolvePlotlyLocaleData(plotlyLocales, locale);
if (!localeData) {
if (locale === undefined || locale === null || String(locale).trim() === "") {
return config;
}
return { ...config, locale: String(locale) };
}

const localeName = localeData?.name ?? String(locale);
return {
...config,
locale: localeName,
locales: {
...(config?.locales ?? {}),
[localeName]: localeData,
},
};
}
""",
]

Expand Down Expand Up @@ -251,7 +297,7 @@ def create(cls, *children, **props) -> Component:

def _exclude_props(self) -> set[str]:
# These props are handled specially in the _render function
return {"data", "layout", "template"}
return {"data", "layout", "template", "locale"}

def _render(self):
tag = super()._render()
Expand Down Expand Up @@ -285,6 +331,16 @@ def _render(self):
Var(_js_expr=str(figure)),
]
)
if self.locale is not None:
config = self.config if self.config is not None else LiteralVar.create({})
tag = tag.set(
props={
**tag.props,
"config": Var(
_js_expr=f"_rxGetPlotlyLocaleConfig({config!s},{self.locale!s},plotlyLocales)"
),
},
)
return tag


Expand Down
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "e3ec310276f9d091fbb0261e523ca9ed",
"packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "da02f81678d920a68101c08fe64483a5",
"packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "d6a02e447dfd3c91bba84bcd02722aed",
"packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "91e956633778c6992f04940c69ff7140",
"packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "3db32f22ee4339e21796e206e1935de8",
"packages/reflex-components-radix/src/reflex_components_radix/__init__.pyi": "19216eb3618f68c8a76e5e43801cf4af",
"packages/reflex-components-radix/src/reflex_components_radix/primitives/__init__.pyi": "5404a8da97e8b5129133d7f300e3f642",
"packages/reflex-components-radix/src/reflex_components_radix/primitives/accordion.pyi": "e8ef2b44f2afe3e9b8d678d523673882",
Expand Down
32 changes: 32 additions & 0 deletions tests/units/components/graphing/test_plotly.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,35 @@ def test_plotly_config_option(plotly_fig: go.Figure):
"""
# This tests just confirm that the component can be created with a config option.
_ = rx.plotly(data=plotly_fig, config={"showLink": True})


def test_plotly_locale_option_merges_into_config(plotly_fig: go.Figure):
"""Test that locale is passed through plot config.

Args:
plotly_fig: The figure to display.
"""
component = rx.plotly(data=plotly_fig, locale="de")
rendered = component._render()

config_var = rendered.props.get("config")
assert config_var is not None
assert "locale" not in rendered.props
assert "_rxGetPlotlyLocaleConfig" in str(config_var)
assert "de" in str(config_var)


def test_plotly_basic_locale_option_merges_into_config(plotly_fig: go.Figure):
"""Test that locale works for dynamic plotly dist variants too.

Args:
plotly_fig: The figure to display.
"""
component = rx.plotly.basic(data=plotly_fig, locale="fr")
rendered = component._render()

config_var = rendered.props.get("config")
assert config_var is not None
assert "locale" not in rendered.props
assert "_rxGetPlotlyLocaleConfig" in str(config_var)
assert "fr" in str(config_var)
Loading