@oanda/labs-crowd-view-widget 1.0.44 → 1.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/main/CrowdViewWidget/Main.js +17 -6
- package/dist/main/CrowdViewWidget/Main.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/Chart.js +6 -10
- package/dist/main/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js +33 -18
- package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/types.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js +23 -9
- package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js +39 -2
- package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
- package/dist/main/CrowdViewWidget/constants.js +5 -4
- package/dist/main/CrowdViewWidget/constants.js.map +1 -1
- package/dist/main/translations/sources/en.json +24 -0
- package/dist/module/CrowdViewWidget/Main.js +18 -7
- package/dist/module/CrowdViewWidget/Main.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/Chart.js +7 -11
- package/dist/module/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js +34 -19
- package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/types.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js +23 -9
- package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js +36 -1
- package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
- package/dist/module/CrowdViewWidget/constants.js +5 -4
- package/dist/module/CrowdViewWidget/constants.js.map +1 -1
- package/dist/module/translations/sources/en.json +24 -0
- package/dist/types/CrowdViewWidget/components/Chart/types.d.ts +6 -2
- package/dist/types/CrowdViewWidget/components/Chart/utils/chartUtils.d.ts +5 -0
- package/dist/types/CrowdViewWidget/constants.d.ts +4 -3
- package/lokalise.config.json +1 -1
- package/package.json +3 -3
- package/src/CrowdViewWidget/Main.tsx +23 -7
- package/src/CrowdViewWidget/components/Chart/Chart.tsx +6 -12
- package/src/CrowdViewWidget/components/Chart/chartOptions.ts +41 -17
- package/src/CrowdViewWidget/components/Chart/types.ts +6 -2
- package/src/CrowdViewWidget/components/Chart/useCrowdViewData.ts +34 -9
- package/src/CrowdViewWidget/components/Chart/utils/chartUtils.ts +69 -1
- package/src/CrowdViewWidget/constants.ts +4 -3
- package/src/translations/sources/en.json +24 -0
- package/test/Main.test.tsx +72 -26
- package/test/components/Chart/utils/chartUtils.test.ts +172 -0
|
@@ -83,6 +83,25 @@ const processOrderPositionBooks = (
|
|
|
83
83
|
return [];
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
return orderPositionData.orderPositionBooks
|
|
87
|
+
.filter((book): book is NonNullable<typeof book> => {
|
|
88
|
+
return book !== null && book.buckets?.length > 0;
|
|
89
|
+
})
|
|
90
|
+
.map((book, index) => {
|
|
91
|
+
const candle = candleMap.get(book.time);
|
|
92
|
+
const price = candle?.high ?? null;
|
|
93
|
+
|
|
94
|
+
return [book.time, price, index] as [string, number, number];
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const processBuckets = (
|
|
99
|
+
orderPositionData: GetOrderPositionBooksQuery | undefined
|
|
100
|
+
) => {
|
|
101
|
+
if (!orderPositionData?.orderPositionBooks?.length) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
return orderPositionData.orderPositionBooks
|
|
87
106
|
.filter((book): book is NonNullable<typeof book> => {
|
|
88
107
|
return book !== null && book.buckets?.length > 0;
|
|
@@ -105,14 +124,7 @@ const processOrderPositionBooks = (
|
|
|
105
124
|
sentiment: bucket!.sentiment!,
|
|
106
125
|
}));
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
const price = candle?.high ?? null;
|
|
110
|
-
|
|
111
|
-
return [book.time, price, JSON.stringify(filteredBuckets)] as [
|
|
112
|
-
string,
|
|
113
|
-
number,
|
|
114
|
-
string,
|
|
115
|
-
];
|
|
127
|
+
return filteredBuckets;
|
|
116
128
|
});
|
|
117
129
|
};
|
|
118
130
|
|
|
@@ -212,6 +224,11 @@ export const useCrowdViewData = ({
|
|
|
212
224
|
[orderPositionData, candleMap]
|
|
213
225
|
);
|
|
214
226
|
|
|
227
|
+
const buckets = useMemo(
|
|
228
|
+
() => processBuckets(orderPositionData),
|
|
229
|
+
[orderPositionData]
|
|
230
|
+
);
|
|
231
|
+
|
|
215
232
|
const error = useMemo((): Error | null => {
|
|
216
233
|
if (priceCandlesError) {
|
|
217
234
|
return new Error(`Price candles error: ${priceCandlesError.message}`);
|
|
@@ -248,12 +265,20 @@ export const useCrowdViewData = ({
|
|
|
248
265
|
);
|
|
249
266
|
|
|
250
267
|
return {
|
|
268
|
+
buckets,
|
|
251
269
|
xAxisData,
|
|
252
270
|
candlesSeriesData,
|
|
253
271
|
orderPositionBooks,
|
|
254
272
|
bucketWidth: orderPositionData.orderPositionBooks?.[0]?.bucketWidth || 0,
|
|
255
273
|
};
|
|
256
|
-
}, [
|
|
274
|
+
}, [
|
|
275
|
+
priceCandlesData,
|
|
276
|
+
orderPositionData,
|
|
277
|
+
error,
|
|
278
|
+
candles,
|
|
279
|
+
orderPositionBooks,
|
|
280
|
+
buckets,
|
|
281
|
+
]);
|
|
257
282
|
|
|
258
283
|
return {
|
|
259
284
|
data,
|
|
@@ -28,7 +28,7 @@ export const getLabelData = ({
|
|
|
28
28
|
})
|
|
29
29
|
.map((item) => ({
|
|
30
30
|
name: new Date(item).toLocaleDateString(undefined, {
|
|
31
|
-
month: 'short',
|
|
31
|
+
month: isGreaterThanTwoWeeks ? 'short' : 'long',
|
|
32
32
|
day: isGreaterThanTwoWeeks ? 'numeric' : undefined,
|
|
33
33
|
}),
|
|
34
34
|
xAxis: item,
|
|
@@ -91,3 +91,71 @@ export const getRectColor = (sentiment: number) =>
|
|
|
91
91
|
sentiment < 0
|
|
92
92
|
? getGradientColor(sentiment * -1, COLOR_MAP.short[0], COLOR_MAP.short[1])
|
|
93
93
|
: getGradientColor(sentiment, COLOR_MAP.long[0], COLOR_MAP.long[1]);
|
|
94
|
+
|
|
95
|
+
export const getTooltipFormatter = (
|
|
96
|
+
params: unknown,
|
|
97
|
+
buckets: Array<Array<{ price: number; sentiment: number }>>,
|
|
98
|
+
bucketWidth: number,
|
|
99
|
+
selectedPrice: number,
|
|
100
|
+
labelCallback: (key: string) => string
|
|
101
|
+
) => {
|
|
102
|
+
const arr = params as unknown as Array<Record<string, unknown>>;
|
|
103
|
+
const candleParam = arr[0] as {
|
|
104
|
+
axisValue: string;
|
|
105
|
+
value: [number, number, number, number, number];
|
|
106
|
+
};
|
|
107
|
+
const booksParam = arr[1] as { value: [string, number, number] };
|
|
108
|
+
const time = new Date(candleParam.axisValue as string);
|
|
109
|
+
|
|
110
|
+
const candleData = candleParam.value;
|
|
111
|
+
const booksData = booksParam?.value ?? [];
|
|
112
|
+
const bucketsIndex = booksData[2];
|
|
113
|
+
const selectedBuckets = buckets[bucketsIndex];
|
|
114
|
+
|
|
115
|
+
const matchedBucket = selectedBuckets?.find(
|
|
116
|
+
({ price }) => selectedPrice >= price && selectedPrice < price + bucketWidth
|
|
117
|
+
);
|
|
118
|
+
const showCandles =
|
|
119
|
+
!!candleData[1] && !!candleData[2] && !!candleData[3] && !!candleData[4];
|
|
120
|
+
|
|
121
|
+
return `<p>${time.toLocaleString(undefined, {
|
|
122
|
+
hour: '2-digit',
|
|
123
|
+
minute: '2-digit',
|
|
124
|
+
year: 'numeric',
|
|
125
|
+
day: 'numeric',
|
|
126
|
+
month: 'numeric',
|
|
127
|
+
timeZoneName: 'short',
|
|
128
|
+
})}</p><br />
|
|
129
|
+
${
|
|
130
|
+
showCandles
|
|
131
|
+
? `<p><b>${labelCallback('candle')}:</b></p>
|
|
132
|
+
<p>${labelCallback('open_price')}: ${candleData[1]} </p>
|
|
133
|
+
<p>${labelCallback('close_price')}: ${candleData[2]} </p>
|
|
134
|
+
<p>${labelCallback('low')}: ${candleData[3]} </p>
|
|
135
|
+
<p>${labelCallback('high')}: ${candleData[4]} </p>
|
|
136
|
+
`
|
|
137
|
+
: ''
|
|
138
|
+
}
|
|
139
|
+
${
|
|
140
|
+
matchedBucket
|
|
141
|
+
? `<br /><p><b>${labelCallback('orders')}:</b></p>
|
|
142
|
+
<p>${labelCallback('price_range')}: ${matchedBucket.price} - ${Number(matchedBucket.price + bucketWidth).toFixed(4)} </p>
|
|
143
|
+
<p>${matchedBucket.sentiment < 0 ? labelCallback('sell_advantage') : labelCallback('buy_advantage')}: ${Math.abs(matchedBucket.sentiment)}% </p>`
|
|
144
|
+
: ''
|
|
145
|
+
}`;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const formatXAxisLabel = (
|
|
149
|
+
value: unknown,
|
|
150
|
+
isGreaterThanTwoWeeks: boolean
|
|
151
|
+
) => {
|
|
152
|
+
const date = new Date(value as string);
|
|
153
|
+
return isGreaterThanTwoWeeks
|
|
154
|
+
? `${date.toLocaleTimeString(undefined, {
|
|
155
|
+
hour: '2-digit',
|
|
156
|
+
minute: '2-digit',
|
|
157
|
+
})}`
|
|
158
|
+
: `${CHART_CONFIG.X_AXIS_DATE_PADDING}${date.toLocaleDateString(undefined, {
|
|
159
|
+
day: 'numeric',
|
|
160
|
+
})}${CHART_CONFIG.X_AXIS_DATE_PADDING}`;
|
|
161
|
+
};
|
|
@@ -17,11 +17,12 @@ export const CHART_CONFIG = {
|
|
|
17
17
|
WIDTH: 9999,
|
|
18
18
|
X_LABEL_SIZE: 40,
|
|
19
19
|
Y_LABEL_SIZE_DESKTOP: 60,
|
|
20
|
-
INITIAL_START_ZOOM:
|
|
20
|
+
INITIAL_START_ZOOM: 0,
|
|
21
21
|
INITIAL_END_ZOOM: 100,
|
|
22
|
+
X_AXIS_DATE_PADDING: ' ',
|
|
22
23
|
} as const;
|
|
23
24
|
|
|
24
25
|
export const COLOR_MAP = {
|
|
25
|
-
long: ['#
|
|
26
|
-
short: ['#
|
|
26
|
+
long: ['#fcedca', '#FAB313'],
|
|
27
|
+
short: ['#c8ebfa', '#309DCC'],
|
|
27
28
|
} as const;
|
|
@@ -1,2 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
+
"data_unavailable": "Data unavailable",
|
|
3
|
+
"no_matching_results": "No matching results",
|
|
4
|
+
"pagination_entries_range": "{{firstItemOnPage}}-{{lastItemOnPage}} of {{itemCount}} entries",
|
|
5
|
+
"order_book": "Order book",
|
|
6
|
+
"position_book": "Position book",
|
|
7
|
+
"long": "Long",
|
|
8
|
+
"short": "Short",
|
|
9
|
+
"instrument": "Instrument",
|
|
10
|
+
"granularity": "Granularity",
|
|
11
|
+
"search": "Search",
|
|
12
|
+
"5_minutes": "5 minutes",
|
|
13
|
+
"15_minutes": "15 minutes",
|
|
14
|
+
"1_hour": "1 hour",
|
|
15
|
+
"4_hours": "4 hours",
|
|
16
|
+
"candle": "Candle",
|
|
17
|
+
"open_price": "Open price",
|
|
18
|
+
"close_price": "Close price",
|
|
19
|
+
"low": "Low",
|
|
20
|
+
"high": "High",
|
|
21
|
+
"orders": "Orders",
|
|
22
|
+
"price_range": "Price range",
|
|
23
|
+
"sentiment": "Sentiment",
|
|
24
|
+
"buy_advantage": "Buy advantage",
|
|
25
|
+
"sell_advantage": "Sell advantage"
|
|
2
26
|
}
|
package/test/Main.test.tsx
CHANGED
|
@@ -9,46 +9,92 @@ import React from 'react';
|
|
|
9
9
|
import { Main } from '../src/CrowdViewWidget/Main';
|
|
10
10
|
import { InstrumentId } from '../src/CrowdViewWidget/types/instruments';
|
|
11
11
|
import { getOrderPositionBooks } from '../src/gql/getOrderPositionBooks';
|
|
12
|
-
import {
|
|
12
|
+
import { getPriceCandles } from '../src/gql/getPriceCandles';
|
|
13
|
+
import {
|
|
14
|
+
BookType,
|
|
15
|
+
DataSource,
|
|
16
|
+
Division,
|
|
17
|
+
Granularity,
|
|
18
|
+
TimeSpan,
|
|
19
|
+
} from '../src/gql/types/graphql';
|
|
20
|
+
|
|
21
|
+
const instrument = InstrumentId.EUR_AUD;
|
|
22
|
+
const division = Division.Oap;
|
|
23
|
+
const granularity = Granularity.H4;
|
|
24
|
+
const timeSpan = TimeSpan.NinetyDays;
|
|
25
|
+
|
|
26
|
+
const candles = [
|
|
27
|
+
{
|
|
28
|
+
point: '2024-02-21T10:00:00Z',
|
|
29
|
+
high: 1.334,
|
|
30
|
+
low: 1.33,
|
|
31
|
+
open: 1.331,
|
|
32
|
+
close: 1.333,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
point: '2024-02-21T10:30:00Z',
|
|
36
|
+
high: 1.335,
|
|
37
|
+
low: 1.3295,
|
|
38
|
+
open: 1.33,
|
|
39
|
+
close: 1.334,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
point: '2024-02-21T11:00:00Z',
|
|
43
|
+
high: 1.333,
|
|
44
|
+
low: 1.329,
|
|
45
|
+
open: 1.331,
|
|
46
|
+
close: 1.332,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const maxPrice = 1.335;
|
|
51
|
+
const minPrice = 1.329;
|
|
52
|
+
const bucketWidth = 0.0005;
|
|
53
|
+
const maxBookPrice = maxPrice + bucketWidth * 2;
|
|
54
|
+
const minBookPrice = minPrice - bucketWidth * 2;
|
|
13
55
|
|
|
14
56
|
const mocks = [
|
|
57
|
+
{
|
|
58
|
+
request: {
|
|
59
|
+
query: getPriceCandles,
|
|
60
|
+
variables: {
|
|
61
|
+
dataSource: DataSource.Mt5,
|
|
62
|
+
division,
|
|
63
|
+
instrument,
|
|
64
|
+
granularity,
|
|
65
|
+
timeSpan,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
result: {
|
|
69
|
+
data: {
|
|
70
|
+
priceCandles: {
|
|
71
|
+
candle: candles,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
15
76
|
{
|
|
16
77
|
request: {
|
|
17
78
|
query: getOrderPositionBooks,
|
|
18
79
|
variables: {
|
|
19
|
-
instrument
|
|
80
|
+
instrument,
|
|
20
81
|
bookType: BookType.Order,
|
|
21
|
-
|
|
82
|
+
timeSpan,
|
|
83
|
+
granularity,
|
|
84
|
+
maxBookPrice,
|
|
85
|
+
minBookPrice,
|
|
22
86
|
},
|
|
23
87
|
},
|
|
24
88
|
result: {
|
|
25
89
|
data: {
|
|
26
90
|
orderPositionBooks: [
|
|
27
91
|
{
|
|
28
|
-
bucketWidth
|
|
29
|
-
price:
|
|
92
|
+
bucketWidth,
|
|
93
|
+
price: maxPrice,
|
|
30
94
|
time: '2024-02-21T10:30:00Z',
|
|
31
95
|
buckets: [
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
longCountPercent: 0.0582,
|
|
35
|
-
shortCountPercent: 0.0,
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
price: 0.7,
|
|
39
|
-
longCountPercent: 0.0582,
|
|
40
|
-
shortCountPercent: 0.0,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
price: 0.9925,
|
|
44
|
-
longCountPercent: 0.0582,
|
|
45
|
-
shortCountPercent: 0.0,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
price: 1.0,
|
|
49
|
-
longCountPercent: 0.1163,
|
|
50
|
-
shortCountPercent: 0.0,
|
|
51
|
-
},
|
|
96
|
+
{ price: 1.3345, sentiment: 0.2 },
|
|
97
|
+
{ price: 1.3305, sentiment: -0.3 },
|
|
52
98
|
],
|
|
53
99
|
},
|
|
54
100
|
],
|
|
@@ -62,7 +108,7 @@ describe('Main component', () => {
|
|
|
62
108
|
const { findByTestId } = render(
|
|
63
109
|
<MockedProvider mocks={mocks}>
|
|
64
110
|
<MockLayoutProvider>
|
|
65
|
-
<Main division={
|
|
111
|
+
<Main division={division} />
|
|
66
112
|
</MockLayoutProvider>
|
|
67
113
|
</MockedProvider>
|
|
68
114
|
);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatXAxisLabel,
|
|
3
|
+
getLabelData,
|
|
4
|
+
getRectColor,
|
|
5
|
+
getTimeSpanForGranularity,
|
|
6
|
+
getTooltipFormatter,
|
|
7
|
+
isDifferenceGreaterThanTwoWeeks,
|
|
8
|
+
} from '../../../../src/CrowdViewWidget/components/Chart/utils/chartUtils';
|
|
9
|
+
import {
|
|
10
|
+
BOOKS_THRESHOLDS,
|
|
11
|
+
COLOR_MAP,
|
|
12
|
+
} from '../../../../src/CrowdViewWidget/constants';
|
|
13
|
+
import { Granularity, TimeSpan } from '../../../../src/gql/types/graphql';
|
|
14
|
+
|
|
15
|
+
describe('chartUtils', () => {
|
|
16
|
+
describe('getTimeSpanForGranularity', () => {
|
|
17
|
+
it('maps granularity to expected TimeSpan', () => {
|
|
18
|
+
expect(getTimeSpanForGranularity(Granularity.M5)).toBe(TimeSpan.TwoDays);
|
|
19
|
+
expect(getTimeSpanForGranularity(Granularity.M15)).toBe(
|
|
20
|
+
TimeSpan.FiveDays
|
|
21
|
+
);
|
|
22
|
+
expect(getTimeSpanForGranularity(Granularity.H1)).toBe(
|
|
23
|
+
TimeSpan.TwentyDays
|
|
24
|
+
);
|
|
25
|
+
expect(getTimeSpanForGranularity(Granularity.H4)).toBe(
|
|
26
|
+
TimeSpan.NinetyDays
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('isDifferenceGreaterThanTwoWeeks', () => {
|
|
32
|
+
// Note: Function returns true when difference is LESS than threshold (14 days)
|
|
33
|
+
it('returns true when time difference is less than 14 days', () => {
|
|
34
|
+
const start = new Date('2025-01-01T00:00:00Z').toISOString();
|
|
35
|
+
const end = new Date('2025-01-05T00:00:00Z').toISOString();
|
|
36
|
+
expect(isDifferenceGreaterThanTwoWeeks(start, end)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns false when time difference is 14 days or more', () => {
|
|
40
|
+
const start = new Date('2025-01-01T00:00:00Z').toISOString();
|
|
41
|
+
const end = new Date('2025-02-10T00:00:00Z').toISOString();
|
|
42
|
+
expect(isDifferenceGreaterThanTwoWeeks(start, end)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('getRectColor', () => {
|
|
47
|
+
it('uses long color scale for positive sentiment', () => {
|
|
48
|
+
const color = getRectColor(BOOKS_THRESHOLDS.MAX);
|
|
49
|
+
expect(typeof color).toBe('string');
|
|
50
|
+
// At max threshold, should be at or near target color
|
|
51
|
+
expect(color.toLowerCase()).toContain(
|
|
52
|
+
COLOR_MAP.long[1].slice(1).toLowerCase().substring(0, 3)
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('uses short color scale for negative sentiment', () => {
|
|
57
|
+
const color = getRectColor(-BOOKS_THRESHOLDS.MAX);
|
|
58
|
+
expect(typeof color).toBe('string');
|
|
59
|
+
expect(color.toLowerCase()).toContain(
|
|
60
|
+
COLOR_MAP.short[1].slice(1).toLowerCase().substring(0, 3)
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('formatXAxisLabel', () => {
|
|
66
|
+
const sampleIso = '2025-03-15T10:30:00Z';
|
|
67
|
+
|
|
68
|
+
it('formats time when flag is true', () => {
|
|
69
|
+
const result = formatXAxisLabel(sampleIso, true);
|
|
70
|
+
expect(typeof result).toBe('string');
|
|
71
|
+
expect(result).toContain(':');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('formats day when flag is false', () => {
|
|
75
|
+
const result = formatXAxisLabel(sampleIso, false);
|
|
76
|
+
expect(typeof result).toBe('string');
|
|
77
|
+
// Contains day of month (15) with surrounding spaces per implementation
|
|
78
|
+
expect(result).toMatch(/\s15\s/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('getLabelData', () => {
|
|
83
|
+
const dates = [
|
|
84
|
+
'2025-03-01T00:00:00Z',
|
|
85
|
+
'2025-03-01T12:00:00Z',
|
|
86
|
+
'2025-03-02T00:00:00Z',
|
|
87
|
+
'2025-03-03T00:00:00Z',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
it('emits label when day changes for < two weeks case', () => {
|
|
91
|
+
const labels = getLabelData({
|
|
92
|
+
xAxisData: dates,
|
|
93
|
+
isGreaterThanTwoWeeks: true,
|
|
94
|
+
});
|
|
95
|
+
// First change happens between 1st and 2nd
|
|
96
|
+
expect(labels.length).toBeGreaterThanOrEqual(2);
|
|
97
|
+
expect(labels[0]).toHaveProperty('xAxis', '2025-03-02T00:00:00Z');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('emits label when month changes for >= two weeks case', () => {
|
|
101
|
+
const monthSpanDates = [
|
|
102
|
+
'2025-01-31T00:00:00Z',
|
|
103
|
+
'2025-02-01T00:00:00Z',
|
|
104
|
+
'2025-02-15T00:00:00Z',
|
|
105
|
+
];
|
|
106
|
+
const labels = getLabelData({
|
|
107
|
+
xAxisData: monthSpanDates,
|
|
108
|
+
isGreaterThanTwoWeeks: false,
|
|
109
|
+
});
|
|
110
|
+
expect(labels.length).toBe(1);
|
|
111
|
+
expect(labels[0]).toHaveProperty('xAxis', '2025-02-01T00:00:00Z');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getTooltipFormatter', () => {
|
|
116
|
+
const labelCallback = (k: string) => k;
|
|
117
|
+
|
|
118
|
+
it('renders candle and book details when available', () => {
|
|
119
|
+
const params = [
|
|
120
|
+
{
|
|
121
|
+
axisValue: '2025-03-15T10:30:00Z',
|
|
122
|
+
value: [0, 1.11111, 1.22222, 1.00001, 1.33333],
|
|
123
|
+
},
|
|
124
|
+
{ value: ['2025-03-15T10:30:00Z', 1.33333, 0] },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const buckets = [
|
|
128
|
+
[
|
|
129
|
+
{ price: 1.33, sentiment: 0.2 },
|
|
130
|
+
{ price: 1.3305, sentiment: -0.3 },
|
|
131
|
+
],
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const html = getTooltipFormatter(
|
|
135
|
+
params,
|
|
136
|
+
buckets,
|
|
137
|
+
0.0005,
|
|
138
|
+
1.3306,
|
|
139
|
+
labelCallback
|
|
140
|
+
);
|
|
141
|
+
expect(html).toContain('candle');
|
|
142
|
+
expect(html).toContain('open_price');
|
|
143
|
+
expect(html).toContain('close_price');
|
|
144
|
+
expect(html).toContain('low');
|
|
145
|
+
expect(html).toContain('high');
|
|
146
|
+
expect(html).toContain('orders');
|
|
147
|
+
expect(html).toContain('price_range');
|
|
148
|
+
// Selected price 1.3306 falls into second bucket 1.3305 - 1.3310 which has negative sentiment
|
|
149
|
+
expect(html).toContain('sell_advantage');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('omits sections when data is missing', () => {
|
|
153
|
+
const params = [
|
|
154
|
+
{
|
|
155
|
+
axisValue: '2025-03-15T10:30:00Z',
|
|
156
|
+
value: [0, 0, 0, 0, 0], // no candle values
|
|
157
|
+
},
|
|
158
|
+
{ value: ['2025-03-15T10:30:00Z', 0, 0] },
|
|
159
|
+
];
|
|
160
|
+
const buckets: Array<Array<{ price: number; sentiment: number }>> = [[]];
|
|
161
|
+
const html = getTooltipFormatter(
|
|
162
|
+
params,
|
|
163
|
+
buckets,
|
|
164
|
+
0.0005,
|
|
165
|
+
0,
|
|
166
|
+
labelCallback
|
|
167
|
+
);
|
|
168
|
+
expect(html).not.toContain('open_price');
|
|
169
|
+
expect(html).not.toContain('orders');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|