You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/07/22 19:54:15 UTC
(superset) 01/01: fix(plugin-chart-echarts): stop clobbering echarts builtin locales with empty objects
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch fix/echarts6-locale-clobber
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 72fbf1358d6afa81eed3bdcf435fbd273ec1032c
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Wed Jul 22 12:53:08 2026 -0700
fix(plugin-chart-echarts): stop clobbering echarts builtin locales with empty objects
The locale loaders imported echarts' i18n/lang*.js bundles, which are
side-effect UMD modules: they register the locale against a second
echarts instance (require('echarts/lib/echarts')) and export nothing.
Webpack's CommonJS interop therefore yielded .default === {}, and
registering that empty object under a builtin locale name ('EN'/'ZH')
wiped the builtin definitions. Every time-axis chart then crashed
inside echarts' time formatter ("TypeError: Cannot read properties of
null (reading '0')") while resolving time.month from the empty locale.
Import the lang*-obj.js variants instead — they export the locale
object itself, carry no side effects, and drop the duplicated echarts
lib bundle from the locale chunks. Guard registerLocale against
content-free locale objects, and register LegacyGridContainLabel so
grid.containLabel keeps working under echarts 6 instead of being
ignored and clipping axis labels.
Co-Authored-By: Claude Fable 5 <no...@anthropic.com>
---
.../plugin-chart-echarts/src/components/Echart.tsx | 64 +------------
.../src/components/echartsLocale.ts | 104 +++++++++++++++++++++
.../test/components/echartsLocale.test.ts | 78 ++++++++++++++++
3 files changed, 187 insertions(+), 59 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
index 043dd716efb..4eba1d22a4a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
@@ -63,7 +63,7 @@ import {
MarkAreaComponent,
MarkLineComponent,
} from 'echarts/components';
-import { LabelLayout } from 'echarts/features';
+import { LabelLayout, LegacyGridContainLabel } from 'echarts/features';
import {
EchartsHandler,
EchartsProps,
@@ -72,6 +72,7 @@ import {
} from '../types';
import { DEFAULT_LOCALE } from '../constants';
import { mergeEchartsThemeOverrides } from '../utils/themeOverrides';
+import { loadLocale } from './echartsLocale';
// Define this interface here to avoid creating a dependency back to superset-frontend,
// TODO: to move the type to @superset-ui/core
@@ -119,66 +120,11 @@ use([
TitleComponent,
VisualMapComponent,
LabelLayout,
+ // Superset chart options rely on `grid.containLabel`, which echarts 6
+ // ignores (clipping axis labels) unless this legacy feature is registered.
+ LegacyGridContainLabel,
]);
-// Explicit per-locale imports rather than a template-literal dynamic
-// import: a computed import makes bundlers build a "context module" over
-// echarts/i18n, and resolving that directory through the echarts package's
-// `exports` map fails intermittently in webpack incremental builds
-// ("Package path ./i18n is exported ... but no valid target file was
-// found"). A static map is also the only thing that lets bundlers
-// code-split exactly the locales listed here. Keys are Superset locales
-// uppercased (see LANGUAGES in superset/config.py); values point at the
-// echarts bundle, whose naming differs for some locales (Slovenian is
-// langSI, Brazilian Portuguese is langPT-br). Superset locales absent
-// from this map fall back to English.
-type EChartsLocaleOption = Parameters<typeof registerLocale>[1];
-
-const LOCALE_LOADERS: Record<
- string,
- () => Promise<{ default: EChartsLocaleOption }>
-> = {
- AR: () => import('echarts/i18n/langAR.js'),
- CS: () => import('echarts/i18n/langCS.js'),
- DE: () => import('echarts/i18n/langDE.js'),
- EL: () => import('echarts/i18n/langEL.js'),
- EN: () => import('echarts/i18n/langEN.js'),
- ES: () => import('echarts/i18n/langES.js'),
- FA: () => import('echarts/i18n/langFA.js'),
- FI: () => import('echarts/i18n/langFI.js'),
- FR: () => import('echarts/i18n/langFR.js'),
- HU: () => import('echarts/i18n/langHU.js'),
- IT: () => import('echarts/i18n/langIT.js'),
- JA: () => import('echarts/i18n/langJA.js'),
- KO: () => import('echarts/i18n/langKO.js'),
- LV: () => import('echarts/i18n/langLV.js'),
- NL: () => import('echarts/i18n/langNL.js'),
- PL: () => import('echarts/i18n/langPL.js'),
- RO: () => import('echarts/i18n/langRO.js'),
- PT_BR: () => import('echarts/i18n/langPT-br.js'),
- RU: () => import('echarts/i18n/langRU.js'),
- SL: () => import('echarts/i18n/langSI.js'),
- SV: () => import('echarts/i18n/langSV.js'),
- TH: () => import('echarts/i18n/langTH.js'),
- TR: () => import('echarts/i18n/langTR.js'),
- UK: () => import('echarts/i18n/langUK.js'),
- VI: () => import('echarts/i18n/langVI.js'),
- ZH: () => import('echarts/i18n/langZH.js'),
-};
-
-const loadLocale = async (locale: string) => {
- const loader = LOCALE_LOADERS[locale];
- if (!loader) {
- // Locale not supported in ECharts
- return undefined;
- }
- try {
- return (await loader()).default;
- } catch {
- return undefined;
- }
-};
-
// Report/thumbnail screenshots use standalone="true" (charts) or 3 (reports);
// live embeds use 1/2 and keep animation. See superset/utils/screenshots.py.
export function isReportScreenshotMode(): boolean {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/echartsLocale.ts b/superset-frontend/plugins/plugin-chart-echarts/src/components/echartsLocale.ts
new file mode 100644
index 00000000000..02b5d430504
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/echartsLocale.ts
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { registerLocale } from 'echarts/core';
+
+export type EChartsLocaleOption = Parameters<typeof registerLocale>[1];
+
+// Import the `lang*-obj.js` locale bundles, NOT the plain `lang*.js` ones.
+// The plain files are side-effect UMD modules: they call
+// `registerLocale` against `require('echarts/lib/echarts')` — a second,
+// separate echarts instance from the treeshaken `echarts/core` the app
+// uses — and export nothing. Under webpack's CommonJS interop that gives
+// `.default === {}`, and registering that empty object under a builtin
+// locale name ('EN'/'ZH') wipes out the builtin definitions: every
+// time-axis chart then crashes in echarts' time formatter reading
+// `time.month` from the empty locale. The `-obj` variants export the
+// locale object itself, with no side effects and no duplicate echarts.
+//
+// Explicit per-locale imports rather than a template-literal dynamic
+// import: a computed import makes bundlers build a "context module" over
+// echarts/i18n, and resolving that directory through the echarts package's
+// `exports` map fails intermittently in webpack incremental builds
+// ("Package path ./i18n is exported ... but no valid target file was
+// found"). A static map is also the only thing that lets bundlers
+// code-split exactly the locales listed here. Keys are Superset locales
+// uppercased (see LANGUAGES in superset/config.py); values point at the
+// echarts bundle, whose naming differs for some locales (Slovenian is
+// langSI, Brazilian Portuguese is langPT-br). Superset locales absent
+// from this map fall back to English.
+export const LOCALE_LOADERS: Record<
+ string,
+ () => Promise<{ default: EChartsLocaleOption }>
+> = {
+ AR: () => import('echarts/i18n/langAR-obj.js'),
+ CS: () => import('echarts/i18n/langCS-obj.js'),
+ DE: () => import('echarts/i18n/langDE-obj.js'),
+ EL: () => import('echarts/i18n/langEL-obj.js'),
+ EN: () => import('echarts/i18n/langEN-obj.js'),
+ ES: () => import('echarts/i18n/langES-obj.js'),
+ FA: () => import('echarts/i18n/langFA-obj.js'),
+ FI: () => import('echarts/i18n/langFI-obj.js'),
+ FR: () => import('echarts/i18n/langFR-obj.js'),
+ HU: () => import('echarts/i18n/langHU-obj.js'),
+ IT: () => import('echarts/i18n/langIT-obj.js'),
+ JA: () => import('echarts/i18n/langJA-obj.js'),
+ KO: () => import('echarts/i18n/langKO-obj.js'),
+ LV: () => import('echarts/i18n/langLV-obj.js'),
+ NL: () => import('echarts/i18n/langNL-obj.js'),
+ PL: () => import('echarts/i18n/langPL-obj.js'),
+ RO: () => import('echarts/i18n/langRO-obj.js'),
+ PT_BR: () => import('echarts/i18n/langPT-br-obj.js'),
+ RU: () => import('echarts/i18n/langRU-obj.js'),
+ SL: () => import('echarts/i18n/langSI-obj.js'),
+ SV: () => import('echarts/i18n/langSV-obj.js'),
+ TH: () => import('echarts/i18n/langTH-obj.js'),
+ TR: () => import('echarts/i18n/langTR-obj.js'),
+ UK: () => import('echarts/i18n/langUK-obj.js'),
+ VI: () => import('echarts/i18n/langVI-obj.js'),
+ ZH: () => import('echarts/i18n/langZH-obj.js'),
+};
+
+/**
+ * Resolve the ECharts locale object for a Superset locale, or undefined
+ * when the locale is unsupported or the bundle yields no usable content.
+ * Returning undefined makes the caller skip `registerLocale`, so echarts
+ * keeps its builtin locale definitions instead of having them overwritten
+ * by an empty object.
+ */
+export const loadLocale = async (
+ locale: string,
+): Promise<EChartsLocaleOption | undefined> => {
+ const loader = LOCALE_LOADERS[locale];
+ if (!loader) {
+ // Locale not supported in ECharts
+ return undefined;
+ }
+ try {
+ const mod = await loader();
+ // Depending on the module system, the locale object is either the
+ // module's default export or the module namespace itself.
+ const localeObj = mod.default ?? (mod as unknown as EChartsLocaleOption);
+ if (localeObj && Object.keys(localeObj).length > 0) {
+ return localeObj;
+ }
+ } catch {
+ // fall through to the builtin locale
+ }
+ return undefined;
+};
diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/components/echartsLocale.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/components/echartsLocale.test.ts
new file mode 100644
index 00000000000..eba54111733
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/test/components/echartsLocale.test.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { init, use, registerLocale } from 'echarts/core';
+import { LineChart } from 'echarts/charts';
+import { SVGRenderer } from 'echarts/renderers';
+import { GridComponent } from 'echarts/components';
+import { LOCALE_LOADERS, loadLocale } from '../../src/components/echartsLocale';
+
+type LocaleWithTime = {
+ time?: { month?: string[]; dayOfWeek?: string[] };
+};
+
+test('loadLocale returns undefined for locales echarts does not ship', async () => {
+ expect(await loadLocale('XX')).toBeUndefined();
+});
+
+test.each(Object.keys(LOCALE_LOADERS))(
+ 'locale %s loads a locale object with time names',
+ async localeKey => {
+ const localeObj = (await loadLocale(localeKey)) as LocaleWithTime;
+ // A locale object without content would clobber echarts' builtin
+ // locales when registered; missing `time` names crash every
+ // time-axis chart inside echarts' time formatter (see #42314
+ // discussion — "Cannot read properties of null (reading '0')").
+ expect(localeObj).toBeDefined();
+ expect(localeObj.time?.month).toHaveLength(12);
+ expect(localeObj.time?.dayOfWeek).toHaveLength(7);
+ },
+);
+
+test('registering the loaded EN locale keeps time-axis charts rendering', async () => {
+ use([SVGRenderer, LineChart, GridComponent]);
+ const localeObj = await loadLocale('EN');
+ expect(localeObj).toBeDefined();
+ registerLocale('EN', localeObj!);
+
+ const chart = init(null, null, {
+ renderer: 'svg',
+ ssr: true,
+ width: 400,
+ height: 300,
+ locale: 'EN',
+ });
+ chart.setOption(
+ {
+ xAxis: { type: 'time' },
+ yAxis: { type: 'value' },
+ series: [
+ {
+ type: 'line',
+ data: [
+ [new Date(1965, 0, 1).getTime(), 100],
+ [new Date(1985, 0, 1).getTime(), 200],
+ [new Date(2005, 0, 1).getTime(), 150],
+ ],
+ },
+ ],
+ },
+ { notMerge: true, lazyUpdate: false },
+ );
+ expect(chart.renderToSVGString()).toContain('<svg');
+});