From 0bb3ab1f3afc0a04c7e1af2418c6fa0a3bdd1af4 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 14 Sep 2023 15:47:42 +0200 Subject: [PATCH] feat: add locale prop to Settings (#2164) This commit adds a way to provide the current locale and give a bit more controls when rendering ordinal dates and wrapping text --- packages/charts/api/charts.api.md | 3 +- .../heatmap/scales/band_color_scale.ts | 3 +- .../state/selectors/get_color_scale.ts | 6 +- .../state/selectors/get_heatmap_table.ts | 10 +- .../chart_types/metric/renderer/dom/index.tsx | 9 +- .../metric/renderer/dom/metric.tsx | 3 + .../chart_types/metric/renderer/dom/text.tsx | 22 ++- .../state/selectors/compute_legend.ts | 8 +- .../partition_chart/state/selectors/tree.ts | 10 +- .../chart_types/timeslip/timeslip/config.ts | 4 +- .../timeslip/render/annotations/time_unit.ts | 25 +-- .../axes/timeslip/continuous_time_rasters.ts | 3 +- .../axes/timeslip/locale_translations.ts | 143 ------------------ .../axes/timeslip/multilayer_ticks.ts | 6 +- .../crosshair_utils.linear_snap.test.ts | 5 + .../crosshair_utils.ordinal_snap.test.ts | 5 + .../xy_chart/domains/x_domain.test.ts | 35 +++-- .../chart_types/xy_chart/domains/x_domain.ts | 3 +- .../xy_chart/renderer/canvas/panels/panels.ts | 52 ++++--- .../xy_chart/renderer/canvas/panels/title.ts | 2 + .../xy_chart/renderer/canvas/renderers.ts | 21 ++- .../xy_chart/renderer/canvas/xy_chart.tsx | 6 +- .../state/selectors/compute_series_domains.ts | 2 +- .../state/selectors/get_debug_state.ts | 7 +- .../xy_chart/state/selectors/visible_ticks.ts | 3 +- .../xy_chart/state/utils/utils.test.ts | 4 +- .../chart_types/xy_chart/state/utils/utils.ts | 10 +- packages/charts/src/common/predicate.ts | 6 +- .../__snapshots__/chart.test.tsx.snap | 2 +- packages/charts/src/specs/constants.ts | 1 + packages/charts/src/specs/settings.tsx | 5 + packages/charts/src/utils/domain.test.ts | 11 +- packages/charts/src/utils/domain.ts | 4 +- packages/charts/src/utils/text/wrap.ts | 6 +- storybook/stories/utils/text/1_wrap.story.tsx | 2 +- 35 files changed, 196 insertions(+), 251 deletions(-) delete mode 100644 packages/charts/src/chart_types/xy_chart/axes/timeslip/locale_translations.ts diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md index 56465c2979..15fc2b8631 100644 --- a/packages/charts/api/charts.api.md +++ b/packages/charts/api/charts.api.md @@ -2484,7 +2484,7 @@ export const Settings: (props: SFProps; +export const settingsBuildProps: BuildProps; // @public (undocumented) export type SettingsProps = ComponentProps; @@ -2508,6 +2508,7 @@ export interface SettingsSpec extends Spec, LegendSpec { debugState?: boolean; // @alpha externalPointerEvents: ExternalPointerEventsSettings; + locale: string; minBrushDelta?: number; noResults?: ComponentType | ReactChild; onAnnotationClick?: AnnotationClickListener; diff --git a/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts b/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts index 9aa21b33e9..ca7868166e 100644 --- a/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts +++ b/packages/charts/src/chart_types/heatmap/scales/band_color_scale.ts @@ -25,10 +25,11 @@ function defaultColorBandFormatter(valueFormatter?: ValueFormatter) { /** @internal */ export function getBandsColorScale( colorScale: HeatmapBandsColorScale, + locale: string, valueFormatter?: ValueFormatter, ): { scale: ColorScale; bands: Required[] } { const labelFormatter = colorScale.labelFormatter ?? defaultColorBandFormatter(valueFormatter); - const ascendingSortFn = getPredicateFn('numAsc', 'start'); + const ascendingSortFn = getPredicateFn('numAsc', locale, 'start'); const bands = colorScale.bands .reduce[]>((acc, { start, end, color, label }) => { // admit only proper bands diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts index bdcc5a1bfb..27df94c690 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_color_scale.ts @@ -9,6 +9,7 @@ import { getHeatmapSpecSelector } from './get_heatmap_spec'; import { Color } from '../../../../common/colors'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { getBandsColorScale } from '../../scales/band_color_scale'; import { ColorBand } from '../../specs/heatmap'; @@ -20,11 +21,12 @@ export type ColorScale = (value: number) => Color; * Gets color scale based on specification and values range. */ export const getColorScale = createCustomCachedSelector( - [getHeatmapSpecSelector], + [getSettingsSpecSelector, getHeatmapSpecSelector], ( + { locale }, spec, ): { scale: ColorScale; bands: Required[]; - } => getBandsColorScale(spec.colorScale, spec.valueFormatter), + } => getBandsColorScale(spec.colorScale, locale, spec.valueFormatter), ); diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts index c886271531..a23623e71d 100644 --- a/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts +++ b/packages/charts/src/chart_types/heatmap/state/selectors/get_heatmap_table.ts @@ -37,7 +37,7 @@ export interface HeatmapTable extends SmallMultiplesSeriesDomains { */ export const getHeatmapTableSelector = createCustomCachedSelector( [getHeatmapSpecSelector, getSettingsSpecSelector, getSmallMultiplesIndexOrderSelector], - (spec, { xDomain }, smallMultiples): HeatmapTable => { + (spec, { xDomain, locale }, smallMultiples): HeatmapTable => { const { data, valueAccessor, xAccessor, yAccessor, xSortPredicate, ySortPredicate, xScale, timeZone } = spec; const smVValues = new Set(); const smHValues = new Set(); @@ -101,18 +101,18 @@ export const getHeatmapTableSelector = createCustomCachedSelector( resultData.xValues = isFiniteNumber(min) && isFiniteNumber(max) ? timeRange(min, max, xScale.interval, timeZone) : []; } else if (xScale.type === ScaleType.Ordinal) { - resultData.xValues.sort(getPredicateFn(xSortPredicate)); + resultData.xValues.sort(getPredicateFn(xSortPredicate, locale)); } // sort Y values by their predicates - resultData.yValues.sort(getPredicateFn(ySortPredicate)); + resultData.yValues.sort(getPredicateFn(ySortPredicate, locale)); // sort small multiples values const horizontalPredicate = smallMultiples?.horizontal?.sort ?? Predicate.DataIndex; - const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate)); + const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate, locale)); const verticalPredicate = smallMultiples?.vertical?.sort ?? Predicate.DataIndex; - const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate)); + const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate, locale)); return { ...resultData, diff --git a/packages/charts/src/chart_types/metric/renderer/dom/index.tsx b/packages/charts/src/chart_types/metric/renderer/dom/index.tsx index aaf6690feb..ea8e197914 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/index.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/index.tsx @@ -18,7 +18,7 @@ import { Metric as MetricComponent } from './metric'; import { highContrastColor } from '../../../../common/color_calcs'; import { colorToRgba } from '../../../../common/color_library_wrappers'; import { Colors } from '../../../../common/colors'; -import { BasicListener, ElementClickListener, ElementOverListener } from '../../../../specs'; +import { BasicListener, ElementClickListener, ElementOverListener, settingsBuildProps } from '../../../../specs'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; import { @@ -47,6 +47,7 @@ interface StateProps { specs: MetricSpec[]; a11y: A11ySettings; style: MetricStyle; + locale: string; onElementClick?: ElementClickListener; onElementOut?: BasicListener; onElementOver?: ElementOverListener; @@ -78,6 +79,7 @@ class Component extends React.Component { onElementClick, onElementOut, onElementOver, + locale, } = this.props; if (!initialized || !spec || width === 0 || height === 0) { return null; @@ -139,6 +141,7 @@ class Component extends React.Component { onElementClick={onElementClick} onElementOut={onElementOut} onElementOver={onElementOver} + locale={locale} /> ); @@ -182,13 +185,14 @@ const DEFAULT_PROPS: StateProps = { }, a11y: DEFAULT_A11Y_SETTINGS, style: LIGHT_THEME.metric, + locale: settingsBuildProps.defaults.locale, }; const mapStateToProps = (state: GlobalChartState): StateProps => { if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { return DEFAULT_PROPS; } - const { onElementClick, onElementOut, onElementOver } = getSettingsSpecSelector(state); + const { onElementClick, onElementOut, onElementOver, locale } = getSettingsSpecSelector(state); return { initialized: true, chartId: state.chartId, @@ -200,6 +204,7 @@ const mapStateToProps = (state: GlobalChartState): StateProps => { onElementOver, onElementOut, style: getChartThemeSelector(state).metric, + locale, }; }; diff --git a/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx b/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx index 0368eb70ab..b745ddf306 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/metric.tsx @@ -39,6 +39,7 @@ export const Metric: React.FunctionComponent<{ datum: MetricDatum; panel: Size; style: MetricStyle; + locale: string; onElementClick?: ElementClickListener; onElementOver?: ElementOverListener; onElementOut?: BasicListener; @@ -52,6 +53,7 @@ export const Metric: React.FunctionComponent<{ datum, panel, style, + locale, onElementClick, onElementOver, onElementOut, @@ -143,6 +145,7 @@ export const Metric: React.FunctionComponent<{ style={updatedStyle} onElementClick={onElementClick ? onElementClickHandler : undefined} highContrastTextColor={highContrastTextColor} + locale={locale} /> {isMetricWTrend(datumWithInteractionColor) && } {isMetricWProgress(datumWithInteractionColor) && ( diff --git a/packages/charts/src/chart_types/metric/renderer/dom/text.tsx b/packages/charts/src/chart_types/metric/renderer/dom/text.tsx index 6078cecbe0..ff8e1de6f4 100644 --- a/packages/charts/src/chart_types/metric/renderer/dom/text.tsx +++ b/packages/charts/src/chart_types/metric/renderer/dom/text.tsx @@ -68,6 +68,7 @@ function elementVisibility( datum: MetricDatum, panel: Size, size: BreakPoint, + locale: string, ): ElementVisibility & { titleLines: string[]; subtitleLines: string[] } { const LEFT_RIGHT_PADDING = 16; const maxTitlesWidth = (size === 's' ? 1 : 0.8) * panel.width - (datum.icon ? 24 : 0) - LEFT_RIGHT_PADDING; @@ -75,7 +76,8 @@ function elementVisibility( const titleHeight = (maxLines: number, textMeasure: TextMeasure) => { return datum.title ? PADDING + - wrapText(datum.title, TITLE_FONT, TITLE_FONT_SIZE[size], maxTitlesWidth, maxLines, textMeasure).length * + wrapText(datum.title, TITLE_FONT, TITLE_FONT_SIZE[size], maxTitlesWidth, maxLines, textMeasure, locale) + .length * TITLE_FONT_SIZE[size] * LINE_HEIGHT : 0; @@ -84,8 +86,15 @@ function elementVisibility( const subtitleHeight = (maxLines: number, textMeasure: TextMeasure) => { return datum.subtitle ? PADDING + - wrapText(datum.subtitle, SUBTITLE_FONT, SUBTITLE_FONT_SIZE[size], maxTitlesWidth, maxLines, textMeasure) - .length * + wrapText( + datum.subtitle, + SUBTITLE_FONT, + SUBTITLE_FONT_SIZE[size], + maxTitlesWidth, + maxLines, + textMeasure, + locale, + ).length * SUBTITLE_FONT_SIZE[size] * LINE_HEIGHT : 0; @@ -126,6 +135,7 @@ function elementVisibility( maxTitlesWidth, visibilityBreakpoint.titleMaxLines, textMeasure, + locale, ), subtitleLines: wrapText( datum.subtitle ?? '', @@ -134,6 +144,7 @@ function elementVisibility( maxTitlesWidth, visibilityBreakpoint.subtitleMaxLines, textMeasure, + locale, ), }; }); @@ -158,7 +169,8 @@ export const MetricText: React.FunctionComponent<{ style: MetricStyle; onElementClick?: () => void; highContrastTextColor: Color; -}> = ({ id, datum, panel, style, onElementClick, highContrastTextColor }) => { + locale: string; +}> = ({ id, datum, panel, style, onElementClick, highContrastTextColor, locale }) => { const { extra, value } = datum; const size = findRange(WIDTH_BP, panel.width); @@ -170,7 +182,7 @@ export const MetricText: React.FunctionComponent<{ 'echMetricText--horizontal': progressBarDirection === LayoutDirection.Horizontal, }); - const visibility = elementVisibility(datum, panel, size); + const visibility = elementVisibility(datum, panel, size, locale); const titleWidthMaxSize = size === 's' ? '100%' : '80%'; const titlesWidth = `min(${titleWidthMaxSize}, calc(${titleWidthMaxSize} - ${datum.icon ? '24px' : '0px'}))`; diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts index 1d1ae749fe..68bb94b0a6 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts @@ -31,8 +31,8 @@ import { import { isLinear } from '../../layout/viewmodel/viewmodel'; import { Layer, PartitionSpec } from '../../specs'; -function compareLegendItemNames(aItem: LegendNode, bItem: LegendNode): number { - return aItem.item.label.localeCompare(bItem.item.label); +function compareLegendItemNames(aItem: LegendNode, bItem: LegendNode, locale: string): number { + return aItem.item.label.localeCompare(bItem.item.label, locale); } function compareDescendingLegendItemValues(aItem: LegendNode, bItem: LegendNode): number { @@ -46,7 +46,7 @@ export const computeLegendSelector = createCustomCachedSelector( if (!spec) return []; const sortingFn = flatLegend && settings.legendSort; - + const { locale } = settings; return trees.flatMap((tree) => { const customSortingFn = sortingFn ? (aItem: LegendNode, bItem: LegendNode) => @@ -80,7 +80,7 @@ export const computeLegendSelector = createCustomCachedSelector( (spec.layout === PartitionLayout.waffle // waffle has inherent top to bottom descending order ? compareDescendingLegendItemValues : isLinear(spec.layout) // icicle/flame are sorted by name - ? compareLegendItemNames + ? (a, b) => compareLegendItemNames(a, b, locale) : () => 0), // all others are sorted by hierarchy ) .map(({ item }) => item); diff --git a/packages/charts/src/chart_types/partition_chart/state/selectors/tree.ts b/packages/charts/src/chart_types/partition_chart/state/selectors/tree.ts index 33e26fadaf..6488cad677 100644 --- a/packages/charts/src/chart_types/partition_chart/state/selectors/tree.ts +++ b/packages/charts/src/chart_types/partition_chart/state/selectors/tree.ts @@ -18,6 +18,7 @@ import { SpecType, } from '../../../../specs'; import { createCustomCachedSelector } from '../../../../state/create_selector'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec'; import { getSmallMultiplesSpecs } from '../../../../state/selectors/get_small_multiples_spec'; import { getSpecs } from '../../../../state/selectors/get_specs'; import { getSpecsFromStore } from '../../../../state/utils'; @@ -42,6 +43,7 @@ function getTreesForSpec( spec: PartitionSpec, smSpecs: SmallMultiplesSpec[], groupBySpecs: GroupBySpec[], + locale: string, ): StyledTree[] { const { layout, data, valueAccessor, layers, smallMultiples: smId } = spec; const smSpec = smSpecs.find((s) => s.id === smId); @@ -68,7 +70,7 @@ function getTreesForSpec( group.push(next); return map; }, new Map()); - return [...groups].sort(getPredicateFn(sort)).map(([groupKey, subData], innerIndex) => ({ + return [...groups].sort(getPredicateFn(sort, locale)).map(([groupKey, subData], innerIndex) => ({ name: format(groupKey), smAccessorValue: groupKey, style: smStyle, @@ -93,7 +95,7 @@ function getTreesForSpec( /** @internal */ export const getTrees = createCustomCachedSelector( - [getPartitionSpecs, getSmallMultiplesSpecs, getGroupBySpecs], - ([spec], smallMultiplesSpecs, groupBySpecs): StyledTree[] => - spec ? getTreesForSpec(spec, smallMultiplesSpecs, groupBySpecs) : [], + [getPartitionSpecs, getSmallMultiplesSpecs, getGroupBySpecs, getSettingsSpecSelector], + ([spec], smallMultiplesSpecs, groupBySpecs, { locale }): StyledTree[] => + spec ? getTreesForSpec(spec, smallMultiplesSpecs, groupBySpecs, locale) : [], ); diff --git a/packages/charts/src/chart_types/timeslip/timeslip/config.ts b/packages/charts/src/chart_types/timeslip/timeslip/config.ts index b581536665..6277d3c622 100644 --- a/packages/charts/src/chart_types/timeslip/timeslip/config.ts +++ b/packages/charts/src/chart_types/timeslip/timeslip/config.ts @@ -10,7 +10,7 @@ import { LocaleOptions } from './render/annotations/time_extent'; import { getValidatedTimeZone, getZoneFromSpecs } from '../../../utils/time_zone'; import { cachedZonedDateTimeFrom, TimeProp } from '../../xy_chart/axes/timeslip/chrono/cached_chrono'; import { RasterConfig, TimeFormatter } from '../../xy_chart/axes/timeslip/continuous_time_rasters'; -import { DEFAULT_LOCALE, MINIMUM_TICK_PIXEL_DISTANCE } from '../../xy_chart/axes/timeslip/multilayer_ticks'; +import { MINIMUM_TICK_PIXEL_DISTANCE } from '../../xy_chart/axes/timeslip/multilayer_ticks'; /** @internal */ export type AxisType = 'continuousTime' | 'continuousNumeric'; @@ -89,7 +89,7 @@ export interface TimeslipConfig extends TimeslipTheme, RasterConfig { /** @internal */ export const rasterConfig: RasterConfig = { minimumTickPixelDistance: MINIMUM_TICK_PIXEL_DISTANCE, - locale: DEFAULT_LOCALE, + locale: 'en-US', }; /** @internal */ diff --git a/packages/charts/src/chart_types/timeslip/timeslip/render/annotations/time_unit.ts b/packages/charts/src/chart_types/timeslip/timeslip/render/annotations/time_unit.ts index c5579e2f92..67c78d8fea 100644 --- a/packages/charts/src/chart_types/timeslip/timeslip/render/annotations/time_unit.ts +++ b/packages/charts/src/chart_types/timeslip/timeslip/render/annotations/time_unit.ts @@ -6,16 +6,26 @@ * Side Public License, v 1. */ -import { hasKey } from '../../../../../common/predicate'; import { BinUnit } from '../../../../xy_chart/axes/timeslip/continuous_time_rasters'; -import { LOCALE_TRANSLATIONS } from '../../../../xy_chart/axes/timeslip/locale_translations'; -import { DEFAULT_LOCALE } from '../../../../xy_chart/axes/timeslip/multilayer_ticks'; + +/** @internal */ +const LOCALE_TRANSLATIONS = { + bar: 'bar', + year: ['year', 'years'], + month: ['month', 'months'], + week: ['week', 'weeks'], + day: ['day', 'days'], + hour: ['hour', 'hours'], + minute: ['minute', 'minutes'], + second: ['second', 'seconds'], + millisecond: ['millisecond', 'milliseconds'], + one: ['', ''], +}; /** @internal */ export function renderTimeUnitAnnotation( ctx: CanvasRenderingContext2D, config: { - locale: string; monospacedFontShorthand: string; subduedFontColor: string; defaultFontColor: string; @@ -27,9 +37,6 @@ export function renderTimeUnitAnnotation( yOffset: number, unitBarMaxWidthPixels: number, ) { - const locale: keyof typeof LOCALE_TRANSLATIONS = hasKey(LOCALE_TRANSLATIONS, config.locale) - ? config.locale - : DEFAULT_LOCALE; const unitBarY = yOffset - chartTopFontSize * 1.7; ctx.save(); @@ -38,9 +45,7 @@ export function renderTimeUnitAnnotation( ctx.font = config.monospacedFontShorthand; ctx.fillStyle = config.a11y.contrast === 'low' ? config.subduedFontColor : config.defaultFontColor; ctx.fillText( - `1 ${LOCALE_TRANSLATIONS[locale].bar} = ${binUnitCount} ${ - LOCALE_TRANSLATIONS[locale][binUnit][binUnitCount === 1 ? 0 : 1] - }`, + `1 ${LOCALE_TRANSLATIONS.bar} = ${binUnitCount} ${LOCALE_TRANSLATIONS[binUnit][binUnitCount === 1 ? 0 : 1]}`, 0, yOffset, ); diff --git a/packages/charts/src/chart_types/xy_chart/axes/timeslip/continuous_time_rasters.ts b/packages/charts/src/chart_types/xy_chart/axes/timeslip/continuous_time_rasters.ts index 802704dc84..5196b67256 100644 --- a/packages/charts/src/chart_types/xy_chart/axes/timeslip/continuous_time_rasters.ts +++ b/packages/charts/src/chart_types/xy_chart/axes/timeslip/continuous_time_rasters.ts @@ -11,7 +11,6 @@ import { cachedTimeDelta, cachedZonedDateTimeFrom, TimeProp } from './chrono/cached_chrono'; import { epochDaysInMonth, epochInSecondsToYear } from './chrono/chrono'; -import { LOCALE_TRANSLATIONS } from './locale_translations'; /** @public */ export type BinUnit = 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond' | 'one'; @@ -81,7 +80,7 @@ export interface AxisLayer { /** @internal */ export interface RasterConfig { minimumTickPixelDistance: number; - locale: keyof typeof LOCALE_TRANSLATIONS; + locale: string; } const millisecondIntervals = (rasterMs: number): IntervalIterableMaker => diff --git a/packages/charts/src/chart_types/xy_chart/axes/timeslip/locale_translations.ts b/packages/charts/src/chart_types/xy_chart/axes/timeslip/locale_translations.ts deleted file mode 100644 index 85a0c6a761..0000000000 --- a/packages/charts/src/chart_types/xy_chart/axes/timeslip/locale_translations.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** @internal */ -export const LOCALE_TRANSLATIONS = { - 'ar-TN': { - bar: 'حاجز', - year: ['سنة', 'سنوات'], - month: ['شهر', 'اشهر'], - week: ['أسبوع', 'أسابيع'], - day: ['يوم', 'أيام'], - hour: ['ساعة', 'ساعات'], - minute: ['دقيقة', 'دقائق'], - second: ['ثانية', 'ثواني'], - millisecond: ['مللي ثانية', 'مللي ثانية'], - one: ['', ''], - }, - 'de-CH': { - bar: 'Balken', - year: ['Jahr', 'Jahre'], - month: ['Monat', 'Monate'], - week: ['Woche', 'Wochen'], - day: ['Tag', 'Tage'], - hour: ['Stunde', 'Stunden'], - minute: ['Minute', 'Minuten'], - second: ['Sekunde', 'Sekunden'], - millisecond: ['Millisekunde', 'Millisekunden'], - one: ['', ''], - }, - 'fr-FR': { - bar: 'barre', - year: ['année', 'ans'], - month: ['mois', 'mois'], - week: ['semaine', 'semaines'], - day: ['jour', 'jours'], - hour: ['heure', 'heures'], - minute: ['minute', 'minutes'], - second: ['seconde', 'secondes'], - millisecond: ['milliseconde', 'millisecondes'], - one: ['', ''], - }, - 'en-US': { - bar: 'bar', - year: ['year', 'years'], - month: ['month', 'months'], - week: ['week', 'weeks'], - day: ['day', 'days'], - hour: ['hour', 'hours'], - minute: ['minute', 'minutes'], - second: ['second', 'seconds'], - millisecond: ['millisecond', 'milliseconds'], - one: ['', ''], - }, - 'el-GR': { - bar: 'γραμμή', - year: ['χρόνος', 'μήνες'], - month: ['μήνα', 'months'], - week: ['εβδομάδα', 'εβδομάδες'], - day: ['μέρα', 'ημέρες'], - hour: ['ώρα', 'ώρες'], - minute: ['λεπτό', 'λεπτά'], - second: ['δευτερόλεπτο', 'δευτερόλεπτα'], - millisecond: ['χιλιοστό του δευτερολέπτου', 'χιλιοστά του δευτερολέπτου'], - one: ['', ''], - }, - 'hu-HU': { - bar: 'oszlop', - year: ['év', 'év'], - month: ['hónap', 'hónap'], - week: ['hét', 'hét'], - day: ['nap', 'nap'], - hour: ['óra', 'óra'], - minute: ['perc', 'perc'], - second: ['másodperc', 'másodperc'], - millisecond: ['ezredmásodperc', 'ezredmásodperc'], - one: ['', ''], - }, - 'he-IL': { - bar: 'עַמוּדָה', - year: ['שנה', 'years'], - month: ['חודש', 'חודשים'], - week: ['שבוע', 'שבועות'], - day: ['יום', 'ימים'], - hour: ['שעה', 'שעות'], - minute: ['דקות', 'דקות'], - second: ['השני', 'שניות'], - millisecond: ['אלפית השנייה', 'אלפיות השנייה'], - one: ['', ''], - }, - 'hi-IN': { - bar: 'बार', - year: ['वर्ष', 'साल'], - month: ['महीना', 'महीने'], - week: ['सप्ताह', 'सप्ताह'], - day: ['दिन', 'दिन'], - hour: ['घंटा', 'घंटे'], - minute: ['मिनट', 'मिनट'], - second: ['सेकंड', 'सेकंड'], - millisecond: ['मिलीसेकंड', 'मिलीसेकेंड'], - one: ['', ''], - }, - 'it-IT': { - bar: 'barra', - year: ['anno', 'anni'], - month: ['mese', 'mesi'], - week: ['settimana', 'settimane'], - day: ['giorno', 'giorni'], - hour: ['ora', 'ore'], - minute: ['minuto', 'minuti'], - second: ['secondo', 'secondi'], - millisecond: ['millisecondo', 'millisecondi'], - one: ['', ''], - }, - 'ja-JA': { - bar: '棒', - year: ['年', '年間'], - month: ['ヶ月', 'ヵ月'], - week: ['週間', '週間'], - day: ['日', '日間'], - hour: ['時間', '時間'], - minute: ['分', '分間'], - second: ['秒', '秒間'], - millisecond: ['ミリ秒', 'ミリ秒'], - one: ['', ''], - }, - 'ru-RU': { - bar: 'полоса', - year: ['год', 'лет'], - month: ['месяц', 'месяцев'], - week: ['неделя', 'недель'], - day: ['день', 'дней'], - hour: ['час', 'часов'], - minute: ['минута', 'минут'], - second: ['секунда', 'секунд'], - millisecond: ['миллисекунда', 'миллисекунд'], - one: ['', ''], - }, -}; diff --git a/packages/charts/src/chart_types/xy_chart/axes/timeslip/multilayer_ticks.ts b/packages/charts/src/chart_types/xy_chart/axes/timeslip/multilayer_ticks.ts index 7875b6a37b..8192c44cc5 100644 --- a/packages/charts/src/chart_types/xy_chart/axes/timeslip/multilayer_ticks.ts +++ b/packages/charts/src/chart_types/xy_chart/axes/timeslip/multilayer_ticks.ts @@ -20,9 +20,6 @@ export const MAX_TIME_TICK_COUNT = 50; // this doesn't do much for narrow charts /** @internal */ export const MAX_TIME_GRID_COUNT = 12; -/** @internal */ -export const DEFAULT_LOCALE = 'en-US'; - /** @internal */ export const MINIMUM_TICK_PIXEL_DISTANCE = 24; @@ -50,9 +47,10 @@ export function multilayerAxisEntry( timeAxisLayerCount: number, scale: ScaleContinuous, getMeasuredTicks: GetMeasuredTicks, + locale: string, ): Projection { const rasterSelector = continuousTimeRasters( - { minimumTickPixelDistance: MINIMUM_TICK_PIXEL_DISTANCE, locale: DEFAULT_LOCALE }, + { minimumTickPixelDistance: MINIMUM_TICK_PIXEL_DISTANCE, locale }, xDomain.timeZone, ); const domainValues = xDomain.domain; // todo consider a property or object type rename diff --git a/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts b/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts index aa9333263c..c6361ec4fe 100644 --- a/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts +++ b/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.linear_snap.test.ts @@ -95,6 +95,7 @@ describe('Crosshair utils linear scale', () => { barSeries, getScaleConfigsFromSpecs([], barSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const multiBarSeries = [barSeries1, barSeries2]; @@ -102,6 +103,7 @@ describe('Crosshair utils linear scale', () => { multiBarSeries, getScaleConfigsFromSpecs([], multiBarSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const lineSeries = [lineSeries1]; @@ -109,6 +111,7 @@ describe('Crosshair utils linear scale', () => { lineSeries, getScaleConfigsFromSpecs([], lineSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const multiLineSeries = [lineSeries1, lineSeries2]; @@ -116,6 +119,7 @@ describe('Crosshair utils linear scale', () => { multiLineSeries, getScaleConfigsFromSpecs([], multiLineSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const mixedLinesBars = [lineSeries1, lineSeries2, barSeries1, barSeries2]; @@ -123,6 +127,7 @@ describe('Crosshair utils linear scale', () => { mixedLinesBars, getScaleConfigsFromSpecs([], mixedLinesBars, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const barSeriesScale = computeXScale({ diff --git a/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts b/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts index c16213797a..2c6df00a37 100644 --- a/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts +++ b/packages/charts/src/chart_types/xy_chart/crosshair/crosshair_utils.ordinal_snap.test.ts @@ -93,6 +93,7 @@ describe('Crosshair utils ordinal scales', () => { barSeries, getScaleConfigsFromSpecs([], barSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const multiBarSeries = [barSeries1, barSeries2]; @@ -100,6 +101,7 @@ describe('Crosshair utils ordinal scales', () => { multiBarSeries, getScaleConfigsFromSpecs([], multiBarSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const lineSeries = [lineSeries1]; @@ -107,6 +109,7 @@ describe('Crosshair utils ordinal scales', () => { lineSeries, getScaleConfigsFromSpecs([], lineSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const multiLineSeries = [lineSeries1, lineSeries2]; @@ -114,6 +117,7 @@ describe('Crosshair utils ordinal scales', () => { multiLineSeries, getScaleConfigsFromSpecs([], multiLineSeries, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const mixedLinesBars = [lineSeries1, lineSeries2, barSeries1, barSeries2]; @@ -121,6 +125,7 @@ describe('Crosshair utils ordinal scales', () => { mixedLinesBars, getScaleConfigsFromSpecs([], mixedLinesBars, MockGlobalSpec.settings()), [], + { locale: 'en-US' }, ); const barSeriesScale = computeXScale({ diff --git a/packages/charts/src/chart_types/xy_chart/domains/x_domain.test.ts b/packages/charts/src/chart_types/xy_chart/domains/x_domain.test.ts index ef85977b68..bfef1faaee 100644 --- a/packages/charts/src/chart_types/xy_chart/domains/x_domain.test.ts +++ b/packages/charts/src/chart_types/xy_chart/domains/x_domain.test.ts @@ -31,6 +31,8 @@ jest.spyOn(Intl, 'DateTimeFormat'); })), }); +const DEFAULT_LOCALE = 'en'; + describe('X Domain', () => { test('Should return a default scale when missing specs or specs types', () => { const seriesSpecs: BasicSeriesSpec[] = []; @@ -255,7 +257,7 @@ describe('X Domain', () => { const specDataSeries: BasicSeriesSpec[] = [ds1, ds2]; const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge bar series correctly', () => { @@ -294,7 +296,7 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge multi bar series correctly', () => { @@ -333,7 +335,7 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge multi bar series correctly - 2', () => { @@ -374,7 +376,7 @@ describe('X Domain', () => { const { xValues } = getDataSeriesFromSpecs(specDataSeries); const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 7]); }); test('Should merge multi bar linear/bar ordinal series correctly', () => { @@ -413,7 +415,7 @@ describe('X Domain', () => { const specDataSeries = [ds1, ds2]; const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); const { xValues } = getDataSeriesFromSpecs(specDataSeries); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); @@ -463,7 +465,7 @@ describe('X Domain', () => { MockGlobalSpec.settings({ xDomain: customDomain }), ); - const getResult = () => mergeXDomain(scalesConfig.x, xValues, ScaleType.Ordinal); + const getResult = () => mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE, ScaleType.Ordinal); expect(getResult).not.toThrow(); @@ -510,7 +512,7 @@ describe('X Domain', () => { const { xValues } = getDataSeriesFromSpecs(specDataSeries); const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); test('Should merge multi bar/line time series correctly', () => { @@ -551,7 +553,7 @@ describe('X Domain', () => { const { xValues } = getDataSeriesFromSpecs(specDataSeries); const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); test('Should merge multi lines series correctly', () => { @@ -592,7 +594,7 @@ describe('X Domain', () => { const { xValues } = getDataSeriesFromSpecs(specDataSeries); const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain).toEqual([0, 1, 2, 5, 7]); }); @@ -627,7 +629,7 @@ describe('X Domain', () => { const { xValues } = getDataSeriesFromSpecs(specDataSeries); const scalesConfig = getScaleConfigsFromSpecs([], specDataSeries, MockGlobalSpec.settings()); - const mergedDomain = mergeXDomain(scalesConfig.x, xValues); + const mergedDomain = mergeXDomain(scalesConfig.x, xValues, DEFAULT_LOCALE); expect(mergedDomain.domain.length).toEqual(maxValues); }); test('should compute minInterval an ordered list of numbers', () => { @@ -666,6 +668,7 @@ describe('X Domain', () => { const basicMergedDomain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(basicMergedDomain.domain).toEqual([0, 3]); @@ -673,6 +676,7 @@ describe('X Domain', () => { let { domain } = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: arrayXDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(domain).toEqual([1, 5]); const warnMessage = 'xDomain for continuous scale should be a DomainRange object, not an array'; @@ -684,6 +688,7 @@ describe('X Domain', () => { domain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, xValues, + DEFAULT_LOCALE, ).domain; expect(domain).toEqual([1, 5]); expect(Logger.warn).toHaveBeenCalledWith( @@ -699,6 +704,7 @@ describe('X Domain', () => { const mergedDomain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(mergedDomain.domain).toEqual([0, 5]); @@ -706,6 +712,7 @@ describe('X Domain', () => { const { domain } = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(domain).toEqual([1, 5]); expect(Logger.warn).toHaveBeenCalledWith( @@ -721,6 +728,7 @@ describe('X Domain', () => { const mergedDomain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(mergedDomain.domain).toEqual([1, 3]); @@ -728,6 +736,7 @@ describe('X Domain', () => { const { domain } = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(domain).toEqual([1, 5]); expect(Logger.warn).toHaveBeenCalledWith( @@ -742,6 +751,7 @@ describe('X Domain', () => { const basicMergedDomain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(basicMergedDomain.domain).toEqual(['a', 'b', 'c']); @@ -749,6 +759,7 @@ describe('X Domain', () => { const { domain } = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: objectXDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(domain).toEqual(['a', 'b', 'c', 'd']); const warnMessage = @@ -765,6 +776,7 @@ describe('X Domain', () => { const mergedDomain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(mergedDomain.minInterval).toEqual(0.5); }); @@ -774,6 +786,7 @@ describe('X Domain', () => { const mergedDomain = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain })).x, new Set([5]), + DEFAULT_LOCALE, ); expect(mergedDomain.minInterval).toEqual(10); }); @@ -783,6 +796,7 @@ describe('X Domain', () => { const { minInterval } = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(minInterval).toEqual(1); const expectedWarning = @@ -795,6 +809,7 @@ describe('X Domain', () => { const { minInterval } = mergeXDomain( getScaleConfigsFromSpecs([], specs, MockGlobalSpec.settings({ xDomain: invalidXDomain })).x, xValues, + DEFAULT_LOCALE, ); expect(minInterval).toEqual(1); const expectedWarning = diff --git a/packages/charts/src/chart_types/xy_chart/domains/x_domain.ts b/packages/charts/src/chart_types/xy_chart/domains/x_domain.ts index d39afde736..4408665bf7 100644 --- a/packages/charts/src/chart_types/xy_chart/domains/x_domain.ts +++ b/packages/charts/src/chart_types/xy_chart/domains/x_domain.ts @@ -25,6 +25,7 @@ import { BasicSeriesSpec, SeriesType, XScaleType } from '../utils/specs'; export function mergeXDomain( { type, nice, isBandScale, timeZone, desiredTickCount, customDomain }: ScaleConfigs['x'], xValues: Set, + locale: string, fallbackScale?: XScaleType, ): XDomain { let seriesXComputedDomains; @@ -35,7 +36,7 @@ export function mergeXDomain( Logger.warn(`Each X value in a ${type} x scale needs be be a number. Using ordinal x scale as fallback.`); } - seriesXComputedDomains = computeOrdinalDataDomain([...xValues], false, true); + seriesXComputedDomains = computeOrdinalDataDomain([...xValues], false, true, locale); if (customDomain) { if (Array.isArray(customDomain)) { seriesXComputedDomains = [...customDomain]; diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts index 9ddb8034c1..5e694dd636 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/panels.ts @@ -33,7 +33,7 @@ export function renderGridPanels(ctx: CanvasRenderingContext2D, { x: chartX, y: ); } -function renderPanel(ctx: CanvasRenderingContext2D, props: AxisProps) { +function renderPanel(ctx: CanvasRenderingContext2D, props: AxisProps, locale: string) { const { size, anchorPoint, debug, axisStyle, axisSpec, panelAnchor, secondary } = props; const { position } = axisSpec; const x = anchorPoint.x + (position === Position.Right ? -1 : 1) * panelAnchor.x; @@ -42,16 +42,21 @@ function renderPanel(ctx: CanvasRenderingContext2D, props: AxisProps) { withContext(ctx, () => { ctx.translate(x, y); if (debug && !secondary) renderDebugRect(ctx, { x: 0, y: 0, ...size }); - renderAxis(ctx, props); // For now, just render the axis line TODO: compute axis dimensions per panel + renderAxis(ctx, props); // TODO: compute axis dimensions per panel, For now, just render the axis line if (!secondary) { const { panelTitle, dimension } = props; - renderTitle(ctx, true, { panelTitle, axisSpec, axisStyle, size, dimension, debug, anchorPoint: { x: 0, y: 0 } }); // fixme axisSpec/Style? + renderTitle( + ctx, + true, + { panelTitle, axisSpec, axisStyle, size, dimension, debug, anchorPoint: { x: 0, y: 0 } }, + locale, + ); // TODO: should we use the axisSpec/Style for the title of small multiple or use their own style? } }); } /** @internal */ -export function renderPanelSubstrates(ctx: CanvasRenderingContext2D, props: AxesProps) { +export function renderPanelSubstrates(ctx: CanvasRenderingContext2D, props: AxesProps, locale: string) { const { axesSpecs, perPanelAxisGeoms, axesStyles, sharedAxesStyle, debug, renderingArea } = props; const seenAxesTitleIds = new Set(); @@ -75,25 +80,34 @@ export function renderPanelSubstrates(ctx: CanvasRenderingContext2D, props: Axes if (!seenAxesTitleIds.has(id)) { seenAxesTitleIds.add(id); - renderTitle(ctx, false, { size: parentSize, debug, panelTitle, anchorPoint, dimension, axisStyle, axisSpec }); + renderTitle( + ctx, + false, + { size: parentSize, debug, panelTitle, anchorPoint, dimension, axisStyle, axisSpec }, + locale, + ); } const layerGirth = dimension.maxLabelBboxHeight; - renderPanel(ctx, { - panelTitle, - secondary, - panelAnchor, - axisSpec, - anchorPoint, - size, - dimension, - ticks, - axisStyle, - debug, - renderingArea, - layerGirth, - }); + renderPanel( + ctx, + { + panelTitle, + secondary, + panelAnchor, + axisSpec, + anchorPoint, + size, + dimension, + ticks, + axisStyle, + debug, + renderingArea, + layerGirth, + }, + locale, + ); }); }); } diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/title.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/title.ts index 85df7f85cd..0fc7938eeb 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/title.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/panels/title.ts @@ -41,6 +41,7 @@ export function renderTitle( debug, anchorPoint, }: TitleProps, + locale: string, ) { const titleToRender = panel ? panelTitle : title; const axisTitleToUse = panel ? axisPanelTitle : axisTitle; @@ -74,6 +75,7 @@ export function renderTitle( horizontal ? width : height, 1, measureText(ctx), + locale, ); if (!wrappedText[0]) return; if (debug) renderDebugRect(ctx, { x, y, width: horizontal ? width : height, height: font.fontSize }, rotation); diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts index 0844752484..86634335a5 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -51,6 +51,7 @@ export function renderXYChartCanvas2d( debug, panelGeoms, hoveredAnnotationIds, + locale, } = props; const transform = { x: renderingArea.left + chartTransform.x, y: renderingArea.top + chartTransform.y }; @@ -71,14 +72,18 @@ export function renderXYChartCanvas2d( }), () => - renderPanelSubstrates(ctx, { - axesSpecs, - perPanelAxisGeoms, - renderingArea, - debug, - axesStyles, - sharedAxesStyle, - }), + renderPanelSubstrates( + ctx, + { + axesSpecs, + perPanelAxisGeoms, + renderingArea, + debug, + axesStyles, + sharedAxesStyle, + }, + locale, + ), // rendering background annotations () => diff --git a/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index ef5fe3c92e..52452fd041 100644 --- a/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/packages/charts/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -15,6 +15,7 @@ import { renderXYChartCanvas2d } from './renderers'; import { hasMostlyRTL } from './utils/has_mostly_rtl'; import { LegendItem } from '../../../../common/legend'; import { ScreenReaderSummary } from '../../../../components/accessibility'; +import { settingsBuildProps } from '../../../../specs'; import { onChartRendered } from '../../../../state/actions/chart'; import { GlobalChartState } from '../../../../state/chart_state'; import { computePanelsSelectors, PanelGeoms } from '../../../../state/selectors/compute_panels'; @@ -77,6 +78,7 @@ export interface ReactiveChartStateProps { annotationSpecs: AnnotationSpec[]; panelGeoms: PanelGeoms; a11ySettings: A11ySettings; + locale: string; } interface ReactiveChartDispatchProps { @@ -237,6 +239,7 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { annotationSpecs: [], panelGeoms: [], a11ySettings: DEFAULT_A11Y_SETTINGS, + locale: settingsBuildProps.defaults.locale, }; const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { @@ -245,10 +248,11 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { } const { geometries, geometriesIndex } = computeSeriesGeometriesSelector(state); - const { debug } = getSettingsSpecSelector(state); + const { debug, locale } = getSettingsSpecSelector(state); const perPanelAxisGeoms = computePerPanelAxesGeomsSelector(state); return { + locale, isRTL: hasMostlyRTL(perPanelAxisGeoms), initialized: true, isChartEmpty: isChartEmptySelector(state), diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts index 3539503fef..da3a93683c 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts @@ -22,8 +22,8 @@ export const computeSeriesDomainsSelector = createCustomCachedSelector( getSeriesSpecsSelector, getScaleConfigsFromSpecsSelector, getAnnotationSpecsSelector, - getDeselectedSeriesSelector, getSettingsSpecSelector, + getDeselectedSeriesSelector, getSmallMultiplesIndexOrderSelector, ], computeSeriesDomains, diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts index e2588e8bea..1cb76a7d6d 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/get_debug_state.ts @@ -49,11 +49,11 @@ export const getDebugStateSelector = createCustomCachedSelector( getSettingsSpecSelector, getAnnotationSpecsSelector, ], - ({ geometries }, legend, axes, gridLines, axesSpecs, { rotation }, annotations): DebugState => { + ({ geometries }, legend, axes, gridLines, axesSpecs, { rotation, locale }, annotations): DebugState => { const seriesNameMap = getSeriesNameMap(legend); return { legend: getLegendState(legend), - axes: getAxes(axes, axesSpecs, gridLines, rotation), + axes: getAxes(axes, axesSpecs, gridLines, rotation, locale), areas: geometries.areas.map(getAreaState(seriesNameMap)), lines: geometries.lines.map(getLineState(seriesNameMap)), bars: getBarsState(seriesNameMap, geometries.bars), @@ -67,6 +67,7 @@ function getAxes( axesSpecs: AxisSpec[], gridLines: LinesGrid[], rotation: Rotation, + locale: string, ): DebugStateAxes { return axesSpecs.reduce( (acc, { position, title, id }) => { @@ -89,7 +90,7 @@ function getAxes( : Predicate.NumAsc; const visibleTicks = geom.visibleTicks .filter(({ label }) => label !== '') - .sort(getPredicateFn(sortingOrder, 'position')); + .sort(getPredicateFn(sortingOrder, locale, 'position')); const labels = visibleTicks.map(({ label }) => label); const values = visibleTicks.map(({ value }) => value); diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts index 33b89b1607..09b471a7ed 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/visible_ticks.ts @@ -220,7 +220,7 @@ export const getVisibleTickSetsSelector = createCustomCachedSelector( ); function getVisibleTickSets( - { rotation: chartRotation }: Pick, + { rotation: chartRotation, locale }: Pick, joinedAxesData: Map, { xDomain, yDomains }: Pick, smScales: SmallMultipleScales, @@ -338,6 +338,7 @@ function getVisibleTickSets( timeAxisLayerCount, scale, getMeasuredTicks, + locale, ), ); } diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts index fa3c1104b9..4614ef7208 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.test.ts @@ -69,7 +69,7 @@ describe('Chart State utils', () => { data: BARCHART_1Y0G, }); const scaleConfig = getScaleConfigsFromSpecs([], [spec1, spec2], MockGlobalSpec.settings()); - const domains = computeSeriesDomains([spec1, spec2], scaleConfig, []); + const domains = computeSeriesDomains([spec1, spec2], scaleConfig, [], { locale: 'en-US' }); expect(domains.xDomain).toEqual( MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0, 3], @@ -121,7 +121,7 @@ describe('Chart State utils', () => { data: BARCHART_1Y1G, }); const scaleConfig = getScaleConfigsFromSpecs([], [spec1, spec2], MockGlobalSpec.settings()); - const domains = computeSeriesDomains([spec1, spec2], scaleConfig, []); + const domains = computeSeriesDomains([spec1, spec2], scaleConfig, [], { locale: 'en-US' }); expect(domains.xDomain).toEqual( MockXDomain.fromScaleType(ScaleType.Linear, { domain: [0, 3], diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts index d80244546d..789ab24fb8 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts @@ -119,11 +119,11 @@ export function computeSeriesDomains( seriesSpecs: BasicSeriesSpec[], scaleConfigs: ScaleConfigs, annotations: AnnotationSpec[], + settingsSpec: Pick, deselectedDataSeries: SeriesIdentifier[] = [], - settingsSpec?: Pick, smallMultiples?: SmallMultiplesGroupBy, ): SeriesDomainsAndData { - const orderOrdinalBinsBy = settingsSpec?.orderOrdinalBinsBy; + const { orderOrdinalBinsBy, locale } = settingsSpec; const { dataSeries, xValues, fallbackScale, smHValues, smVValues } = getDataSeriesFromSpecs( seriesSpecs, deselectedDataSeries, @@ -131,7 +131,7 @@ export function computeSeriesDomains( smallMultiples, ); // compute the x domain merging any custom domain - const xDomain = mergeXDomain(scaleConfigs.x, xValues, fallbackScale); + const xDomain = mergeXDomain(scaleConfigs.x, xValues, locale, fallbackScale); // fill series with missing x values const filledDataSeries = fillSeries(dataSeries, xValues, xDomain.type); @@ -150,10 +150,10 @@ export function computeSeriesDomains( // sort small multiples values const horizontalPredicate = smallMultiples?.horizontal?.sort ?? Predicate.DataIndex; - const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate)); + const smHDomain = [...smHValues].sort(getPredicateFn(horizontalPredicate, locale)); const verticalPredicate = smallMultiples?.vertical?.sort ?? Predicate.DataIndex; - const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate)); + const smVDomain = [...smVValues].sort(getPredicateFn(verticalPredicate, locale)); return { xDomain, diff --git a/packages/charts/src/common/predicate.ts b/packages/charts/src/common/predicate.ts index 479c7499f2..e710b5d0a5 100644 --- a/packages/charts/src/common/predicate.ts +++ b/packages/charts/src/common/predicate.ts @@ -21,19 +21,19 @@ export const Predicate = Object.freeze({ export type Predicate = $Values; /** @internal */ -export function getPredicateFn(predicate: Predicate, accessor?: keyof T): (a: T, b: T) => number { +export function getPredicateFn(predicate: Predicate, locale: string, accessor?: keyof T): (a: T, b: T) => number { switch (predicate) { case 'alphaAsc': return (a: T, b: T) => { const aValue = String(accessor ? a[accessor] : a); const bValue = String(accessor ? b[accessor] : b); - return aValue.localeCompare(bValue); + return aValue.localeCompare(bValue, locale); }; case 'alphaDesc': return (a: T, b: T) => { const aValue = String(accessor ? a[accessor] : a); const bValue = String(accessor ? b[accessor] : b); - return bValue.localeCompare(aValue); + return bValue.localeCompare(aValue, locale); }; case 'numDesc': return (a: T, b: T) => { diff --git a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap index cbab7b746b..45fe4b4646 100644 --- a/packages/charts/src/components/__snapshots__/chart.test.tsx.snap +++ b/packages/charts/src/components/__snapshots__/chart.test.tsx.snap @@ -69,7 +69,7 @@ exports[`Chart should render the legend name test 1`] = ` - +
diff --git a/packages/charts/src/specs/constants.ts b/packages/charts/src/specs/constants.ts index e907d58c81..2215c5cb6e 100644 --- a/packages/charts/src/specs/constants.ts +++ b/packages/charts/src/specs/constants.ts @@ -167,6 +167,7 @@ export const settingsBuildProps = buildSFProps()( allowBrushingLastHistogramBin: true, pointBuffer: 10, ...DEFAULT_LEGEND_CONFIG, + locale: 'en-US', }, ); diff --git a/packages/charts/src/specs/settings.tsx b/packages/charts/src/specs/settings.tsx index 2c14148ab3..ca7f173af7 100644 --- a/packages/charts/src/specs/settings.tsx +++ b/packages/charts/src/specs/settings.tsx @@ -617,6 +617,11 @@ export interface SettingsSpec extends Spec, LegendSpec { * User can provide a table description of the data */ ariaTableCaption?: string; + + /** + * Unicode Locale Identifier, default `en` + */ + locale: string; } /** diff --git a/packages/charts/src/utils/domain.test.ts b/packages/charts/src/utils/domain.test.ts index ed5b8b5c92..5e9ba736de 100644 --- a/packages/charts/src/utils/domain.test.ts +++ b/packages/charts/src/utils/domain.test.ts @@ -11,6 +11,7 @@ import { computeContinuousDataDomain, computeDomainExtent, computeOrdinalDataDom import { ScaleType } from '../scales/constants'; import { DomainPaddingUnit } from '../specs'; +const DEFAULT_LOCALE = 'en'; describe('utils/domain', () => { test('should return [] domain if no data', () => { const data: any[] = []; @@ -18,7 +19,7 @@ describe('utils/domain', () => { const isSorted = true; const removeNull = true; - const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull); + const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull, DEFAULT_LOCALE); expect(ordinalDataDomain).toEqual([]); }); @@ -29,7 +30,7 @@ describe('utils/domain', () => { const isSorted = true; const removeNull = true; - const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull); + const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull, DEFAULT_LOCALE); const expectedOrdinalDomain = ['a', 'b', 'd']; @@ -42,7 +43,7 @@ describe('utils/domain', () => { const isSorted = false; const removeNull = true; - const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull); + const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull, DEFAULT_LOCALE); const expectedOrdinalDomain = ['d', 'a', 'b']; @@ -55,7 +56,7 @@ describe('utils/domain', () => { const isSorted = true; const removeNull = false; - const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull); + const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull, DEFAULT_LOCALE); const expectedOrdinalDomain = ['a', 'b', 'd', null]; @@ -68,7 +69,7 @@ describe('utils/domain', () => { const isSorted = false; const removeNull = false; - const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull); + const ordinalDataDomain = computeOrdinalDataDomain(data.map(accessor), isSorted, removeNull, DEFAULT_LOCALE); const expectedOrdinalDomain = ['d', 'a', null, 'b']; diff --git a/packages/charts/src/utils/domain.ts b/packages/charts/src/utils/domain.ts index 31016ce845..78e1a5d8de 100644 --- a/packages/charts/src/utils/domain.ts +++ b/packages/charts/src/utils/domain.ts @@ -33,9 +33,9 @@ function constrainPadding( } /** @internal */ -export function computeOrdinalDataDomain(data: T[], sorted: boolean, removeNull: boolean): T[] { +export function computeOrdinalDataDomain(data: T[], sorted: boolean, removeNull: boolean, locale: string): T[] { const uniqueValues = [...new Set(removeNull ? data.filter((d) => d !== null) : data)]; - return sorted ? uniqueValues.sort((a, b) => `${a}`.localeCompare(`${b}`)) : uniqueValues; + return sorted ? uniqueValues.sort((a, b) => `${a}`.localeCompare(`${b}`, locale)) : uniqueValues; } function getPaddedDomain(start: number, end: number, domainOptions: YDomainRange): [number, number] { diff --git a/packages/charts/src/utils/text/wrap.ts b/packages/charts/src/utils/text/wrap.ts index f8b3087381..eb6f2c70f7 100644 --- a/packages/charts/src/utils/text/wrap.ts +++ b/packages/charts/src/utils/text/wrap.ts @@ -20,12 +20,12 @@ export function wrapText( maxLineWidth: number, maxLines: number, measure: TextMeasure, + locale: string, ): string[] { if (maxLines <= 0) { return []; } - // TODO add locale - const segmenter = textSegmenter([]); + const segmenter = textSegmenter(locale); // remove new lines and multi-spaces. const cleanedText = text.replaceAll('\n', ' ').replaceAll(/ +(?= )/g, ''); @@ -71,7 +71,7 @@ export function wrapText( return lines; } -function textSegmenter(locale: string[]): (text: string) => { segment: string; index: number; isWordLike?: boolean }[] { +function textSegmenter(locale: string): (text: string) => { segment: string; index: number; isWordLike?: boolean }[] { if ('Segmenter' in Intl) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore diff --git a/storybook/stories/utils/text/1_wrap.story.tsx b/storybook/stories/utils/text/1_wrap.story.tsx index c662578f95..1df6bc5679 100644 --- a/storybook/stories/utils/text/1_wrap.story.tsx +++ b/storybook/stories/utils/text/1_wrap.story.tsx @@ -47,7 +47,7 @@ export const Example = () => { ctx.fillStyle = 'black'; ctx.strokeRect(0, 0, maxLineWidth, fontSize * maxLines); withTextMeasure((measure) => { - const lines = wrapText(text, font, fontSize, maxLineWidth, maxLines, measure); + const lines = wrapText(text, font, fontSize, maxLineWidth, maxLines, measure, 'en'); lines.forEach((line, i) => { ctx.fillText(line, 0, i * fontSize); });