@oanda/labs-crowd-view-widget 1.0.43 → 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.
Files changed (180) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/dist/main/CrowdViewWidget/CrowdViewWidget.js +3 -3
  3. package/dist/main/CrowdViewWidget/CrowdViewWidget.js.map +1 -1
  4. package/dist/main/CrowdViewWidget/Main.js +22 -12
  5. package/dist/main/CrowdViewWidget/Main.js.map +1 -1
  6. package/dist/main/CrowdViewWidget/components/Chart/Chart.js +16 -17
  7. package/dist/main/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
  8. package/dist/main/CrowdViewWidget/components/Chart/ChartWithData.js +16 -13
  9. package/dist/main/CrowdViewWidget/components/Chart/ChartWithData.js.map +1 -1
  10. package/dist/main/CrowdViewWidget/components/Chart/{getOption.js → chartOptions.js} +79 -62
  11. package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -0
  12. package/dist/main/CrowdViewWidget/components/Chart/index.js +44 -0
  13. package/dist/main/CrowdViewWidget/components/Chart/index.js.map +1 -1
  14. package/dist/main/CrowdViewWidget/components/Chart/types.js.map +1 -1
  15. package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js +184 -0
  16. package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -0
  17. package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js +107 -0
  18. package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -0
  19. package/dist/main/CrowdViewWidget/components/Legend/Legend.js +3 -2
  20. package/dist/main/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
  21. package/dist/main/CrowdViewWidget/components/Legend/LegendBar.js +9 -11
  22. package/dist/main/CrowdViewWidget/components/Legend/LegendBar.js.map +1 -1
  23. package/dist/main/CrowdViewWidget/constants.js +31 -0
  24. package/dist/main/CrowdViewWidget/constants.js.map +1 -0
  25. package/dist/main/CrowdViewWidget/render.js +1 -0
  26. package/dist/main/CrowdViewWidget/render.js.map +1 -1
  27. package/dist/main/CrowdViewWidget/selectConfig.js +121 -0
  28. package/dist/main/CrowdViewWidget/selectConfig.js.map +1 -0
  29. package/dist/main/CrowdViewWidget/types/index.js +17 -0
  30. package/dist/main/CrowdViewWidget/types/index.js.map +1 -0
  31. package/dist/main/CrowdViewWidget/types/instruments.js +45 -0
  32. package/dist/main/CrowdViewWidget/types/instruments.js.map +1 -0
  33. package/dist/main/CrowdViewWidget/types.js +0 -44
  34. package/dist/main/CrowdViewWidget/types.js.map +1 -1
  35. package/dist/main/CrowdViewWidget/utils/instrumentUtils.js +13 -0
  36. package/dist/main/CrowdViewWidget/utils/instrumentUtils.js.map +1 -0
  37. package/dist/main/gql/getOrderPositionBooks.js +1 -1
  38. package/dist/main/gql/getOrderPositionBooks.js.map +1 -1
  39. package/dist/main/gql/getPriceCandles.js +11 -0
  40. package/dist/main/gql/getPriceCandles.js.map +1 -0
  41. package/dist/main/gql/types/gql.js +2 -3
  42. package/dist/main/gql/types/gql.js.map +1 -1
  43. package/dist/main/gql/types/graphql.js +161 -160
  44. package/dist/main/gql/types/graphql.js.map +1 -1
  45. package/dist/main/translations/sources/en.json +24 -0
  46. package/dist/module/CrowdViewWidget/CrowdViewWidget.js +3 -3
  47. package/dist/module/CrowdViewWidget/CrowdViewWidget.js.map +1 -1
  48. package/dist/module/CrowdViewWidget/Main.js +23 -13
  49. package/dist/module/CrowdViewWidget/Main.js.map +1 -1
  50. package/dist/module/CrowdViewWidget/components/Chart/Chart.js +15 -16
  51. package/dist/module/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
  52. package/dist/module/CrowdViewWidget/components/Chart/ChartWithData.js +16 -12
  53. package/dist/module/CrowdViewWidget/components/Chart/ChartWithData.js.map +1 -1
  54. package/dist/module/CrowdViewWidget/components/Chart/{getOption.js → chartOptions.js} +77 -59
  55. package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -0
  56. package/dist/module/CrowdViewWidget/components/Chart/index.js +4 -0
  57. package/dist/module/CrowdViewWidget/components/Chart/index.js.map +1 -1
  58. package/dist/module/CrowdViewWidget/components/Chart/types.js.map +1 -1
  59. package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js +177 -0
  60. package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -0
  61. package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js +94 -0
  62. package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -0
  63. package/dist/module/CrowdViewWidget/components/Legend/Legend.js +3 -2
  64. package/dist/module/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
  65. package/dist/module/CrowdViewWidget/components/Legend/LegendBar.js +9 -11
  66. package/dist/module/CrowdViewWidget/components/Legend/LegendBar.js.map +1 -1
  67. package/dist/module/CrowdViewWidget/constants.js +25 -0
  68. package/dist/module/CrowdViewWidget/constants.js.map +1 -0
  69. package/dist/module/CrowdViewWidget/render.js +1 -0
  70. package/dist/module/CrowdViewWidget/render.js.map +1 -1
  71. package/dist/module/CrowdViewWidget/selectConfig.js +116 -0
  72. package/dist/module/CrowdViewWidget/selectConfig.js.map +1 -0
  73. package/dist/module/CrowdViewWidget/types/index.js +2 -0
  74. package/dist/module/CrowdViewWidget/types/index.js.map +1 -0
  75. package/dist/module/CrowdViewWidget/types/instruments.js +39 -0
  76. package/dist/module/CrowdViewWidget/types/instruments.js.map +1 -0
  77. package/dist/module/CrowdViewWidget/types.js +1 -43
  78. package/dist/module/CrowdViewWidget/types.js.map +1 -1
  79. package/dist/module/CrowdViewWidget/utils/instrumentUtils.js +6 -0
  80. package/dist/module/CrowdViewWidget/utils/instrumentUtils.js.map +1 -0
  81. package/dist/module/gql/getOrderPositionBooks.js +1 -1
  82. package/dist/module/gql/getOrderPositionBooks.js.map +1 -1
  83. package/dist/module/gql/getPriceCandles.js +6 -0
  84. package/dist/module/gql/getPriceCandles.js.map +1 -0
  85. package/dist/module/gql/types/gql.js +2 -3
  86. package/dist/module/gql/types/gql.js.map +1 -1
  87. package/dist/module/gql/types/graphql.js +160 -159
  88. package/dist/module/gql/types/graphql.js.map +1 -1
  89. package/dist/module/translations/sources/en.json +24 -0
  90. package/dist/types/CrowdViewWidget/CrowdViewWidget.d.ts +1 -1
  91. package/dist/types/CrowdViewWidget/components/Chart/ChartWithData.d.ts +1 -1
  92. package/dist/types/CrowdViewWidget/components/Chart/index.d.ts +4 -0
  93. package/dist/types/CrowdViewWidget/components/Chart/types.d.ts +23 -16
  94. package/dist/types/CrowdViewWidget/components/Chart/useCrowdViewData.d.ts +2 -0
  95. package/dist/types/CrowdViewWidget/components/Chart/{utils.d.ts → utils/chartUtils.d.ts} +9 -11
  96. package/dist/types/CrowdViewWidget/components/Legend/Legend.d.ts +3 -3
  97. package/dist/types/CrowdViewWidget/constants.d.ts +24 -0
  98. package/dist/types/CrowdViewWidget/selectConfig.d.ts +19 -0
  99. package/dist/types/CrowdViewWidget/types/index.d.ts +1 -0
  100. package/dist/types/CrowdViewWidget/types/instruments.d.ts +36 -0
  101. package/dist/types/CrowdViewWidget/types.d.ts +2 -50
  102. package/dist/types/CrowdViewWidget/utils/instrumentUtils.d.ts +8 -0
  103. package/dist/types/gql/types/gql.d.ts +10 -14
  104. package/dist/types/gql/types/graphql.d.ts +71 -63
  105. package/lokalise.config.json +1 -1
  106. package/package.json +6 -4
  107. package/src/CrowdViewWidget/CrowdViewWidget.tsx +2 -2
  108. package/src/CrowdViewWidget/Main.tsx +32 -22
  109. package/src/CrowdViewWidget/components/Chart/Chart.tsx +21 -17
  110. package/src/CrowdViewWidget/components/Chart/ChartWithData.tsx +12 -12
  111. package/src/CrowdViewWidget/components/Chart/chartOptions.ts +205 -0
  112. package/src/CrowdViewWidget/components/Chart/index.ts +4 -0
  113. package/src/CrowdViewWidget/components/Chart/types.ts +30 -24
  114. package/src/CrowdViewWidget/components/Chart/useCrowdViewData.ts +288 -0
  115. package/src/CrowdViewWidget/components/Chart/utils/chartUtils.ts +161 -0
  116. package/src/CrowdViewWidget/components/Legend/Legend.tsx +7 -3
  117. package/src/CrowdViewWidget/components/Legend/LegendBar.tsx +16 -20
  118. package/src/CrowdViewWidget/constants.ts +28 -0
  119. package/src/CrowdViewWidget/render.tsx +1 -0
  120. package/src/CrowdViewWidget/{config.ts → selectConfig.ts} +65 -43
  121. package/src/CrowdViewWidget/types/index.ts +1 -0
  122. package/src/CrowdViewWidget/types/instruments.ts +37 -0
  123. package/src/CrowdViewWidget/types.ts +4 -55
  124. package/src/CrowdViewWidget/utils/instrumentUtils.ts +11 -0
  125. package/src/gql/getOrderPositionBooks.ts +9 -4
  126. package/src/gql/{mock/getPriceCandles.ts → getPriceCandles.ts} +5 -5
  127. package/src/gql/types/gql.ts +6 -14
  128. package/src/gql/types/graphql.ts +170 -160
  129. package/src/translations/sources/en.json +24 -0
  130. package/test/Main.test.tsx +73 -27
  131. package/test/components/Chart/utils/chartUtils.test.ts +172 -0
  132. package/test/components/Legend.test.tsx +3 -6
  133. package/test/components/LegendBar.test.tsx +7 -8
  134. package/test/utils/instrumentUtils.test.ts +52 -0
  135. package/dist/main/CrowdViewWidget/components/Chart/constants.js +0 -14
  136. package/dist/main/CrowdViewWidget/components/Chart/constants.js.map +0 -1
  137. package/dist/main/CrowdViewWidget/components/Chart/getOption.js.map +0 -1
  138. package/dist/main/CrowdViewWidget/components/Chart/getOrderPositionDataMock.js +0 -47
  139. package/dist/main/CrowdViewWidget/components/Chart/getOrderPositionDataMock.js.map +0 -1
  140. package/dist/main/CrowdViewWidget/components/Chart/getPriceCandlesMock.js +0 -36
  141. package/dist/main/CrowdViewWidget/components/Chart/getPriceCandlesMock.js.map +0 -1
  142. package/dist/main/CrowdViewWidget/components/Chart/utils.js +0 -166
  143. package/dist/main/CrowdViewWidget/components/Chart/utils.js.map +0 -1
  144. package/dist/main/CrowdViewWidget/config.js +0 -107
  145. package/dist/main/CrowdViewWidget/config.js.map +0 -1
  146. package/dist/main/gql/mock/getPriceCandles.js +0 -11
  147. package/dist/main/gql/mock/getPriceCandles.js.map +0 -1
  148. package/dist/main/gql/mock/schema.graphqls +0 -62
  149. package/dist/main/gql/validateInstruments.js +0 -11
  150. package/dist/main/gql/validateInstruments.js.map +0 -1
  151. package/dist/module/CrowdViewWidget/components/Chart/constants.js +0 -8
  152. package/dist/module/CrowdViewWidget/components/Chart/constants.js.map +0 -1
  153. package/dist/module/CrowdViewWidget/components/Chart/getOption.js.map +0 -1
  154. package/dist/module/CrowdViewWidget/components/Chart/getOrderPositionDataMock.js +0 -40
  155. package/dist/module/CrowdViewWidget/components/Chart/getOrderPositionDataMock.js.map +0 -1
  156. package/dist/module/CrowdViewWidget/components/Chart/getPriceCandlesMock.js +0 -29
  157. package/dist/module/CrowdViewWidget/components/Chart/getPriceCandlesMock.js.map +0 -1
  158. package/dist/module/CrowdViewWidget/components/Chart/utils.js +0 -156
  159. package/dist/module/CrowdViewWidget/components/Chart/utils.js.map +0 -1
  160. package/dist/module/CrowdViewWidget/config.js +0 -102
  161. package/dist/module/CrowdViewWidget/config.js.map +0 -1
  162. package/dist/module/gql/mock/getPriceCandles.js +0 -6
  163. package/dist/module/gql/mock/getPriceCandles.js.map +0 -1
  164. package/dist/module/gql/mock/schema.graphqls +0 -62
  165. package/dist/module/gql/validateInstruments.js +0 -5
  166. package/dist/module/gql/validateInstruments.js.map +0 -1
  167. package/dist/types/CrowdViewWidget/components/Chart/constants.d.ts +0 -7
  168. package/dist/types/CrowdViewWidget/components/Chart/getOrderPositionDataMock.d.ts +0 -14
  169. package/dist/types/CrowdViewWidget/components/Chart/getPriceCandlesMock.d.ts +0 -2
  170. package/dist/types/CrowdViewWidget/config.d.ts +0 -22
  171. package/dist/types/gql/validateInstruments.d.ts +0 -1
  172. package/src/CrowdViewWidget/components/Chart/constants.tsx +0 -8
  173. package/src/CrowdViewWidget/components/Chart/getOption.ts +0 -200
  174. package/src/CrowdViewWidget/components/Chart/getOrderPositionDataMock.ts +0 -66
  175. package/src/CrowdViewWidget/components/Chart/getPriceCandlesMock.ts +0 -43
  176. package/src/CrowdViewWidget/components/Chart/utils.ts +0 -191
  177. package/src/gql/mock/schema.graphqls +0 -62
  178. package/src/gql/validateInstruments.ts +0 -10
  179. /package/dist/types/CrowdViewWidget/components/Chart/{getOption.d.ts → chartOptions.d.ts} +0 -0
  180. /package/dist/types/gql/{mock/getPriceCandles.d.ts → getPriceCandles.d.ts} +0 -0
