diff --git a/web/src/features/charts/FuturePrice.stories.ts b/web/src/features/charts/FuturePrice.stories.ts index a9c40460b2..6707ea72a0 100644 --- a/web/src/features/charts/FuturePrice.stories.ts +++ b/web/src/features/charts/FuturePrice.stories.ts @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/react'; -import { halfHourPriceData, priceData } from 'stories/mockData'; +import { halfHourPriceData, priceData, priceData2 } from 'stories/mockData'; import { mockDateDecorator } from 'storybook-mock-date-decorator'; import { FuturePrice } from './FuturePrice'; @@ -25,8 +25,10 @@ export const PositivePrices: Story = { const negativePriceData = JSON.parse(JSON.stringify(priceData)); negativePriceData.priceData['2024-09-02 03:00:00+00:00'] = -0.2; -negativePriceData.priceData['2024-09-02 13:00:00+00:00'] = -0.9; +negativePriceData.priceData['2024-09-02 13:00:00+00:00'] = -1; negativePriceData.priceData['2024-09-02 08:00:00+00:00'] = -0.3; +negativePriceData.priceData['2024-09-02 09:00:00+00:00'] = 0; +negativePriceData.priceData['2024-09-02 10:00:00+00:00'] = -0.0001; export const NegativePrices: Story = { args: { @@ -60,3 +62,12 @@ export const HalfHourPrices: Story = { date: new Date('2024-09-02 01:00:00+00:00'), }, }; + +export const PricesGoingUp: Story = { + args: { + futurePrice: priceData2, + }, + parameters: { + date: new Date('2024-09-01 12:00:00+00:00'), + }, +}; diff --git a/web/src/features/charts/FuturePrice.tsx b/web/src/features/charts/FuturePrice.tsx index 5d62999e48..87b07fd6f3 100644 --- a/web/src/features/charts/FuturePrice.tsx +++ b/web/src/features/charts/FuturePrice.tsx @@ -18,6 +18,7 @@ import { getGranularity, negativeToPostivePercentage, normalizeToGranularity, + priceIn5Percentile, } from './futurePriceUtils'; export function FuturePrice({ futurePrice }: { futurePrice: FuturePriceData | null }) { @@ -44,24 +45,23 @@ export function FuturePrice({ futurePrice }: { futurePrice: FuturePriceData | nu () => calculatePriceBound(filteredPriceData, Math.min, granularity, true), [filteredPriceData, granularity] ); + const hasNegativePrice = minPriceTotal < 0; + const negativePercentage = negativeToPostivePercentage(minPriceTotal, maxPriceTotal); if (!futurePrice || !isFuturePrice(futurePrice)) { return null; } - const hasNegativePrice = minPriceTotal < 0; - const negativePercentage = negativeToPostivePercentage(minPriceTotal, maxPriceTotal); - return (
trackEvent(TrackEvent.FUTURE_PRICE_EXPANDED)} @@ -81,31 +81,33 @@ export function FuturePrice({ futurePrice }: { futurePrice: FuturePriceData | nu : '' } > - {dateIsFirstHourOfTomorrow(new Date(date)) && ( -
- - -
- )} -
- - -
- {hasNegativePrice && ( -
- {price < 0 && ( +
+ {dateIsFirstHourOfTomorrow(new Date(date)) && ( +
+ + +
+ )} +
+ + +
+ {hasNegativePrice && price < 0 && ( +
- )} -
- )} -
- {price > 0 && ( - + )} + {price >= 0 && ( +
+ {price >= 0 && ( + )} - /> +
)}
@@ -172,10 +179,13 @@ export function PriceBar({ maxPrice: number; color: string; }) { + const nonNegativePrice = Math.abs(price); return (
); @@ -214,7 +224,7 @@ function TimeDisplay({ date, granularity }: { date: string; granularity: number if (isNow(date, granularity)) { return ( -

+

{t(`country-panel.price-chart.now`)}

); @@ -225,11 +235,7 @@ function TimeDisplay({ date, granularity }: { date: string; granularity: number minute: '2-digit', }).format(datetime); - return ( -

- {`${t(`country-panel.price-chart.at`)} ${formattedDate}`} -

- ); + return

{formattedDate}

; } function PriceDisclaimer() { @@ -292,10 +298,16 @@ const getColor = ( normalizeToGranularity(new Date(), granularity) ) { return 'bg-price-light dark:bg-price-dark opacity-50'; - } else if (price === maxPrice) { + } else if ( + priceIn5Percentile(price, maxPrice, minPrice, true) && + maxPrice != minPrice + ) { return 'bg-danger dark:bg-red-400'; - } else if (price === minPrice) { - return 'bg-success dark:bg-emerald-500'; + } else if ( + priceIn5Percentile(price, maxPrice, minPrice, false) && + maxPrice != minPrice + ) { + return 'bg-success dark:bg-success-dark'; } else { return 'bg-price-light dark:bg-price-dark'; } diff --git a/web/src/features/charts/RoundedCard.tsx b/web/src/features/charts/RoundedCard.tsx index 20d1420530..3292578a25 100644 --- a/web/src/features/charts/RoundedCard.tsx +++ b/web/src/features/charts/RoundedCard.tsx @@ -8,7 +8,7 @@ export const RoundedCard = forwardRef< return (
{ @@ -85,4 +86,40 @@ describe('FuturePrice Utility Functions', () => { const percentage = negativeToPostivePercentage(minPrice, maxPrice); expect(percentage).to.equal(0); }); + + test('priceIn5Percentile returns true if price is in top 5 percentile', () => { + const price = 95; + const maxPrice = 100; + const minPrice = 0; + const inTop = true; + const result = priceIn5Percentile(price, maxPrice, minPrice, inTop); + expect(result).to.equal(true); + }); + + test('priceIn5Percentile returns false if price is not in top 5 percentile', () => { + const price = 80; + const maxPrice = 100; + const minPrice = 50; + const inTop = true; + const result = priceIn5Percentile(price, maxPrice, minPrice, inTop); + expect(result).to.equal(false); + }); + + test('priceIn5Percentile returns true if price is in bottom 5 percentile', () => { + const price = 52; + const maxPrice = 100; + const minPrice = 50; + const inTop = false; + const result = priceIn5Percentile(price, maxPrice, minPrice, inTop); + expect(result).to.equal(true); + }); + + test('priceIn5Percentile returns false if price is not in bottom 5 percentile', () => { + const price = 60; + const maxPrice = 100; + const minPrice = 50; + const inTop = false; + const result = priceIn5Percentile(price, maxPrice, minPrice, inTop); + expect(result).to.equal(false); + }); }); diff --git a/web/src/features/charts/futurePriceUtils.ts b/web/src/features/charts/futurePriceUtils.ts index 54890aaa30..f781b3ea0b 100644 --- a/web/src/features/charts/futurePriceUtils.ts +++ b/web/src/features/charts/futurePriceUtils.ts @@ -72,3 +72,18 @@ export const negativeToPostivePercentage = ( return Math.round(Math.abs((minPrice / (maxPrice + Math.abs(minPrice))) * 100)); }; + +export const priceIn5Percentile = ( + price: number, + maxPrice: number, + minPrice: number, + inTop: boolean +): boolean => { + const fivePercent = 0.05; + const priceRange = maxPrice - minPrice; + + if (inTop) { + return price >= maxPrice - priceRange * fivePercent; + } + return price <= minPrice + priceRange * fivePercent; +}; diff --git a/web/src/stories/mockData.ts b/web/src/stories/mockData.ts index 0f9ffe31b3..fbf4c24366 100644 --- a/web/src/stories/mockData.ts +++ b/web/src/stories/mockData.ts @@ -125,13 +125,53 @@ export const zoneStateMock: StateZoneData = { e: 0, }; +export const priceData2 = { + entryCount: 24, + priceData: { + '2024-09-01 09:00:00+00:00': 0, + '2024-09-01 10:00:00+00:00': 1, + '2024-09-01 11:00:00+00:00': 2, + '2024-09-01 12:00:00+00:00': 3, + '2024-09-01 13:00:00+00:00': 4, + '2024-09-01 14:00:00+00:00': 5, + '2024-09-01 15:00:00+00:00': 6, + '2024-09-01 16:00:00+00:00': 7, + '2024-09-01 17:00:00+00:00': 8, + '2024-09-01 18:00:00+00:00': 9, + '2024-09-01 19:00:00+00:00': 10, + '2024-09-01 20:00:00+00:00': 11, + '2024-09-01 21:00:00+00:00': 12, + '2024-09-01 22:00:00+00:00': 13, + '2024-09-01 23:00:00+00:00': 14, + '2024-09-02 00:00:00+00:00': 15, + '2024-09-02 01:00:00+00:00': 16, + '2024-09-02 02:00:00+00:00': 17, + '2024-09-02 03:00:00+00:00': 18, + '2024-09-02 04:00:00+00:00': 19, + '2024-09-02 05:00:00+00:00': 20, + '2024-09-02 06:00:00+00:00': 21, + '2024-09-02 07:00:00+00:00': 22, + '2024-09-02 08:00:00+00:00': 23, + '2024-09-02 09:00:00+00:00': 24, + '2024-09-02 10:00:00+00:00': 25, + '2024-09-02 11:00:00+00:00': 26, + '2024-09-02 12:00:00+00:00': 27, + '2024-09-02 13:00:00+00:00': 28, + '2024-09-02 14:00:00+00:00': 29, + '2024-09-02 15:00:00+00:00': 30, + }, + currency: 'EUR', + source: 'nordpool.com', + zoneKey: 'DE', +}; + export const priceData = { entryCount: 24, priceData: { '2024-09-01 22:00:00+00:00': 25, '2024-09-01 23:00:00+00:00': 15, '2024-09-02 00:00:00+00:00': 12, - '2024-09-02 01:00:00+00:00': 8, + '2024-09-02 01:00:00+00:00': 40, '2024-09-02 02:00:00+00:00': 21, '2024-09-02 03:00:00+00:00': 16, '2024-09-02 04:00:00+00:00': 19, @@ -145,7 +185,7 @@ export const priceData = { '2024-09-02 12:00:00+00:00': 26, '2024-09-02 13:00:00+00:00': 14, '2024-09-02 14:00:00+00:00': 23, - '2024-09-02 15:00:00+00:00': 30, + '2024-09-02 15:00:00+00:00': 29, '2024-09-02 16:00:00+00:00': 17, '2024-09-02 17:00:00+00:00': 11, '2024-09-02 18:00:00+00:00': 22, @@ -168,8 +208,8 @@ export const halfHourPriceData = { '2024-09-01 22:00:00+00:00': 25, '2024-09-01 22:30:00+00:00': 15, '2024-09-01 23:00:00+00:00': 12, - '2024-09-01 23:30:00+00:00': 28, - '2024-09-02 00:00:00+00:00': 21, + '2024-09-01 23:30:00+00:00': -20, + '2024-09-02 00:00:00+00:00': 0, '2024-09-02 00:30:00+00:00': -5, '2024-09-02 01:00:00+00:00': 19, '2024-09-02 01:30:00+00:00': 24, @@ -227,7 +267,7 @@ export const halfHourPriceData = { '2024-09-03 03:30:00+00:00': 24, '2024-09-03 04:00:00+00:00': 27, '2024-09-03 04:30:00+00:00': 22, - '2024-09-03 05:00:00+00:00': -10, + '2024-09-03 05:00:00+00:00': -40, '2024-09-03 05:30:00+00:00': 29, '2024-09-03 06:00:00+00:00': 18, '2024-09-03 06:30:00+00:00': 20, diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs index 77eb8410d7..4d35675dc4 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.cjs @@ -66,6 +66,7 @@ const config = { dark: colors.red[400], }, }, + minWidth: { 18: '4.5rem' }, }, fontFamily: { sans: ['Inter', ...defaultConfig.theme.fontFamily.sans],