Skip to content

Commit

Permalink
Added custom number formatting to better format the numbers on the ch…
Browse files Browse the repository at this point in the history
…art (#5104)

Co-authored-by: Yohann Paris <[email protected]>
  • Loading branch information
jryu01 and YohannParis authored Oct 10, 2024
1 parent 54325de commit 59ae904
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 19 deletions.
39 changes: 32 additions & 7 deletions packages/client/hmi-client/src/components/widgets/VegaChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,39 @@
</template>

<script setup lang="ts">
import embed, { Result, VisualizationSpec } from 'vega-embed';
import { Config as VgConfig } from 'vega';
import { Config as VlConfig } from 'vega-lite';
import { format } from 'd3';
import embed, { Config, Result, VisualizationSpec } from 'vega-embed';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import { countDigits, fixPrecisionError } from '@/utils/number';
import { ref, watch, toRaw, isRef, isReactive, isProxy, computed, h, render } from 'vue';
export type Config = VgConfig | VlConfig;
const NUMBER_FORMAT = '.3~s';
// Define the custom expression functions that can be registered and used in the Vega charts
const expressionFunctions = {
// chartNumberFormatter is a custom number format that will display numbers in a more readable format
chartNumberFormatter: (value: number) => {
const correctedValue = fixPrecisionError(value);
if (value > -1 && value < 1) {
return countDigits(correctedValue) > 6 ? correctedValue.toExponential(3) : correctedValue.toString();
}
return format(NUMBER_FORMAT)(correctedValue);
},
// Just show full value in tooltip
tooltipFormatter: (value) => fixPrecisionError(value)
};
// This config is default for all charts, but can be overridden by individual chart spec
const defaultChartConfig: Partial<Config> = {
customFormatTypes: true,
numberFormatType: 'chartNumberFormatter',
numberFormat: 'chartNumberFormatter',
tooltipFormat: {
numberFormat: 'tooltipFormatter',
numberFormatType: 'tooltipFormatter'
}
};
const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -138,8 +162,9 @@ async function createVegaVisualization(
container,
{ ...visualizationSpec },
{
config: config || {},
actions: options.actions === false ? false : undefined
config: { ...defaultChartConfig, ...config } as Config,
actions: options.actions === false ? false : undefined,
expressionFunctions // Register expression functions
}
);
props.intervalSelectionSignalNames.forEach((signalName) => {
Expand Down
7 changes: 0 additions & 7 deletions packages/client/hmi-client/src/services/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ const VEGALITE_SCHEMA = 'https://vega.github.io/schema/vega-lite/v5.json';

export const CATEGORICAL_SCHEME = ['#1B8073', '#6495E8', '#8F69B9', '#D67DBF', '#E18547', '#D2C446', '#84594D'];

export const NUMBER_FORMAT = '.3~s';
export const LABEL_EXPR = `
datum.value > -1 && datum.value < 1 ? format(datum.value, '.3~f') : format(datum.value, '${NUMBER_FORMAT}')
`;

interface BaseChartOptions {
title?: string;
width: number;
Expand Down Expand Up @@ -370,7 +365,6 @@ export function createForecastChart(
};
const yaxis = structuredClone(xaxis);
yaxis.title = options.yAxisTitle;
yaxis.labelExpr = LABEL_EXPR;

const translationMap = options.translationMap;
let labelExpr = '';
Expand Down Expand Up @@ -626,7 +620,6 @@ export function createSuccessCriteriaChart(
};
const yaxis = structuredClone(xaxis);
yaxis.title = options.yAxisTitle;
yaxis.labelExpr = LABEL_EXPR;

return {
$schema: VEGALITE_SCHEMA,
Expand Down
30 changes: 25 additions & 5 deletions packages/client/hmi-client/src/utils/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,37 @@ export function nistToNumber(numStr: string): number {
* @returns {string} The number in either exponential form or NIST form.
*/
export function displayNumber(num: string): string {
const number = fixPrecisionError(parseFloat(num));
if (countDigits(number) > 6) return number.toExponential(3);
return numberToNist(number.toString());
}

/**
* Counts the number of digits in a given number.
*/
export function countDigits(num: number): number {
if (Number.isNaN(num)) return 0;
let digitString = num.toString();
// Remove negative sign if present
let digitString = parseFloat(num).toString();
if (num.startsWith('-')) {
if (digitString.startsWith('-')) {
digitString = digitString.substring(1);
}

// Remove decimal point if present
digitString = digitString.replace('.', '');
return digitString.length;
}

if (digitString.length > 6) return parseFloat(num).toExponential(3);
return numberToNist(parseFloat(num).toString());
/**
* Fixes floating-point precision errors by rounding the number to a specified precision.
* JavaScript's floating-point arithmetic can introduce small errors (e.g., 0.1 + 0.2 = 0.30000000000000004).
* This causes issues like 0.30000000000000004 ends up as a long number with insignificant trailing digits or '3.000e-1' (with exponential formatting) instead of '0.3'.
* This function rounds the number to mitigate such errors (e.g., fixFloatingPrecisionError(0.1 + 0.2) = 0.3).
* For num > 0, the function rounds to 3 decimal places.
*
* !! Note: Only use this function for display or formatting purposes. Do not use it for calculations. !!
*/
export function fixPrecisionError(num: number): number {
return num < 1 && num > -1 ? Number(num.toFixed(15)) : Number(num.toFixed(3));
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/client/hmi-client/tests/unit/utils/number.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,18 @@ describe('number util tests', () => {
expect(displayNumber('abc')).to.eq('');
expect(displayNumber('1.23abc')).to.eq('1.23');
});

it('should correct floating point precision errors', () => {
const num = 0.1 + 0.2; // 0.30000000000000004
expect(displayNumber(num.toString())).to.eq('0.3');
expect(displayNumber((-num).toString())).to.eq('-0.3');

// 100.19999999999999
expect(displayNumber((100.1 + 0.1).toString())).to.eq('100.2');

// normal rounding
expect(displayNumber('-134.1234500001')).to.eq('-134.123');
expect(displayNumber('0.000001234500001')).to.eq('1.235e-6');
});
});
});

0 comments on commit 59ae904

Please sign in to comment.