@@ -0,0 +1,288 @@
1
+ import { useQuery } from '@apollo/client';
2
+ import { useMemo } from 'react';
3
+
4
+ import { getOrderPositionBooks } from '../../../gql/getOrderPositionBooks';
5
+ import { getPriceCandles } from '../../../gql/getPriceCandles';
6
+ import type {
7
+ GetOrderPositionBooksQuery,
8
+ GetOrderPositionBooksQueryVariables,
9
+ GetPriceCandlesQuery,
10
+ GetPriceCandlesQueryVariables,
11
+ } from '../../../gql/types/graphql';
12
+ import { BookType, DataSource, Division } from '../../../gql/types/graphql';
13
+ import { BOOKS_THRESHOLDS, BUCKET_CONFIG } from '../../constants';
14
+ import type { UseCrowdViewDataProps, UseCrowdViewDataReturn } from './types';
15
+ import { getTimeSpanForGranularity } from './utils/chartUtils';
16
+
17
+ const processPriceCandles = (
18
+ priceCandlesData: GetPriceCandlesQuery | undefined
19
+ ) => {
20
+ if (!priceCandlesData?.priceCandles?.candle?.length) {
21
+ return {
22
+ minPrice: 0,
23
+ maxPrice: 0,
24
+ hasValidCandles: false,
25
+ candleMap: new Map(),
26
+ candles: [],
27
+ };
28
+ }
29
+
30
+ const candles = priceCandlesData.priceCandles.candle;
31
+ let calculatedMinPrice = Number.MAX_VALUE;
32
+ let calculatedMaxPrice = Number.MIN_VALUE;
33
+
34
+ const candleMap = new Map<
35
+ string,
36
+ {
37
+ point?: string;
38
+ high?: number;
39
+ low?: number;
40
+ open?: number;
41
+ close?: number;
42
+ }
43
+ >();
44
+
45
+ candles.forEach((candle) => {
46
+ if (!candle) return;
47
+
48
+ if (candle.high > calculatedMaxPrice) {
49
+ calculatedMaxPrice = candle.high;
50
+ }
51
+ if (candle.low < calculatedMinPrice) {
52
+ calculatedMinPrice = candle.low;
53
+ }
54
+
55
+ if (candle.point) {
56
+ candleMap.set(candle.point, candle);
57
+ }
58
+ });
59
+
60
+ return {
61
+ minPrice: calculatedMinPrice,
62
+ maxPrice: calculatedMaxPrice,
63
+ hasValidCandles: true,
64
+ candleMap,
65
+ candles,
66
+ };
67
+ };
68
+
69
+ const processOrderPositionBooks = (
70
+ orderPositionData: GetOrderPositionBooksQuery | undefined,
71
+ candleMap: Map<
72
+ string,
73
+ {
74
+ point?: string;
75
+ high?: number;
76
+ low?: number;
77
+ open?: number;
78
+ close?: number;
79
+ }
80
+ >
81
+ ) => {
82
+ if (!orderPositionData?.orderPositionBooks?.length) {
83
+ return [];
84
+ }
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
+
105
+ return orderPositionData.orderPositionBooks
106
+ .filter((book): book is NonNullable<typeof book> => {
107
+ return book !== null && book.buckets?.length > 0;
108
+ })
109
+ .map((book) => {
110
+ const filteredBuckets = book.buckets
111
+ .filter((bucket) => {
112
+ if (
113
+ !bucket ||
114
+ bucket.sentiment === undefined ||
115
+ bucket.sentiment === null ||
116
+ bucket.price === undefined
117
+ ) {
118
+ return false;
119
+ }
120
+ return Math.abs(bucket.sentiment) >= BOOKS_THRESHOLDS.MIN;
121
+ })
122
+ .map((bucket) => ({
123
+ price: bucket!.price!,
124
+ sentiment: bucket!.sentiment!,
125
+ }));
126
+
127
+ return filteredBuckets;
128
+ });
129
+ };
130
+
131
+ const validateData = (
132
+ priceCandlesData: GetPriceCandlesQuery | undefined,
133
+ orderPositionData: GetOrderPositionBooksQuery | undefined,
134
+ hasValidCandles: boolean
135
+ ): Error | null => {
136
+ const hasValidPriceData =
137
+ (priceCandlesData?.priceCandles?.candle?.length ?? 0) >= 1;
138
+ const hasValidOrderData =
139
+ (orderPositionData?.orderPositionBooks?.length ?? 0) >= 1;
140
+
141
+ if (!hasValidPriceData) {
142
+ return new Error('Insufficient price candle data');
143
+ }
144
+ if (!hasValidOrderData) {
145
+ return new Error('Insufficient order position data');
146
+ }
147
+ if (!hasValidCandles) {
148
+ return new Error('Invalid candle data');
149
+ }
150
+
151
+ return null;
152
+ };
153
+
154
+ export const useCrowdViewData = ({
155
+ instrument,
156
+ bookType,
157
+ division,
158
+ granularity,
159
+ }: UseCrowdViewDataProps): UseCrowdViewDataReturn => {
160
+ const {
161
+ loading: priceCandlesLoading,
162
+ data: priceCandlesData,
163
+ error: priceCandlesError,
164
+ } = useQuery<GetPriceCandlesQuery, GetPriceCandlesQueryVariables>(
165
+ getPriceCandles,
166
+ {
167
+ variables: {
168
+ dataSource: division === Division.Oc ? DataSource.V20 : DataSource.Mt5,
169
+ division,
170
+ instrument,
171
+ granularity,
172
+ timeSpan: getTimeSpanForGranularity(granularity),
173
+ },
174
+ fetchPolicy: 'no-cache',
175
+ }
176
+ );
177
+
178
+ const priceCandlesProcessed = useMemo(
179
+ () => processPriceCandles(priceCandlesData),
180
+ [priceCandlesData]
181
+ );
182
+
183
+ const { minPrice, maxPrice, hasValidCandles, candleMap, candles } =
184
+ priceCandlesProcessed;
185
+
186
+ const maxBookPrice = useMemo(
187
+ () =>
188
+ maxPrice +
189
+ BUCKET_CONFIG.DEFAULT_WIDTH * BUCKET_CONFIG.PRICE_PADDING_MULTIPLIER,
190
+ [maxPrice]
191
+ );
192
+
193
+ const minBookPrice = useMemo(
194
+ () =>
195
+ minPrice -
196
+ BUCKET_CONFIG.DEFAULT_WIDTH * BUCKET_CONFIG.PRICE_PADDING_MULTIPLIER,
197
+ [minPrice]
198
+ );
199
+
200
+ const {
201
+ loading: orderPositionLoading,
202
+ data: orderPositionData,
203
+ error: orderPositionError,
204
+ } = useQuery<GetOrderPositionBooksQuery, GetOrderPositionBooksQueryVariables>(
205
+ getOrderPositionBooks,
206
+ {
207
+ variables: {
208
+ instrument,
209
+ bookType: bookType || BookType.Order,
210
+ timeSpan: getTimeSpanForGranularity(granularity),
211
+ granularity,
212
+ maxBookPrice,
213
+ minBookPrice,
214
+ },
215
+ fetchPolicy: 'no-cache',
216
+ skip: priceCandlesLoading || !!priceCandlesError,
217
+ }
218
+ );
219
+
220
+ const loading = priceCandlesLoading || orderPositionLoading;
221
+
222
+ const orderPositionBooks = useMemo(
223
+ () => processOrderPositionBooks(orderPositionData, candleMap),
224
+ [orderPositionData, candleMap]
225
+ );
226
+
227
+ const buckets = useMemo(
228
+ () => processBuckets(orderPositionData),
229
+ [orderPositionData]
230
+ );
231
+
232
+ const error = useMemo((): Error | null => {
233
+ if (priceCandlesError) {
234
+ return new Error(`Price candles error: ${priceCandlesError.message}`);
235
+ }
236
+ if (orderPositionError) {
237
+ return new Error(`Order position error: ${orderPositionError.message}`);
238
+ }
239
+ if (loading) {
240
+ return null;
241
+ }
242
+ return validateData(priceCandlesData, orderPositionData, hasValidCandles);
243
+ }, [
244
+ priceCandlesError,
245
+ orderPositionError,
246
+ loading,
247
+ priceCandlesData,
248
+ orderPositionData,
249
+ hasValidCandles,
250
+ ]);
251
+
252
+ const data = useMemo(() => {
253
+ if (!priceCandlesData || !orderPositionData || error) {
254
+ return null;
255
+ }
256
+
257
+ const xAxisData = candles.map((candle) => candle?.point || '');
258
+ const candlesSeriesData: [number, number, number, number][] = candles.map(
259
+ (candle) => [
260
+ candle?.open || 0,
261
+ candle?.close || 0,
262
+ candle?.low || 0,
263
+ candle?.high || 0,
264
+ ]
265
+ );
266
+
267
+ return {
268
+ buckets,
269
+ xAxisData,
270
+ candlesSeriesData,
271
+ orderPositionBooks,
272
+ bucketWidth: orderPositionData.orderPositionBooks?.[0]?.bucketWidth || 0,
273
+ };
274
+ }, [
275
+ priceCandlesData,
276
+ orderPositionData,
277
+ error,
278
+ candles,
279
+ orderPositionBooks,
280
+ buckets,
281
+ ]);
282
+
283
+ return {
284
+ data,
285
+ loading,
286
+ error: !!error,
287
+ };
288
+ };
@@ -0,0 +1,161 @@
1
+ import chroma from 'chroma-js';
2
+
3
+ import { Granularity, TimeSpan } from '../../../../gql/types/graphql';
4
+ import {
5
+ BOOKS_THRESHOLDS,
6
+ CHART_CONFIG,
7
+ COLOR_MAP,
8
+ TIME_THRESHOLDS,
9
+ } from '../../../constants';
10
+ import type { GetLabelsDataProps } from '../types';
11
+
12
+ export const getLabelData = ({
13
+ xAxisData,
14
+ isGreaterThanTwoWeeks,
15
+ }: GetLabelsDataProps) =>
16
+ xAxisData
17
+ .filter((record, index, arr) => {
18
+ if (index === 0) {
19
+ return false;
20
+ }
21
+ const previousTimestamp = arr[index - 1];
22
+ const currentDate = new Date(record);
23
+ const previousDate = new Date(previousTimestamp);
24
+
25
+ return isGreaterThanTwoWeeks
26
+ ? currentDate.getDate() !== previousDate.getDate()
27
+ : currentDate.getMonth() !== previousDate.getMonth();
28
+ })
29
+ .map((item) => ({
30
+ name: new Date(item).toLocaleDateString(undefined, {
31
+ month: isGreaterThanTwoWeeks ? 'short' : 'long',
32
+ day: isGreaterThanTwoWeeks ? 'numeric' : undefined,
33
+ }),
34
+ xAxis: item,
35
+ y: CHART_CONFIG.HEIGHT - 22,
36
+ silent: true,
37
+ emphasis: {
38
+ disabled: true,
39
+ },
40
+ label: {
41
+ fontFamily: 'Sofia W03',
42
+ fontSize: 10,
43
+ position: 'bottom',
44
+ align: 'center',
45
+ formatter: '{b}',
46
+ },
47
+ }));
48
+
49
+ export const isDifferenceGreaterThanTwoWeeks = (
50
+ startDate: string,
51
+ endDate: string
52
+ ) => {
53
+ const date1 = new Date(startDate);
54
+ const date2 = new Date(endDate);
55
+
56
+ const differenceInMs = Math.abs(date2.getTime() - date1.getTime());
57
+
58
+ return differenceInMs < TIME_THRESHOLDS.TWO_WEEKS_MS;
59
+ };
60
+
61
+ export const getTimeSpanForGranularity = (
62
+ granularity: Granularity
63
+ ): TimeSpan => {
64
+ const granularityTimeSpanMap: Record<Granularity, TimeSpan> = {
65
+ [Granularity.M5]: TimeSpan.TwoDays,
66
+ [Granularity.M15]: TimeSpan.FiveDays,
67
+ [Granularity.H1]: TimeSpan.TwentyDays,
68
+ [Granularity.H4]: TimeSpan.NinetyDays,
69
+ };
70
+
71
+ return granularityTimeSpanMap[granularity] || TimeSpan.TwoDays;
72
+ };
73
+
74
+ const getGradientColor = (
75
+ value: number,
76
+ startColor: string,
77
+ targetColor: string
78
+ ): string => {
79
+ const startThreshold = BOOKS_THRESHOLDS.MIN;
80
+ const endThreshold = BOOKS_THRESHOLDS.MAX;
81
+
82
+ const colorScale = chroma
83
+ .scale([startColor, targetColor])
84
+ .domain([startThreshold, endThreshold])
85
+ .mode('rgb');
86
+
87
+ return colorScale(value).hex();
88
+ };
89
+
90
+ export const getRectColor = (sentiment: number) =>
91
+ sentiment < 0
92
+ ? getGradientColor(sentiment * -1, COLOR_MAP.short[0], COLOR_MAP.short[1])
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
+ };
@@ -1,14 +1,18 @@
1
1
  import { useLocale } from '@oanda/mono-i18n';
