Skip to content

Commit

Permalink
feat: add locale prop to Settings (#2164)
Browse files Browse the repository at this point in the history
This commit adds a way to provide the current locale and give a bit more controls when rendering ordinal dates and wrapping text
  • Loading branch information
markov00 committed Sep 14, 2023
1 parent 1ab954d commit 0bb3ab1
Show file tree
Hide file tree
Showing 35 changed files with 196 additions and 251 deletions.
3 changes: 2 additions & 1 deletion packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2484,7 +2484,7 @@ export const Settings: (props: SFProps<SettingsSpec, keyof (typeof settingsBuild
// Warning: (ae-forgotten-export) The symbol "BuildProps" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "debug" | "rotation" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary" | "legendPosition" | "flatLegend" | "legendMaxDepth" | "legendSize" | "showLegend" | "showLegendExtra" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "resizeDebounce" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin", "ariaLabel" | "xDomain" | "ariaDescription" | "ariaDescribedBy" | "ariaLabelledBy" | "ariaTableCaption" | "theme" | "legendAction" | "legendColorPicker" | "legendStrategy" | "onLegendItemClick" | "customLegend" | "onLegendItemMinusClick" | "onLegendItemOut" | "onLegendItemOver" | "onLegendItemPlusClick" | "orderOrdinalBinsBy" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onBrushEnd" | "onPointerUpdate" | "onRenderChange" | "onProjectionAreaChange" | "onAnnotationClick" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "noResults" | "legendSort", never>;
export const settingsBuildProps: BuildProps<SettingsSpec, "id" | "chartType" | "specType", "debug" | "locale" | "rotation" | "ariaLabelHeadingLevel" | "ariaUseDefaultSummary" | "legendPosition" | "flatLegend" | "legendMaxDepth" | "legendSize" | "showLegend" | "showLegendExtra" | "baseTheme" | "rendering" | "animateData" | "externalPointerEvents" | "pointBuffer" | "resizeDebounce" | "pointerUpdateTrigger" | "brushAxis" | "minBrushDelta" | "allowBrushingLastHistogramBin", "ariaLabel" | "xDomain" | "ariaDescription" | "ariaDescribedBy" | "ariaLabelledBy" | "ariaTableCaption" | "theme" | "legendAction" | "legendColorPicker" | "legendStrategy" | "onLegendItemClick" | "customLegend" | "onLegendItemMinusClick" | "onLegendItemOut" | "onLegendItemOver" | "onLegendItemPlusClick" | "orderOrdinalBinsBy" | "debugState" | "onProjectionClick" | "onElementClick" | "onElementOver" | "onElementOut" | "onBrushEnd" | "onPointerUpdate" | "onRenderChange" | "onProjectionAreaChange" | "onAnnotationClick" | "pointerUpdateDebounce" | "roundHistogramBrushValues" | "noResults" | "legendSort", never>;

// @public (undocumented)
export type SettingsProps = ComponentProps<typeof Settings>;
Expand All @@ -2508,6 +2508,7 @@ export interface SettingsSpec extends Spec, LegendSpec {
debugState?: boolean;
// @alpha
externalPointerEvents: ExternalPointerEventsSettings;
locale: string;
minBrushDelta?: number;
noResults?: ComponentType | ReactChild;
onAnnotationClick?: AnnotationClickListener;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ function defaultColorBandFormatter(valueFormatter?: ValueFormatter) {
/** @internal */
export function getBandsColorScale(
colorScale: HeatmapBandsColorScale,
locale: string,
valueFormatter?: ValueFormatter,
): { scale: ColorScale; bands: Required<ColorBand>[] } {
const labelFormatter = colorScale.labelFormatter ?? defaultColorBandFormatter(valueFormatter);
const ascendingSortFn = getPredicateFn('numAsc', 'start');
const ascendingSortFn = getPredicateFn('numAsc', locale, 'start');
const bands = colorScale.bands
.reduce<Required<ColorBand>[]>((acc, { start, end, color, label }) => {
// admit only proper bands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<ColorBand>[];
} => getBandsColorScale(spec.colorScale, spec.valueFormatter),
} => getBandsColorScale(spec.colorScale, locale, spec.valueFormatter),
);
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | number>();
const smHValues = new Set<string | number>();
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions packages/charts/src/chart_types/metric/renderer/dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,6 +47,7 @@ interface StateProps {
specs: MetricSpec[];
a11y: A11ySettings;
style: MetricStyle;
locale: string;
onElementClick?: ElementClickListener;
onElementOut?: BasicListener;
onElementOver?: ElementOverListener;
Expand Down Expand Up @@ -78,6 +79,7 @@ class Component extends React.Component<StateProps & DispatchProps> {
onElementClick,
onElementOut,
onElementOver,
locale,
} = this.props;
if (!initialized || !spec || width === 0 || height === 0) {
return null;
Expand Down Expand Up @@ -139,6 +141,7 @@ class Component extends React.Component<StateProps & DispatchProps> {
onElementClick={onElementClick}
onElementOut={onElementOut}
onElementOver={onElementOver}
locale={locale}
/>
</li>
);
Expand Down Expand Up @@ -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,
Expand All @@ -200,6 +204,7 @@ const mapStateToProps = (state: GlobalChartState): StateProps => {
onElementOver,
onElementOut,
style: getChartThemeSelector(state).metric,
locale,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const Metric: React.FunctionComponent<{
datum: MetricDatum;
panel: Size;
style: MetricStyle;
locale: string;
onElementClick?: ElementClickListener;
onElementOver?: ElementOverListener;
onElementOut?: BasicListener;
Expand All @@ -52,6 +53,7 @@ export const Metric: React.FunctionComponent<{
datum,
panel,
style,
locale,
onElementClick,
onElementOver,
onElementOut,
Expand Down Expand Up @@ -143,6 +145,7 @@ export const Metric: React.FunctionComponent<{
style={updatedStyle}
onElementClick={onElementClick ? onElementClickHandler : undefined}
highContrastTextColor={highContrastTextColor}
locale={locale}
/>
{isMetricWTrend(datumWithInteractionColor) && <SparkLine id={metricHTMLId} datum={datumWithInteractionColor} />}
{isMetricWProgress(datumWithInteractionColor) && (
Expand Down
22 changes: 17 additions & 5 deletions packages/charts/src/chart_types/metric/renderer/dom/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,16 @@ 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;

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;
Expand All @@ -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;
Expand Down Expand Up @@ -126,6 +135,7 @@ function elementVisibility(
maxTitlesWidth,
visibilityBreakpoint.titleMaxLines,
textMeasure,
locale,
),
subtitleLines: wrapText(
datum.subtitle ?? '',
Expand All @@ -134,6 +144,7 @@ function elementVisibility(
maxTitlesWidth,
visibilityBreakpoint.subtitleMaxLines,
textMeasure,
locale,
),
};
});
Expand All @@ -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);
Expand All @@ -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'}))`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) =>
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -68,7 +70,7 @@ function getTreesForSpec(
group.push(next);
return map;
}, new Map<string, HierarchyOfArrays>());
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,
Expand All @@ -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) : [],
);
4 changes: 2 additions & 2 deletions packages/charts/src/chart_types/timeslip/timeslip/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,7 +80,7 @@ export interface AxisLayer<T extends Interval> {
/** @internal */
export interface RasterConfig {
minimumTickPixelDistance: number;
locale: keyof typeof LOCALE_TRANSLATIONS;
locale: string;
}

const millisecondIntervals = (rasterMs: number): IntervalIterableMaker<Interval> =>
Expand Down
Loading

0 comments on commit 0bb3ab1

Please sign in to comment.