Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: convert EUR/MWh to EUR/KWh #7023

Merged
merged 4 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions web/src/features/charts/bar-breakdown/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Mode } from 'utils/constants';

import {
convertPrice,
ExchangeDataType,
getDataBlockPositions,
getElectricityProductionValue,
Expand Down Expand Up @@ -461,3 +462,35 @@ describe('getExchangeCo2Intensity', () => {
});
});
});

describe('convertPrice', () => {
it('converts EUR to price/KWh', () => {
const result = convertPrice(120, 'EUR');
expect(result).to.deep.eq({ value: 0.12, currency: 'EUR', unit: 'kWh' });
});

it('dont convert USD to price/KWh', () => {
const result = convertPrice(120, 'USD');
expect(result).to.deep.eq({ value: 120, currency: 'USD', unit: 'MWh' });
});

it('handles missing currency', () => {
const result = convertPrice(120, undefined);
expect(result).to.deep.eq({ value: 120, currency: undefined, unit: 'MWh' });
});

it('handles missing price with EUR', () => {
const result = convertPrice(undefined, 'EUR');
expect(result).to.deep.eq({ value: undefined, currency: 'EUR', unit: 'kWh' });
});

it('handles missing price without EUR', () => {
const result = convertPrice(undefined, 'USD');
expect(result).to.deep.eq({ value: undefined, currency: 'USD', unit: 'MWh' });
});

it('handles missing price and currency', () => {
const result = convertPrice(undefined, undefined);
expect(result).to.deep.eq({ value: undefined, currency: undefined, unit: 'MWh' });
});
});
22 changes: 21 additions & 1 deletion web/src/features/charts/bar-breakdown/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
ZoneKey,
} from 'types';
import { Mode, modeOrderBarBreakdown } from 'utils/constants';
import { getProductionCo2Intensity } from 'utils/helpers';
import { getProductionCo2Intensity, round } from 'utils/helpers';
import { EnergyUnits } from 'utils/units';

import exchangesToExclude from '../../../../config/excluded_aggregated_exchanges.json';

Expand Down Expand Up @@ -192,3 +193,22 @@ export const getExchangesToDisplay = (
(exchangeZoneKey) => !exchangeZoneKeysToRemove.has(exchangeZoneKey)
);
};

/**
* Convents the price value and unit to the correct value and unit for the matching currency.
*
* If no currency is provided, the parameters are returned as is.
*/
export const convertPrice = (
value?: number,
currency?: string,
unit: EnergyUnits = EnergyUnits.MEGAWATT_HOURS
): { value?: number; currency?: string; unit: EnergyUnits } => {
if (currency === 'EUR') {
if (value) {
value = round(value / 1000, 4);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be 2dp as a cent is the most granular unit of the currencies we use

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some research and testing and the problem with 2 decimal points is that we loose a lot of information.

For example if the price is 0,56 EUR per MWh it would be 0,0006 EUR per kWh if using 4 decimals but 0.01 EUR per kWh if we used 2 decimals.

The rounding to 4 decimals causes an error of 0.6 if converted back to per MWh but if the same is done with 2 decimals the error would be 9.44, which is very bad.

Other sites that show the price per kWh shows it with 4 decimals as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alportan with the above in mind you to think it makes sense to have a toggle in the settings to show prices as per MWh or per kWh?

I would assume some energy experts and the like want to have the most accurate value all the time, even if it's per MWh?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good point! I think a toggle in the settings does make a lot of sense. Should I sketch something? 🙌

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good point! I think a toggle in the settings does make a lot of sense. Should I sketch something? 🙌

If you wish!

I'm thinking that maybe we should try and move stuff into a settings modal next cycle. Even if it's just the current stuff and then we can expand from there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy for this to go through as it is if you're happy with the 4dp

}
return { value: value, currency, unit: EnergyUnits.KILOWATT_HOURS };
}
return { value, currency, unit };
};
20 changes: 13 additions & 7 deletions web/src/features/charts/hooks/usePriceChartData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import useGetZone from 'api/getZone';
import getSymbolFromCurrency from 'currency-symbol-map';
import { max as d3Max, min as d3Min } from 'd3-array';
import { scaleLinear } from 'd3-scale';
import { EnergyUnits } from 'utils/units';

import { convertPrice } from '../bar-breakdown/utils';
import { AreaGraphElement } from '../types';

export function getFills(data: AreaGraphElement[]) {
Expand Down Expand Up @@ -32,26 +32,32 @@ export function usePriceChartData() {
return { isLoading, isError };
}

const firstZoneState = Object.values(zoneData.zoneStates)[0].price;
// We assume that if the first element has price disabled, all of them do
const priceDisabledReason = Object.values(zoneData.zoneStates)[0].price?.disabledReason;
const priceDisabledReason = firstZoneState?.disabledReason;

const { currency, unit } = convertPrice(
firstZoneState?.value,
firstZoneState?.currency
);

const chartData: AreaGraphElement[] = [];

for (const [datetimeString, value] of Object.entries(zoneData.zoneStates)) {
const datetime = new Date(datetimeString);
const { value: price } = convertPrice(value.price?.value, value.price?.currency);

chartData.push({
datetime,
layerData: {
price: value.price?.value ?? Number.NaN,
price: price ?? Number.NaN,
},
meta: value,
});
}

const currencySymbol: string = getSymbolFromCurrency(
Object.values(zoneData.zoneStates)[0].price?.currency // Every price has the same currency
);
const valueAxisLabel = `${currencySymbol || '?'} / ${EnergyUnits.MEGAWATT_HOURS}`;
const currencySymbol: string = getSymbolFromCurrency(currency?.toUpperCase());
const valueAxisLabel = `${currencySymbol || '?'} / ${unit}`;

const { layerFill, markerFill } = getFills(chartData);

Expand Down
20 changes: 11 additions & 9 deletions web/src/features/charts/tooltips/PriceChartTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import getSymbolFromCurrency from 'currency-symbol-map';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
import { timeAverageAtom } from 'utils/state/atoms';
import { EnergyUnits } from 'utils/units';

import { convertPrice } from '../bar-breakdown/utils';
import { InnerAreaGraphTooltipProps } from '../types';
import AreaGraphToolTipHeader from './AreaGraphTooltipHeader';

Expand All @@ -14,23 +14,25 @@ export default function PriceChartTooltip({ zoneDetail }: InnerAreaGraphTooltipP
if (!zoneDetail) {
return null;
}
const { price, stateDatetime } = zoneDetail;

const priceIsDefined = typeof price?.value === 'number';
const currency = priceIsDefined ? getSymbolFromCurrency(price?.currency) : '?';
const value = priceIsDefined ? price?.value : '';
const { price: priceObject, stateDatetime } = zoneDetail;
const { value, currency, unit } = convertPrice(
priceObject?.value,
priceObject?.currency
);
const currencySymbol = getSymbolFromCurrency(currency) ?? '?';
const price = value ?? Number.NaN;

return (
<div className="w-full rounded-md bg-white p-3 shadow-xl dark:border dark:border-gray-700 dark:bg-gray-800 sm:w-64">
<AreaGraphToolTipHeader
datetime={new Date(stateDatetime)}
timeAverage={timeAverage}
squareColor="#7f7f7f" // TODO: use price scale color
title={t('tooltips.price')} // TODO: get from translation
title={t('tooltips.price')}
/>
<p className="flex justify-center text-base">
<b className="mr-1">{value}</b>
{currency} / {EnergyUnits.MEGAWATT_HOURS}
<b className="mr-1">{price}</b>
{currencySymbol} / {unit}
</p>
</div>
);
Expand Down
Loading