2
2
  import React from 'react';
3
3
 
4
+ import { BOOKS_THRESHOLDS } from '../../constants';
4
5
  import { LegendBar } from './LegendBar';
5
6
 
6
7
  interface LegendProps {
7
- longValues: number[];
8
- shortValues: number[];
8
+ longValues?: [number, number];
9
+ shortValues?: [number, number];
9
10
  }
10
11
 
11
- export const Legend = ({ longValues, shortValues }: LegendProps) => {
12
+ export const Legend = ({
13
+ longValues = [BOOKS_THRESHOLDS.MIN, BOOKS_THRESHOLDS.MAX],
14
+ shortValues = [BOOKS_THRESHOLDS.MIN, BOOKS_THRESHOLDS.MAX],
15
+ }: LegendProps) => {
12
16
  const { lang } = useLocale();
13
17
 
14
18
  return (
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
- import { colorMap } from '../../config';
3
+ import { COLOR_MAP } from '../../constants';
4
4
 
5
5
  export type LegendType = 'long' | 'short';
6
6
 
@@ -11,31 +11,27 @@ interface LegendBarProps {
11
11
  }
12
12
 
13
13
  export const LegendBar = ({ values, type, label }: LegendBarProps) => {
14
- const colors = colorMap[type];
14
+ const colors = COLOR_MAP[type];
15
15
 
16
16
  return (
17
17
  <div className="lw-flex lw-w-full lw-flex-col lw-space-y-1 lw-border-border-primary">
18
18
  <div className="lw-flex lw-h-2 lw-w-full lw-overflow-hidden lw-border lw-border-border-primary">
19
- {values.map((_, index) => (
20
- <div
21
- key={`${label}_${colors[index]}__color`}
22
- className="lw-h-full lw-flex-1"
23
- data-testid="legend-bar-segment"
24
- style={{
25
- backgroundColor: colors[index],
26
- }}
27
- />
28
- ))}
19
+ <div
20
+ className="lw-h-full lw-flex-1"
21
+ data-testid="legend-bar-segment"
22
+ style={{
23
+ background: `linear-gradient(90deg,${colors[0]} 0%, ${colors[1]} 100%)`,
24
+ }}
25
+ />
29
26
  </div>
30
27
 
31
- <div className="lw-flex lw-w-full">
32
- {values.map((value) => (
33
- <div key={`${value}_${type}`} className="lw-flex lw-w-1/4">
34
- <span className="lw-whitespace-nowrap lw-font-sans lw-text-xs lw-text-text-primary">
35
- {value.toFixed(2)}% {label}
36
- </span>
37
- </div>
38
- ))}
28
+ <div className="lw-flex lw-w-full lw-justify-between">
29
+ <span className="lw-whitespace-nowrap lw-font-sans lw-text-xs lw-text-text-primary">
30
+ {values[0].toFixed(2)}% {label}
31
+ </span>
32
+ <span className="lw-whitespace-nowrap lw-font-sans lw-text-xs lw-text-text-primary">
33
+ {values[1].toFixed(2)}% {label}
34
+ </span>
39
35
  </div>
40
36
  </div>
41
37
  );
@@ -0,0 +1,28 @@
1
+ export const BOOKS_THRESHOLDS = {
2
+ MIN: 0.15,
3
+ MAX: 0.55,
4
+ } as const;
5
+
6
+ export const BUCKET_CONFIG = {
7
+ DEFAULT_WIDTH: 0.0005,
8
+ PRICE_PADDING_MULTIPLIER: 2,
9
+ } as const;
10
+
11
+ export const TIME_THRESHOLDS = {
12
+ TWO_WEEKS_MS: 14 * 24 * 60 * 60 * 1000,
13
+ } as const;
14
+
15
+ export const CHART_CONFIG = {
16
+ HEIGHT: 425,
17
+ WIDTH: 9999,
18
+ X_LABEL_SIZE: 40,
19
+ Y_LABEL_SIZE_DESKTOP: 60,
20
+ INITIAL_START_ZOOM: 0,
21
+ INITIAL_END_ZOOM: 100,
22
+ X_AXIS_DATE_PADDING: ' ',
23
+ } as const;
24
+
25
+ export const COLOR_MAP = {
26
+ long: ['#fcedca', '#FAB313'],
27
+ short: ['#c8ebfa', '#309DCC'],
28
+ } as const;
@@ -42,6 +42,7 @@ dataCrowdViewParamsElements.forEach((element) => {
42
42
 
43
43
  root.render(
44
44
  <CrowdViewWidget
45
+ division={division}
45
46
  graphqlUrl={graphqlUrl}
46
47
  isParamError={isParamError}
47
48
  locale={locale}