diff --git a/packages/core/src/components/axes/axis.ts b/packages/core/src/components/axes/axis.ts index 8d7f09ce2a..7f62726878 100644 --- a/packages/core/src/components/axes/axis.ts +++ b/packages/core/src/components/axes/axis.ts @@ -224,6 +224,7 @@ export class Axis extends Component { // create the right ticks formatter let formatter: any const userProvidedFormatter = getProperty(axisOptions, 'ticks', 'formatter') + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') if (isTimeScaleType) { const timeInterval = computeTimeIntervalName( axis.tickValues(), @@ -232,7 +233,7 @@ export class Axis extends Component { if (userProvidedFormatter === null) { formatter = (t: number, i: number) => - formatTick(t, i, axis.tickValues(), timeInterval, timeScaleOptions) + formatTick(t, i, axis.tickValues(), timeInterval, timeScaleOptions, options.locale) } else { formatter = (t: number, i: number) => { const defaultFormattedValue = formatTick( @@ -240,7 +241,8 @@ export class Axis extends Component { i, axis.tickValues(), timeInterval, - timeScaleOptions + timeScaleOptions, + options.locale ) return userProvidedFormatter(t, i, defaultFormattedValue) } @@ -248,7 +250,7 @@ export class Axis extends Component { } else { if (userProvidedFormatter === null) { if (scaleType === ScaleTypes.LINEAR) { - formatter = (t: any) => t.toLocaleString() + formatter = (t: any) => numberFormatter(t, localeCode) } } else { formatter = userProvidedFormatter diff --git a/packages/core/src/components/axes/ruler-binned.ts b/packages/core/src/components/axes/ruler-binned.ts index 0c135fc098..6b060285ac 100644 --- a/packages/core/src/components/axes/ruler-binned.ts +++ b/packages/core/src/components/axes/ruler-binned.ts @@ -100,7 +100,10 @@ export class BinnedRuler extends Ruler { ...(getProperty(options, 'tooltip', 'showTotal') === true ? [ { - label: get(options, 'tooltip.totalLabel') || 'Total', + label: + get(options, 'locale.translations.total') || + get(options, 'tooltip.totalLabel') || + 'Total', value: activeDataGroupNames.reduce( (accum: number, currentValue: any) => accum + parseFloat(get(sampleMatchData, `data.${currentValue}`)), diff --git a/packages/core/src/components/axes/toolbar.ts b/packages/core/src/components/axes/toolbar.ts index db334ed624..095d6b5ce8 100644 --- a/packages/core/src/components/axes/toolbar.ts +++ b/packages/core/src/components/axes/toolbar.ts @@ -159,7 +159,7 @@ export class Toolbar extends Component { .classed('cds--overflow-menu-options__option--disabled', (d: any) => d.shouldBeDisabled()) .attr('aria-disabled', (d: any) => d.shouldBeDisabled()) .selectAll('button') - .text((d: any) => d.text) + .text((d: any) => d.title) } isOverflowMenuOpen() { @@ -367,7 +367,6 @@ export class Toolbar extends Component { getControlConfigs() { const numberOfIcons = getProperty(this.getOptions(), 'toolbar', 'numberOfIcons') - 1 const controls = getProperty(this.getOptions(), 'toolbar', 'controls') - const overflowSpecificControls: any[] = [] const buttonList: any[] = [] const overflowList: any[] = [] @@ -468,6 +467,13 @@ export class Toolbar extends Component { !this.services.zoom.isEmptyState() const displayData = this.model.getDisplayData() + const options = this.model.getOptions() + const { exportAsCSV, exportAsJPG, exportAsPNG } = getProperty( + options, + 'locale', + 'translations', + 'toolbar' + ) let controlConfig: any switch (controlType) { @@ -539,7 +545,7 @@ export class Toolbar extends Component { case ToolbarControlTypes.EXPORT_CSV: controlConfig = { id: 'toolbar-export-CSV', - title: 'Export as CSV', + title: exportAsCSV, shouldBeDisabled: () => false, iconSVG: { content: this.getControlIconByType(controlType) @@ -550,7 +556,7 @@ export class Toolbar extends Component { case ToolbarControlTypes.EXPORT_PNG: controlConfig = { id: 'toolbar-export-PNG', - title: 'Export as PNG', + title: exportAsPNG, shouldBeDisabled: () => false, iconSVG: { content: this.getControlIconByType(controlType) @@ -561,7 +567,7 @@ export class Toolbar extends Component { case ToolbarControlTypes.EXPORT_JPG: controlConfig = { id: 'toolbar-export-JPG', - title: 'Export as JPG', + title: exportAsJPG, shouldBeDisabled: () => false, iconSVG: { content: this.getControlIconByType(controlType) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 87aeee5586..c828cccfcd 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -157,9 +157,12 @@ export class ColorScaleLegend extends Legend { // Create scale & ticks const linearScale = scaleLinear().domain(domain).range([0, barWidth]) - const legendAxis = axisBottom(linearScale).tickSize(0).tickValues(quant) + //translating ticks into given locale language + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') + legendAxis.tickFormat(d => numberFormatter(d, localeCode)) + let rangeStart: any // avoid unexpected lexical declaration in case block switch (colorScaleType) { case ColorLegendType.LINEAR: diff --git a/packages/core/src/components/essentials/modal.ts b/packages/core/src/components/essentials/modal.ts index 5eb8a1d824..4dfa28d732 100644 --- a/packages/core/src/components/essentials/modal.ts +++ b/packages/core/src/components/essentials/modal.ts @@ -73,6 +73,8 @@ export class Modal extends Component { const options = this.model.getOptions() + const { title, downloadAsCSV } = getProperty(options, 'locale', 'translations', 'tabularRep') + const chartprefix = getProperty(options, 'style', 'prefix') const tableArray = this.model.getTabularDataArray() @@ -80,7 +82,8 @@ export class Modal extends Component { return `
-

Tabular representation

+ +

${sanitizeText( options.title @@ -122,7 +125,7 @@ export class Modal extends Component {

` } diff --git a/packages/core/src/components/essentials/threshold.ts b/packages/core/src/components/essentials/threshold.ts index 63c666b104..419ddb0535 100644 --- a/packages/core/src/components/essentials/threshold.ts +++ b/packages/core/src/components/essentials/threshold.ts @@ -201,7 +201,7 @@ export class Threshold extends Component { const { value, axisPosition } = datum const options = this.getOptions() const scaleType = this.services.cartesianScales.getScaleTypeByPosition(axisPosition) - + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') // If scale is time, format the threshold date as the ticks format if (scaleType === ScaleTypes.TIME) { const isVertical = [AxisPositions.LEFT, AxisPositions.RIGHT].includes(axisPosition) @@ -215,10 +215,10 @@ export class Threshold extends Component { getProperty(timeScaleOptions, 'timeInterval') ) - return formatTick(value, 0, scale.ticks(), timeInterval, timeScaleOptions) + return formatTick(value, 0, scale.ticks(), timeInterval, timeScaleOptions, options.locale) } - return value.toLocaleString('en') + return numberFormatter(value, localeCode) } appendThresholdLabel() { diff --git a/packages/core/src/components/essentials/title-meter.ts b/packages/core/src/components/essentials/title-meter.ts index 07afaea82e..ebe6a45e22 100644 --- a/packages/core/src/components/essentials/title-meter.ts +++ b/packages/core/src/components/essentials/title-meter.ts @@ -18,7 +18,7 @@ export class MeterTitle extends Title { const options = this.getOptions() const svg = this.getComponentContainer() const { groupMapsTo } = options.data - + const meterTitle = options.locale.translations.meter.title const proportional = getProperty(options, 'meter', 'proportional') if (proportional) { @@ -26,8 +26,9 @@ export class MeterTitle extends Title { this.displayBreakdownTitle() } else { // the title for a meter, is the label for that dataset - const title = svg.selectAll('text.meter-title').data([dataset[groupMapsTo]]) - + const title = svg + .selectAll('text.meter-title') + .data(meterTitle ? [meterTitle] : [dataset[groupMapsTo]]) title .enter() .append('text') @@ -73,13 +74,14 @@ export class MeterTitle extends Title { const difference = total !== null ? total - datasetsTotal : datasetsTotal //breakdownFormatter const breakdownFormatter = getProperty(options, 'meter', 'proportional', 'breakdownFormatter') + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') data = breakdownFormatter !== null ? breakdownFormatter({ datasetsTotal: datasetsTotal, total: total }) - : `${datasetsTotal} ${unit} used (${difference} ${unit} available)` + : `${numberFormatter(datasetsTotal, localeCode)} ${unit} used (${numberFormatter(difference, localeCode)} ${unit} available)` } // the breakdown part to whole of the datasets to the overall total @@ -121,9 +123,12 @@ export class MeterTitle extends Title { // totalFormatter function const totalFormatter = getProperty(options, 'meter', 'proportional', 'totalFormatter') + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') const totalString = - totalFormatter !== null ? totalFormatter(totalValue) : `${total} ${unit} total` + totalFormatter !== null + ? totalFormatter(totalValue) + : `${numberFormatter(total, localeCode)} ${unit} total` const containerBounds = DOMUtils.getHTMLElementSize(this.services.domUtils.getMainContainer()) @@ -214,7 +219,7 @@ export class MeterTitle extends Title { */ appendPercentage() { const dataValue = getProperty(this.model.getDisplayData(), 0, 'value') - + const { code: localeCode, number: numberFormatter } = getProperty(this.getOptions(), 'locale') // use the title's position to append the percentage to the end const svg = this.getComponentContainer() const title = DOMUtils.appendOrSelect(svg, 'text.meter-title') @@ -237,7 +242,7 @@ export class MeterTitle extends Title { .append('text') .classed('percent-value', true) .merge(percentage as any) - .text((d: any) => `${d}%`) + .text((d: any) => `${numberFormatter(d, localeCode)}%`) .attr('x', +title.attr('x') + title.node().getComputedTextLength() + offset) // set the position to after the title .attr('y', title.attr('y')) diff --git a/packages/core/src/components/essentials/tooltip-axis.ts b/packages/core/src/components/essentials/tooltip-axis.ts index 184d1af5a5..21b83b2470 100644 --- a/packages/core/src/components/essentials/tooltip-axis.ts +++ b/packages/core/src/components/essentials/tooltip-axis.ts @@ -73,7 +73,7 @@ export class AxisChartsTooltip extends Tooltip { } items.push({ - label: options.tooltip.groupLabel, + label: get(options, 'locale.translations.group') || get(options, 'tooltip.groupLabel'), value: datum[groupMapsTo], color: this.model.getFillColor(datum[groupMapsTo]), class: this.model.getColorClassName({ @@ -113,7 +113,10 @@ export class AxisChartsTooltip extends Tooltip { // use the primary/only range id const rangeIdentifier = cartesianScales.getRangeIdentifier() items.push({ - label: get(options, 'tooltip.totalLabel') || 'Total', + label: + get(options, 'locale.translations.total') || + get(options, 'tooltip.totalLabel') || + 'Total', value: data.reduce( (accumulator: number, datum: any) => accumulator + datum[rangeIdentifier], 0 diff --git a/packages/core/src/components/essentials/tooltip.ts b/packages/core/src/components/essentials/tooltip.ts index 5385375181..6bb006b398 100644 --- a/packages/core/src/components/essentials/tooltip.ts +++ b/packages/core/src/components/essentials/tooltip.ts @@ -1,5 +1,4 @@ import { select, pointer } from 'd3' -import { format } from 'date-fns/format' import Position, { PLACEMENTS } from '@carbon/utils-position' // position service import { getProperty, truncateLabel } from '@/tools' import { zoomBar as zoomBarConfigs, tooltips as tooltipConfigs } from '@/configuration' @@ -182,25 +181,35 @@ export class Tooltip extends Component { valueFormatter(value: any, label: string) { const options = this.getOptions() const valueFormatter = getProperty(options, 'tooltip', 'valueFormatter') + const { + code: localeCode, + number: numberFormatter, + date: dateFormatter + } = getProperty(options, 'locale') if (valueFormatter) { return valueFormatter(value, label) } if (typeof value.getTime === 'function') { - return format(value, 'MMM d, yyyy') + return dateFormatter(value, localeCode, { month: 'short', day: 'numeric', year: 'numeric' }) } try { // it's a correct ISO format Date string if (typeof value === 'string' && /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(value)) { - return format(Date.parse(value), 'MMM d, yyyy') + const newDate = new Date(value) + return dateFormatter(newDate, localeCode, { + month: 'short', + day: 'numeric', + year: 'numeric' + }) } } catch (e) { // not a valid ISO format string } - return value.toLocaleString() + return numberFormatter(value, localeCode) } // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/packages/core/src/components/graphs/alluvial.ts b/packages/core/src/components/graphs/alluvial.ts index 443b935fef..cb2c1f4823 100644 --- a/packages/core/src/components/graphs/alluvial.ts +++ b/packages/core/src/components/graphs/alluvial.ts @@ -252,6 +252,7 @@ export class Alluvial extends Component { this.services.domUtils.generateElementIDString(`alluvial-node-title-${d.index}`) ) + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') // Node title - text textNode .append('text') @@ -267,7 +268,7 @@ export class Alluvial extends Component { // shift 13 pixels down to fit background container .attr('dy', 13) .text((d: any) => { - return `${d.name} (${d.value})` + return `${d.name} (${numberFormatter(d.value, localeCode)})` }) .attr('aria-label', (d: any) => { return `${d.name} (${d.value})` @@ -327,6 +328,7 @@ export class Alluvial extends Component { addLineEventListener() { const options = this.getOptions() const self = this + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') // Set delay to counter flashy behaviour const debouncedLineHighlight = debounce((link, event = 'mouseover') => { @@ -379,7 +381,10 @@ export class Alluvial extends Component { items: [ { label: datum.target.name, - value: datum.value + (options.alluvial.units ? ` ${options.alluvial.units}` : ''), + value: + (numberFormatter(datum.value, localeCode) + ? `${numberFormatter(datum.value, localeCode)}` + : '-') + (options.alluvial.units ? ` ${options.alluvial.units}` : ''), color: strokeColor, labelIcon: self.getRightArrowIcon() } diff --git a/packages/core/src/components/graphs/boxplot.ts b/packages/core/src/components/graphs/boxplot.ts index 26e6fece2c..5d4f08e624 100644 --- a/packages/core/src/components/graphs/boxplot.ts +++ b/packages/core/src/components/graphs/boxplot.ts @@ -1,5 +1,5 @@ import { select } from 'd3' -import { flipDomainAndRangeBasedOnOrientation, generateSVGPathString } from '@/tools' +import { flipDomainAndRangeBasedOnOrientation, generateSVGPathString, getProperty } from '@/tools' import { boxplot as boxplotConfigs } from '@/configuration' import { BoxplotChartModel } from '@/model/boxplot' import { Component } from '@/components/component' @@ -334,7 +334,10 @@ export class Boxplot extends Component { hoveredElement, items: [ { - label: options.tooltip.groupLabel, + label: + getProperty(options, 'locale', 'translations', 'group') || + getProperty(options, 'tooltip', 'groupLabel') || + 'Group', value: datum[groupMapsTo], class: self.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.TOOLTIP] @@ -442,7 +445,10 @@ export class Boxplot extends Component { hoveredElement, items: [ { - label: options.tooltip.groupLabel, + label: + getProperty(options, 'locale', 'translations', 'group') || + getProperty(options, 'tooltip', 'groupLabel') || + 'Group', value: datum[groupMapsTo], class: self.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.TOOLTIP] diff --git a/packages/core/src/components/graphs/bullet.ts b/packages/core/src/components/graphs/bullet.ts index 0515c5539b..13f1018ee7 100644 --- a/packages/core/src/components/graphs/bullet.ts +++ b/packages/core/src/components/graphs/bullet.ts @@ -322,6 +322,8 @@ export class Bullet extends Component { const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier() + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') + this.parent .selectAll('path.bar') .on('mouseover', function (event: MouseEvent, datum: any) { @@ -345,7 +347,10 @@ export class Bullet extends Component { hoveredElement, items: [ { - label: options.tooltip.groupLabel || 'Group', + label: + getProperty(options, 'locale', 'translations', 'group') || + getProperty(options, 'tooltip', 'groupLabel') || + 'Group', value: datum[groupMapsTo], class: self.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.TOOLTIP], @@ -362,7 +367,7 @@ export class Bullet extends Component { }, { label: 'Percentage', - value: `${Math.floor((datum[rangeIdentifier] / datum.marker) * 100)}%` + value: `${numberFormatter(Math.floor((datum[rangeIdentifier] / datum.marker) * 100), localeCode)}%` }, { label: 'Performance', diff --git a/packages/core/src/components/graphs/circle-pack.ts b/packages/core/src/components/graphs/circle-pack.ts index 9b48d1725f..58f4f512c8 100644 --- a/packages/core/src/components/graphs/circle-pack.ts +++ b/packages/core/src/components/graphs/circle-pack.ts @@ -260,7 +260,10 @@ export class CirclePack extends Component { const options = self.model.getOptions() totalValue = [ { - label: get(options, 'tooltip.totalLabel') || 'Total', + label: + get(options, 'locale.translations.total') || + get(options, 'tooltip.totalLabel') || + 'Total', value: datum.value, bold: true } diff --git a/packages/core/src/components/graphs/donut.ts b/packages/core/src/components/graphs/donut.ts index 2912b1bafb..45d38e9717 100644 --- a/packages/core/src/components/graphs/donut.ts +++ b/packages/core/src/components/graphs/donut.ts @@ -93,8 +93,8 @@ export class Donut extends Pie { const i = interpolateFunction(currentValue, donutCenterFigure) return (t: any) => { - const { numberFormatter } = options.donut.center - d3Ref.text(numberFormatter(i(t))) + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') + d3Ref.text(numberFormatter(i(t), localeCode)) } } } diff --git a/packages/core/src/components/graphs/gauge.ts b/packages/core/src/components/graphs/gauge.ts index 50165171ad..2a7382226e 100644 --- a/packages/core/src/components/graphs/gauge.ts +++ b/packages/core/src/components/graphs/gauge.ts @@ -199,7 +199,7 @@ export class Gauge extends Component { const fontSize = valueFontSize(radius) // Add the big number const valueNumberGroup = DOMUtils.appendOrSelect(numbersGroup, 'g.gauge-value-number') - + const { code: localeCode, number: localeNumberFormatter } = getProperty(options, 'locale') const numberFormatter = getProperty(options, 'gauge', 'numberFormatter') const valueNumber = valueNumberGroup.selectAll('text.gauge-value-number').data([value]) @@ -210,7 +210,7 @@ export class Gauge extends Component { .merge(valueNumber as any) .style('font-size', `${fontSize}px`) .attr('text-anchor', 'middle') - .text((d: any) => numberFormatter(d)) + .text((d: any) => localeNumberFormatter(Number(numberFormatter(d)), localeCode)) // add the percentage symbol beside the valueNumber const { width: valueNumberWidth } = DOMUtils.getSVGElementSize( @@ -245,7 +245,7 @@ export class Gauge extends Component { const svg = this.getComponentContainer() const options = this.getOptions() const delta = this.getDelta() - + const { code: localeCode, number: localeNumberFormatter } = getProperty(options, 'locale') if (!delta) { const deltaGroup = svg.select('g.gauge-delta') @@ -287,7 +287,10 @@ export class Gauge extends Component { .merge(deltaNumber) .attr('text-anchor', 'middle') .style('font-size', `${deltaFontSize(radius)}px`) - .text((d: any) => `${numberFormatter(d)}${gaugeSymbol}`) + .text( + (d: any) => + `${localeNumberFormatter(Number(numberFormatter(d)), localeCode)}${gaugeSymbol}` + ) // Add the caret for the delta number const { width: deltaNumberWidth } = DOMUtils.getSVGElementSize( diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 40c0d13d3d..30427b2282 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -203,7 +203,8 @@ export class Heatmap extends Component { const self = this const { cartesianScales } = this.services const options = this.getOptions() - const totalLabel = get(options, 'tooltip.totalLabel') + const totalLabel = + get(options, 'locale.translations.total') || get(options, 'tooltip.totalLabel') || 'Total' const domainIdentifier = cartesianScales.getDomainIdentifier() const rangeIdentifier = cartesianScales.getRangeIdentifier() @@ -253,7 +254,7 @@ export class Heatmap extends Component { value: datum[rangeIdentifier] }, { - label: totalLabel || 'Total', + label: totalLabel, value: datum['value'], color: hoveredElement.style('fill') } diff --git a/packages/core/src/components/graphs/histogram.ts b/packages/core/src/components/graphs/histogram.ts index 30b625d1ea..b710f1b182 100644 --- a/packages/core/src/components/graphs/histogram.ts +++ b/packages/core/src/components/graphs/histogram.ts @@ -153,7 +153,7 @@ export class Histogram extends Component { addEventListeners() { const options = this.model.getOptions() const { groupMapsTo } = options.data - + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') const self = this this.parent .selectAll('path.bar') @@ -162,8 +162,8 @@ export class Histogram extends Component { hoveredElement.classed('hovered', true) - const x0 = parseFloat(get(datum, 'data.x0')) - const x1 = parseFloat(get(datum, 'data.x1')) + const x0 = numberFormatter(parseFloat(get(datum, 'data.x0')), localeCode) + const x1 = numberFormatter(parseFloat(get(datum, 'data.x1')), localeCode) const rangeAxisPosition = self.services.cartesianScales.getRangeAxisPosition() const rangeScaleLabel = self.services.cartesianScales.getScaleLabel(rangeAxisPosition) diff --git a/packages/core/src/components/graphs/pie.ts b/packages/core/src/components/graphs/pie.ts index 2f2cf3b450..e32d5aeae1 100644 --- a/packages/core/src/components/graphs/pie.ts +++ b/packages/core/src/components/graphs/pie.ts @@ -130,6 +130,7 @@ export class Pie extends Component { }) // Draw the slice labels + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') const renderLabels = options.pie.labels.enabled const labelData = renderLabels ? pieLayoutData.filter(x => (x.data as any)[valueMapsTo] > 0) @@ -165,7 +166,12 @@ export class Pie extends Component { ) }) } - return convertValueToPercentage(d.data[valueMapsTo], displayData, valueMapsTo) + '%' + return ( + numberFormatter( + convertValueToPercentage(d.data[valueMapsTo], displayData, valueMapsTo), + localeCode + ) + '%' + ) }) // Calculate dimensions in order to transform .datum(function (d: any) { diff --git a/packages/core/src/components/graphs/radar.ts b/packages/core/src/components/graphs/radar.ts index cc5d4fe7ca..531cff1676 100644 --- a/packages/core/src/components/graphs/radar.ts +++ b/packages/core/src/components/graphs/radar.ts @@ -487,6 +487,7 @@ export class Radar extends Component { .attr('transform', (key: any) => `rotate(${radToDeg(xScale(key))}, ${c.x}, ${c.y})`) // y labels (show only the min and the max labels) + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') const yLabels = DOMUtils.appendOrSelect(svg, 'g.y-labels').attr('role', Roles.GROUP) const yLabelUpdate = yLabels.selectAll('text').data(extent(yTicks)) yLabelUpdate.join( @@ -494,7 +495,7 @@ export class Radar extends Component { enter .append('text') .attr('opacity', 0) - .text((tick: any) => tick) + .text((tick: any) => numberFormatter(tick, localeCode)) .attr( 'x', (tick: any) => polarToCartesianCoords(-Math.PI / 2, yScale(tick), c).x + yLabelPadding diff --git a/packages/core/src/components/graphs/wordcloud.ts b/packages/core/src/components/graphs/wordcloud.ts index 77428dab39..a2e167f7f4 100644 --- a/packages/core/src/components/graphs/wordcloud.ts +++ b/packages/core/src/components/graphs/wordcloud.ts @@ -1,6 +1,6 @@ import { extent, scaleLinear, select } from 'd3' import cloud from 'd3-cloud' -import { debounce } from 'lodash-es' +import { debounce, get } from 'lodash-es' import { Component } from '@/components/component' import { DOMUtils } from '@/services/essentials/dom-utils' import { Events, ColorClassNameTypes, RenderTypes } from '@/interfaces/enums' @@ -217,7 +217,10 @@ export class WordCloud extends Component { value: datum.value }, { - label: options.tooltip.groupLabel, + label: + get(options, 'locale.translations.group') || + get(options, 'tooltip.groupLabel') || + 'Group', value: datum[groupMapsTo], class: self.model.getColorClassName({ classNameTypes: [ColorClassNameTypes.TOOLTIP], diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index 40ba2444ec..3c65904bd0 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -1,6 +1,7 @@ import { enUS as localeObject } from 'date-fns/locale' import { merge } from 'lodash-es' import { circlePack } from './configuration-non-customizable' + import type { AlluvialChartOptions, AreaChartOptions, @@ -52,7 +53,8 @@ import type { LegendOptions, StackedBarOptions, ToolbarOptions, - ZoomBarsOptions + ZoomBarsOptions, + Locale } from '@/interfaces/components' /* @@ -70,6 +72,187 @@ const standardTruncationOptions = { numCharacter: 14 } +/** + * Locale options + */ +const locale: Locale = { + code: navigator.language, // read from browser's navigator.language + number: (value, language = navigator.language) => value.toLocaleString(language), // based on code property if specified + date: (value, language = navigator.language, options = {}, preformattedLocaleValue = null) => + preformattedLocaleValue ? preformattedLocaleValue : value.toLocaleDateString(language, options), // based on code property if specified + time: (value, language = navigator.language, options = {}, preformattedLocaleValue = null) => + preformattedLocaleValue ? preformattedLocaleValue : value.toLocaleTimeString(language, options), // based on code property if specified + optionsObject: { + '15seconds': { + primary: { + 'MMM d, pp': { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h12' + }, + 'MMM d, h:mm:ss.SSS a': { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + fractionalSecondDigits: 3, + hourCycle: 'h12' + } + }, + secondary: { + pp: { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h12' + }, + 'h:mm:ss.SSS a': { + hour: 'numeric', + minute: '2-digit', + fractionalSecondDigits: 3, + hourCycle: 'h12' + } + }, + type: 'time' + }, + minute: { + primary: { + 'MMM d, p': { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hourCycle: 'h12' + } + }, + secondary: { + p: { + hour: 'numeric', + minute: '2-digit', + hourCycle: 'h12' + } + }, + type: 'time' + }, + '30minutes': { + primary: { + 'MMM d, p': { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hourCycle: 'h12' + } + }, + secondary: { + p: { + hour: 'numeric', + minute: '2-digit', + hourCycle: 'h12' + } + }, + type: 'time' + }, + hourly: { + primary: { + 'MMM d, hh a': { + month: 'short', + day: 'numeric', + hour: '2-digit', + hourCycle: 'h12' + } + }, + secondary: { + 'hh a': { + hour: '2-digit', + hourCycle: 'h12' + } + }, + type: 'time' + }, + daily: { + primary: { + 'MMM d': { + month: 'short', + day: 'numeric' + } + }, + secondary: { + d: { + day: 'numeric' + } + }, + type: 'date' + }, + weekly: { + primary: { + 'eee, MMM d': { + weekday: 'short', + month: 'short', + day: 'numeric' + } + }, + secondary: { + eee: { + weekday: 'short' + } + }, + type: 'date' + }, + monthly: { + primary: { + 'MMM yyyy': { + month: 'short', + year: 'numeric' + } + }, + secondary: { + MMM: { + month: 'short' + } + }, + type: 'date' + }, + quarterly: { + primary: {}, + secondary: {}, + type: 'date' + }, + yearly: { + primary: { + yyyy: { + year: 'numeric' + } + }, + secondary: { + yyyy: { + year: 'numeric' + } + }, + type: 'date' + } + }, + translations: { + group: 'Group', + total: 'Total', + meter: { + title: '' //default is emply string as meter title is dataset label + }, + tabularRep: { + title: 'Tabular representation', + downloadAsCSV: 'Download as CSV' + }, + toolbar: { + exportAsCSV: 'Export to CSV', + exportAsJPG: 'Export to JPG', + exportAsPNG: 'Export to PNG' + } + } +} + /** * Legend options */ @@ -179,6 +362,7 @@ const chart: BaseChartOptions = { theme: ChartTheme.WHITE, tooltip: baseTooltip, legend, + locale, style: { prefix: 'cc' }, @@ -519,8 +703,7 @@ const radarChart: RadarChartOptions = merge({}, chart, { tooltip: { gridline: { enabled: true - }, - valueFormatter: value => (value !== null && value !== undefined ? value : 'N/A') + } } } as RadarChartOptions) diff --git a/packages/core/src/demo/charts/bar.ts b/packages/core/src/demo/charts/bar.ts index fc95b89023..0a0a8aa90a 100644 --- a/packages/core/src/demo/charts/bar.ts +++ b/packages/core/src/demo/charts/bar.ts @@ -495,6 +495,91 @@ export const simpleBarTurkishLocaleOptions = { } } +//using locale interface to reformat everything to Arabic +export const simpleBarArabicLocaleOptions = { + title: 'Arabic locale using Locale Interface', + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'date', + scaleType: ScaleTypes.TIME + } + }, + locale: { + code: 'ar-SA' + } +} + +//using locale interface to reformat everything to Iranian +export const simpleBarIranianLocaleOptions = { + title: 'Iranian locale using Locale Interface', + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'date', + scaleType: ScaleTypes.TIME + } + }, + locale: { + code: 'fa-IR' + } +} + +//using locale interface to reformat everything to Japanese +export const simpleBarJapaneseLocaleOptions = { + title: 'Japanese locale using Locale Interface', + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'date', + scaleType: ScaleTypes.TIME + } + }, + locale: { + code: 'ja-JP' + } +} + +//using locale interface to reformat everything to Hindi +export const simpleBarHindiLocaleOptions = { + title: 'Hindi locale using Locale Interface', + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'date', + scaleType: ScaleTypes.TIME + } + }, + locale: { + code: 'hi-IN' + } +} + +//using locale interface to reformat everything to Bangla +export const simpleBarBanglaLocaleOptions = { + title: 'Bangla locale using Locale Interface', + axes: { + left: { + mapsTo: 'value' + }, + bottom: { + mapsTo: 'date', + scaleType: ScaleTypes.TIME + } + }, + locale: { + code: 'bn-BD' + } +} + // Horizontal simple time series export const simpleHorizontalBarTimeSeriesOptions = { title: 'Horizontal simple bar (time series)', diff --git a/packages/core/src/demo/charts/index.ts b/packages/core/src/demo/charts/index.ts index 3226be8d2e..e364948b50 100644 --- a/packages/core/src/demo/charts/index.ts +++ b/packages/core/src/demo/charts/index.ts @@ -287,6 +287,31 @@ const utilityDemoGroups: DemoGroup[] = [ options: barDemos.simpleBarTurkishLocaleOptions, data: barDemos.simpleBarTurkishLocaleData, chartType: chartTypes.SimpleBarChart + }, + { + options: barDemos.simpleBarArabicLocaleOptions, + data: barDemos.simpleBarTurkishLocaleData, + chartType: chartTypes.SimpleBarChart + }, + { + options: barDemos.simpleBarIranianLocaleOptions, + data: barDemos.simpleBarTurkishLocaleData, + chartType: chartTypes.SimpleBarChart + }, + { + options: barDemos.simpleBarJapaneseLocaleOptions, + data: barDemos.simpleBarTurkishLocaleData, + chartType: chartTypes.SimpleBarChart + }, + { + options: barDemos.simpleBarHindiLocaleOptions, + data: barDemos.simpleBarTurkishLocaleData, + chartType: chartTypes.SimpleBarChart + }, + { + options: barDemos.simpleBarBanglaLocaleOptions, + data: barDemos.simpleBarTurkishLocaleData, + chartType: chartTypes.SimpleBarChart } ] }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 67a28ac49d..273d6762e7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -90,6 +90,8 @@ export type { ComboChartAxisOptions, GridOptions, LegendOptions, + Locale, + LocaleTimeScaleOptions, RulerOptions, StackedBarOptions, TimeScaleOptions, diff --git a/packages/core/src/interfaces/charts.ts b/packages/core/src/interfaces/charts.ts index 75461fedc4..591f220b25 100644 --- a/packages/core/src/interfaces/charts.ts +++ b/packages/core/src/interfaces/charts.ts @@ -18,6 +18,7 @@ import type { ToolbarOptions, TooltipOptions, ZoomBarsOptions, + Locale, TabularRepCustomizationOptions } from './components' import type { @@ -36,6 +37,10 @@ export interface BaseChartOptions { * Optionally specify a title for the chart */ title?: string + /** + * Locale configuration + */ + locale?: Locale /** * boolean to disable animations (enabled by default) */ diff --git a/packages/core/src/interfaces/components.ts b/packages/core/src/interfaces/components.ts index f9a3e70d78..2b1a62d7fe 100644 --- a/packages/core/src/interfaces/components.ts +++ b/packages/core/src/interfaces/components.ts @@ -8,6 +8,89 @@ import type { import type { Component } from '../components' import type { TruncationOptions } from './truncation' +/** + *Locale Options Interface + */ +export interface Locale { + code?: string // BCP 47 language tag + number?: (value: number, language: string) => string + date?: ( + value: Date, + language: string, + options: Intl.DateTimeFormatOptions, + preformattedLocaleValue?: string + ) => string + time?: ( + value: Date, + language: string, + options: Intl.DateTimeFormatOptions, + preformattedLocaleValue?: string + ) => string + optionsObject?: { + '15seconds'?: LocaleTimeScaleOptions + minute?: LocaleTimeScaleOptions + '30minutes'?: LocaleTimeScaleOptions + hourly?: LocaleTimeScaleOptions + daily?: LocaleTimeScaleOptions + weekly?: LocaleTimeScaleOptions + monthly?: LocaleTimeScaleOptions + quarterly?: LocaleTimeScaleOptions + yearly?: LocaleTimeScaleOptions + } + translations?: { + group?: string // used by Tooltip and Toolbar / Tabular Representation + total?: string // ditto + meter?: { + title?: string + } + tabularRep: { + title?: string + downloadAsCSV?: string + } + toolbar: { + exportAsCSV?: string + exportAsJPG?: string + exportAsPNG?: string + } + } +} + +export interface LocaleTimeScaleOptions { + primary?: Record< + string, + { + month?: string + day?: string + hour?: string + minute?: string + second?: string + fractionalSecondDigits?: number + weekday?: string + year?: string + hourCycle?: string + hour12?: boolean + dayPeriod?: string + } + > + secondary?: Record< + string, + { + month?: string + day?: string + hour?: string + minute?: string + second?: string + fractionalSecondDigits?: number + weekday?: string + year?: string + hourCycle?: string + hour12?: boolean + dayPeriod?: string + } + > + type?: string +} + /** * customize the overlay contents */ diff --git a/packages/core/src/interfaces/index.ts b/packages/core/src/interfaces/index.ts index bac323f3a6..374da2aa43 100644 --- a/packages/core/src/interfaces/index.ts +++ b/packages/core/src/interfaces/index.ts @@ -52,6 +52,8 @@ export type { LayoutComponentChild, LegendItem, LegendOptions, + Locale, + LocaleTimeScaleOptions, RulerOptions, StackedBarOptions, ThresholdOptions, diff --git a/packages/core/src/model/alluvial.ts b/packages/core/src/model/alluvial.ts index 0393e5e0c7..c6f2551b85 100644 --- a/packages/core/src/model/alluvial.ts +++ b/packages/core/src/model/alluvial.ts @@ -1,6 +1,6 @@ // Internal Imports import { ChartModelCartesian } from './cartesian-charts' - +import { getProperty } from '@/tools' /** * Alluvial chart model layer */ @@ -11,12 +11,17 @@ export class AlluvialChartModel extends ChartModelCartesian { getTabularDataArray() { const displayData = this.getDisplayData() + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') // Sort array by source to get a close depiction of the alluvial chart displayData.sort((a: any, b: any) => a['source'].localeCompare(b['source'])) const headers = ['Source', 'Target', 'Value'] const cells = [ - ...displayData.map((datum: any) => [datum['source'], datum['target'], datum['value']]) + ...displayData.map((datum: any) => [ + datum['source'], + datum['target'], + numberFormatter(datum['value'], localeCode) + ]) ] return super.formatTable({ headers, cells }) diff --git a/packages/core/src/model/binned-charts.ts b/packages/core/src/model/binned-charts.ts index 91728d40a9..a2434af13e 100644 --- a/packages/core/src/model/binned-charts.ts +++ b/packages/core/src/model/binned-charts.ts @@ -1,6 +1,6 @@ // Internal Imports import { ChartModelCartesian } from './cartesian-charts' - +import { getProperty } from '@/tools' import { get } from 'lodash-es' /** @@ -10,17 +10,22 @@ export class ChartModelBinned extends ChartModelCartesian { getTabularDataArray() { const options = this.getOptions() const { groupMapsTo } = options.data - + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') const binnedStackedData = this.getBinnedStackedData() - const headers = [ + const headers = [ get(options, 'bins.rangeLabel') || 'Range', ...binnedStackedData.map(datum => get(datum, `0.${groupMapsTo}`)) ] const cells = [ ...get(binnedStackedData, 0).map((d, i) => [ - `${get(d, 'data.x0')} – ${get(d, 'data.x1')}`, - ...binnedStackedData.map(datum => get(datum[i], `data.${get(datum[i], groupMapsTo)}`)) + `${numberFormatter(Number(get(d, 'data.x0')), localeCode)} – ${numberFormatter( + Number(get(d, 'data.x1')), + localeCode + )}`, + ...binnedStackedData.map(datum => + numberFormatter(get(datum[i], `data.${get(datum[i], groupMapsTo)}`), localeCode) + ) ]) ] diff --git a/packages/core/src/model/boxplot.ts b/packages/core/src/model/boxplot.ts index a402d0951b..91d1359b84 100644 --- a/packages/core/src/model/boxplot.ts +++ b/packages/core/src/model/boxplot.ts @@ -80,8 +80,8 @@ export class BoxplotChartModel extends ChartModelCartesian { getTabularDataArray() { const options = this.getOptions() const { groupMapsTo } = options.data - const boxplotData = this.getBoxplotData() + const { number: numberFormatter, code: localeCode } = getProperty(options, 'locale') const headers = ['Group', 'Minimum', 'Q1', 'Median', 'Q3', 'Maximum', 'IQR', 'Outlier(s)'] const cells = [ @@ -93,27 +93,28 @@ export class BoxplotChartModel extends ChartModelCartesian { return [ datum[groupMapsTo], getProperty(datum, 'whiskers', 'min') !== null - ? getProperty(datum, 'whiskers', 'min').toLocaleString() + ? numberFormatter(getProperty(datum, 'whiskers', 'min'), localeCode) : '–', getProperty(datum, 'quartiles', 'q_25') !== null - ? getProperty(datum, 'quartiles', 'q_25').toLocaleString() + ? numberFormatter(getProperty(datum, 'quartiles', 'q_25'), localeCode) : '–', getProperty(datum, 'quartiles', 'q_50') !== null - ? getProperty(datum, 'quartiles', 'q_50').toLocaleString() + ? numberFormatter(getProperty(datum, 'quartiles', 'q_50'), localeCode) : '–', getProperty(datum, 'quartiles', 'q_75') !== null - ? getProperty(datum, 'quartiles', 'q_75').toLocaleString() + ? numberFormatter(getProperty(datum, 'quartiles', 'q_75'), localeCode) : '–', getProperty(datum, 'whiskers', 'max') !== null - ? getProperty(datum, 'whiskers', 'max').toLocaleString() + ? numberFormatter(getProperty(datum, 'whiskers', 'max'), localeCode) : '–', getProperty(datum, 'quartiles', 'q_75') !== null && getProperty(datum, 'quartiles', 'q_25') !== null - ? ( + ? (numberFormatter( getProperty(datum, 'quartiles', 'q_75') - getProperty(datum, 'quartiles', 'q_25') - ).toLocaleString() + ), + localeCode) : '–', - outliers.map((d: any) => d.toLocaleString()).join(',') + outliers.map((d: any) => numberFormatter(d, localeCode)).join(',') ] }) ] diff --git a/packages/core/src/model/bullet.ts b/packages/core/src/model/bullet.ts index 7557bc9050..61e70d039a 100644 --- a/packages/core/src/model/bullet.ts +++ b/packages/core/src/model/bullet.ts @@ -33,6 +33,7 @@ export class BulletChartModel extends ChartModelCartesian { const options = this.getOptions() const { groupMapsTo } = options.data const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier() + const { number: numberFormatter, code: localeCode } = getProperty(options, 'locale') const performanceAreaTitles = getProperty(options, 'bullet', 'performanceAreaTitles') const headers = ['Title', 'Group', 'Value', 'Target', 'Percentage', 'Performance'] @@ -40,11 +41,13 @@ export class BulletChartModel extends ChartModelCartesian { ...displayData.map((datum: any) => [ datum['title'], datum[groupMapsTo], - datum['value'] === null ? '–' : datum['value'], - getProperty(datum, 'marker') === null ? '–' : datum['marker'], + datum['value'] === null ? '–' : numberFormatter(datum['value'], localeCode), getProperty(datum, 'marker') === null ? '–' - : `${Math.floor((datum[rangeIdentifier] / datum.marker) * 100)}%`, + : numberFormatter(datum['marker'], localeCode), + getProperty(datum, 'marker') === null + ? '–' + : `${numberFormatter(Math.floor((datum[rangeIdentifier] / datum.marker) * 100), localeCode)}%`, performanceAreaTitles[this.getMatchingRangeIndexForDatapoint(datum)] ]) ] diff --git a/packages/core/src/model/cartesian-charts.ts b/packages/core/src/model/cartesian-charts.ts index d3f4af328b..5f670e132f 100644 --- a/packages/core/src/model/cartesian-charts.ts +++ b/packages/core/src/model/cartesian-charts.ts @@ -55,8 +55,9 @@ export class ChartModelCartesian extends ChartModel { const { groupMapsTo } = options.data const { primaryDomain, primaryRange, secondaryDomain, secondaryRange } = this.assignRangeAndDomains() + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') - const headers = [ + const headers = [ 'Group', primaryDomain.label, primaryRange.label, @@ -68,20 +69,20 @@ export class ChartModelCartesian extends ChartModel { datum[primaryDomain.identifier] === null ? '–' : datum[primaryDomain.identifier], datum[primaryRange.identifier] === null || isNaN(datum[primaryRange.identifier]) ? '–' - : datum[primaryRange.identifier].toLocaleString(), + : numberFormatter(datum[primaryRange.identifier], localeCode), ...(secondaryDomain ? [ datum[secondaryDomain.identifier] === null ? '–' : datum[secondaryDomain.identifier] - ] + ] : []), ...(secondaryRange ? [ datum[secondaryRange.identifier] === null || isNaN(datum[secondaryRange.identifier]) ? '–' : datum[secondaryRange.identifier] - ] + ] : []) ]) diff --git a/packages/core/src/model/choropleth.ts b/packages/core/src/model/choropleth.ts index 4485d5a226..7b710499b9 100644 --- a/packages/core/src/model/choropleth.ts +++ b/packages/core/src/model/choropleth.ts @@ -68,12 +68,14 @@ export class ChoroplethModel extends ChartModel { */ getTabularDataArray() { const displayData = this.getDisplayData() + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') + const headers = ['Country ID', 'Country Name', 'Value'] const cells = [ ...displayData.map(datum => [ datum['id'] === null ? '–' : datum['id'], datum['name'], - datum['value'] + numberFormatter(datum['value'], localeCode) ]) ] diff --git a/packages/core/src/model/circle-pack.ts b/packages/core/src/model/circle-pack.ts index bbc6267781..5ba55f1d03 100644 --- a/packages/core/src/model/circle-pack.ts +++ b/packages/core/src/model/circle-pack.ts @@ -127,6 +127,7 @@ export class CirclePackChartModel extends ChartModel { getTabularDataArray() { const displayData = this.getDisplayData() + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') const headers = ['Child', 'Parent', 'Value'] const cells = [] @@ -137,7 +138,7 @@ export class CirclePackChartModel extends ChartModel { // Call recursive function value += this.getChildrenDatums(datum.children, datum.name, cells, 0) } - cells.push(['–', datum.name, value]) + cells.push(['–', datum.name, numberFormatter(value, localeCode)]) }) return super.formatTable({ headers, cells }) @@ -153,6 +154,7 @@ export class CirclePackChartModel extends ChartModel { */ private getChildrenDatums(children: any, parent: any, result: string[][] = [], totalSum = 0) { const grandParent = parent + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') children.forEach((child: any) => { const parentWithinIteration = child.name @@ -165,7 +167,7 @@ export class CirclePackChartModel extends ChartModel { } sum += this.getChildrenDatums(child.children, parentWithinIteration, result, sum) - result.push([parentWithinIteration, grandParent, sum]) + result.push([parentWithinIteration, grandParent, numberFormatter(sum, localeCode)]) totalSum += sum } } else { @@ -174,7 +176,7 @@ export class CirclePackChartModel extends ChartModel { value = child.value totalSum += child.value } - result.push([child.name, grandParent, value]) + result.push([child.name, grandParent, numberFormatter(value, localeCode)]) } }) diff --git a/packages/core/src/model/gauge.ts b/packages/core/src/model/gauge.ts index b82e5d8f44..f820ccde19 100644 --- a/packages/core/src/model/gauge.ts +++ b/packages/core/src/model/gauge.ts @@ -1,4 +1,5 @@ import { ChartModel } from './model' +import { getProperty } from '@/tools' /** * The gauge chart model layer @@ -16,11 +17,13 @@ export class GaugeChartModel extends ChartModel { const displayData = this.getDisplayData() const options = this.getOptions() const { groupMapsTo } = options.data + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') + const headers = ['Group', 'Value'] const cells = [ ...displayData.map((datum: any) => [ datum[groupMapsTo], - datum['value'] === null ? '–' : datum['value'].toLocaleString() + datum['value'] === null ? '–' : numberFormatter(datum['value'], localeCode) ]) ] diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 1635c55609..b476a47fd3 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -236,10 +236,10 @@ export class HeatmapModel extends ChartModelCartesian { */ getTabularDataArray() { const displayData = this.getDisplayData() - const { primaryDomain, primaryRange } = this.assignRangeAndDomains() - + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') let domainValueFormatter: any + const headers = [primaryDomain.label, primaryRange.label, 'Value'] const cells = [ ...displayData.map((datum: any) => [ @@ -249,10 +249,8 @@ export class HeatmapModel extends ChartModelCartesian { ? domainValueFormatter(datum[primaryDomain.identifier]) : datum[primaryDomain.identifier], - datum[primaryRange.identifier] === null - ? '–' - : datum[primaryRange.identifier].toLocaleString(), - datum['value'] + datum[primaryRange.identifier] === null ? '–' : datum[primaryRange.identifier], + numberFormatter(datum['value'], localeCode) ]) ] diff --git a/packages/core/src/model/meter.ts b/packages/core/src/model/meter.ts index c22124cb6e..d234935a8a 100644 --- a/packages/core/src/model/meter.ts +++ b/packages/core/src/model/meter.ts @@ -73,6 +73,8 @@ export class MeterChartModel extends ChartModel { const { groupMapsTo } = options.data const status = this.getStatus() const proportional = getProperty(options, 'meter', 'proportional') + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') + let headers = [] let cells: ChartTabularData = [] let domainMax: number @@ -81,17 +83,27 @@ export class MeterChartModel extends ChartModel { domainMax = 100 const datum = displayData[0] headers = ['Group', 'Value', ...(status ? ['Status'] : [])] - cells = [[datum[groupMapsTo], datum['value'], ...(status ? [status] : [])]] + cells = [ + [ + datum[groupMapsTo], + numberFormatter(datum['value'], localeCode), + ...(status ? [status] : []) + ] + ] } else { const total = getProperty(proportional, 'total') domainMax = total ? total : this.getMaximumDomain(displayData) headers = ['Group', 'Value', 'Percentage of total'] cells = [ - ...displayData.map((datum: any) => [ - datum[groupMapsTo], - datum['value'], - ((datum['value'] / domainMax) * 100).toFixed(2) + ' %' - ]) + ...displayData.map((datum: any) => { + const value = datum['value'] + const percentValue = Number(((datum['value'] / domainMax) * 100).toFixed(2)) + return [ + datum[groupMapsTo], + numberFormatter(value, localeCode), + numberFormatter(percentValue, localeCode) + ' %' + ] + }) ] } diff --git a/packages/core/src/model/model.ts b/packages/core/src/model/model.ts index 50e2855a08..089c2b6323 100644 --- a/packages/core/src/model/model.ts +++ b/packages/core/src/model/model.ts @@ -1,4 +1,3 @@ -import { format } from 'date-fns' import { bin as d3Bin, scaleOrdinal, stack, stackOffsetDiverging } from 'd3' import { cloneDeep, fromPairs, groupBy, merge, uniq } from 'lodash-es' import { getProperty, updateLegendAdditionalItems } from '@/tools' @@ -57,6 +56,11 @@ export class ChartModel { formatTable({ headers, cells }) { const options = this.getOptions() + const { + code: localeCode, + date: dateFormatter, + number: numberFormatter + } = getProperty(options, 'locale') const tableHeadingFormatter = getProperty(options, 'tabularRepModal', 'tableHeadingFormatter') const tableCellFormatter = getProperty(options, 'tabularRepModal', 'tableCellFormatter') const { cartesianScales } = this.services @@ -64,8 +68,10 @@ export class ChartModel { let domainValueFormatter: any if (domainScaleType === ScaleTypes.TIME) { - domainValueFormatter = (d: any) => format(d, 'MMM d, yyyy') + domainValueFormatter = (d: any) => + dateFormatter(d, localeCode, { month: 'short', day: 'numeric', year: 'numeric' }) } + const result = [ typeof tableHeadingFormatter === 'function' ? tableHeadingFormatter(headers) : headers, ...(typeof tableCellFormatter === 'function' @@ -74,6 +80,12 @@ export class ChartModel { if (domainValueFormatter) { data[1] = domainValueFormatter(data[1]) as string } + for (let i in data) { + let val = data[i] + if (typeof val === 'number') { + data[i] = numberFormatter(val, localeCode) + } + } return data })) ] diff --git a/packages/core/src/model/pie.ts b/packages/core/src/model/pie.ts index 425a62249b..3bc1064cf6 100644 --- a/packages/core/src/model/pie.ts +++ b/packages/core/src/model/pie.ts @@ -1,4 +1,5 @@ import { ChartModel } from './model' +import { getProperty } from '@/tools' /** The charting model layer which includes mainly the chart data and options, * as well as some misc. information to be shared among components */ @@ -29,12 +30,13 @@ export class PieChartModel extends ChartModel { const options = this.getOptions() const { groupMapsTo } = options.data const { valueMapsTo } = options.pie + const { number: numberFormatter, code: localeCode } = getProperty(options, 'locale') const headers = ['Group', 'Value'] const cells = [ ...displayData.map((datum: any) => [ datum[groupMapsTo], - datum[valueMapsTo] === null ? '–' : datum[valueMapsTo].toLocaleString() + datum[valueMapsTo] === null ? '–' : numberFormatter(datum[valueMapsTo], localeCode) ]) ] diff --git a/packages/core/src/model/radar.ts b/packages/core/src/model/radar.ts index 2ae2e69687..3ebdb83c3b 100644 --- a/packages/core/src/model/radar.ts +++ b/packages/core/src/model/radar.ts @@ -10,10 +10,9 @@ export class RadarChartModel extends ChartModelCartesian { getTabularDataArray() { const options = this.getOptions() - const groupedData = this.getGroupedData() - const { angle, value } = getProperty(options, 'radar', 'axes') + const { number: numberFormatter, code: localeCode } = getProperty(options, 'locale') const additionalHeaders = getProperty(groupedData, '0', 'data').map((d: any) => d[angle]) const headers = ['Group', ...additionalHeaders] @@ -23,7 +22,7 @@ export class RadarChartModel extends ChartModelCartesian { datum['name'], ...additionalHeaders.map((_: any, i: number) => getProperty(datum, 'data', i, value) !== null - ? getProperty(datum, 'data', i, value).toLocaleString() + ? numberFormatter(getProperty(datum, 'data', i, value), localeCode) : '–' ) ] diff --git a/packages/core/src/model/treemap.ts b/packages/core/src/model/treemap.ts index b99f678519..5a408f7844 100644 --- a/packages/core/src/model/treemap.ts +++ b/packages/core/src/model/treemap.ts @@ -11,16 +11,17 @@ export class TreemapChartModel extends ChartModel { getTabularDataArray() { const displayData = this.getDisplayData() + const { number: numberFormatter, code: localeCode } = getProperty(this.getOptions(), 'locale') const headers = ['Child', 'Group', 'Value'] const cells = [] displayData.forEach((datum: any) => { if (Array.isArray(datum.children)) { datum.children.forEach((child: any) => { - cells.push([child.name, datum.name, child.value]) + cells.push([child.name, datum.name, numberFormatter(child.value, localeCode)]) }) } else if (getProperty(datum.name) !== null && getProperty(datum.value)) { - cells.push(['–', datum.name, datum.value]) + cells.push(['–', datum.name, numberFormatter(datum.value, localeCode)]) } }) diff --git a/packages/core/src/model/wordcloud.ts b/packages/core/src/model/wordcloud.ts index e3f984b435..b1da778a63 100644 --- a/packages/core/src/model/wordcloud.ts +++ b/packages/core/src/model/wordcloud.ts @@ -1,3 +1,4 @@ +import { getProperty } from '@/tools' import { ChartModel } from './model' /** The charting model layer which includes mainly the chart data and options, @@ -9,16 +10,17 @@ export class WordCloudModel extends ChartModel { getTabularDataArray() { const displayData = this.getDisplayData() - const options = this.getOptions() const { fontSizeMapsTo, wordMapsTo } = options.wordCloud const { groupMapsTo } = options.data + const { code: localeCode, number: numberFormatter } = getProperty(options, 'locale') + const headers = [options.tooltip.wordLabel, 'Group', options.tooltip.valueLabel] const cells = [ ...displayData.map((datum: any) => [ datum[wordMapsTo], datum[groupMapsTo], - datum[fontSizeMapsTo] + numberFormatter(datum[fontSizeMapsTo], localeCode) ]) ] diff --git a/packages/core/src/services/time-series.ts b/packages/core/src/services/time-series.ts index df6f0679fa..f2ab9545a0 100644 --- a/packages/core/src/services/time-series.ts +++ b/packages/core/src/services/time-series.ts @@ -1,6 +1,6 @@ import { min } from 'd3' import { format } from 'date-fns/format' - +import { Locale } from '../interfaces/components' import { getProperty } from '@/tools' import { TimeIntervalFormats, TimeIntervalNames, TimeScaleOptions } from '@/interfaces/axis-scales' @@ -84,7 +84,8 @@ export function formatTick( i: number, allTicks: Array, interval: string, - timeScaleOptions: TimeScaleOptions + timeScaleOptions: TimeScaleOptions, + localeOptions?: Locale ): string { const showDayName = timeScaleOptions.showDayName const intervalConsideringAlsoShowDayNameOption = @@ -95,7 +96,8 @@ export function formatTick( ] const primary = getProperty(formats, 'primary') const secondary = getProperty(formats, 'secondary') - let formatString = isTickPrimary(tick, i, allTicks, interval, showDayName) ? primary : secondary + const primaryTickFlag = isTickPrimary(tick, i, allTicks, interval, showDayName) + let formatString = primaryTickFlag ? primary : secondary // if the interval, and the timestamp includes milliseconds value if (interval === '15seconds' && date.getMilliseconds() !== 0) { @@ -104,8 +106,25 @@ export function formatTick( } const locale = timeScaleOptions.localeObject - - return format(date, formatString, { locale }) + const { code: localeCode, optionsObject } = localeOptions + const formatterType = optionsObject[interval]['type'] + const formatterOptions = + optionsObject[interval][primaryTickFlag ? 'primary' : 'secondary'][formatString] + + if (interval === 'quarterly' || !formatterOptions) { + const formattedDate = format(date, formatString, { locale }) + const formatArr = formattedDate.split('').map(val => { + let num = Number(val) + if (val !== ' ' && !Number.isNaN(num)) { + return num.toLocaleString(localeCode) + } else { + return val + } + }) + return localeOptions[formatterType](date, localeCode, {}, formatArr.join('')) + } else { + return localeOptions[formatterType](date, localeCode, formatterOptions) + } } // Given a timestamp, return an object of useful time formats