Skip to content

Commit 43ba5d3

Browse files
committed
Show chart history loading state
1 parent d459cb2 commit 43ba5d3

13 files changed

Lines changed: 524 additions & 129 deletions

File tree

src/hyperopen/runtime/app_effects.cljs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
state
2121
path-values))))
2222

23+
(defn- update-chart-history-backfill-pending
24+
[state delta]
25+
(let [current (or (get-in state [:chart-options :history-backfill-pending-count]) 0)
26+
next-count (max 0 (+ current delta))]
27+
(assoc-in state [:chart-options :history-backfill-pending-count] next-count)))
28+
2329
(defn local-storage-set!
2430
[key value]
2531
(try
@@ -59,7 +65,8 @@
5965
active-asset (:active-asset @store)
6066
target-coin (if (seq requested-coin)
6167
requested-coin
62-
active-asset)]
68+
active-asset)
69+
historical-backfill? (some? end-time-ms)]
6370
(log-fn "Fetching candle snapshot..."
6471
(clj->js {:coin target-coin
6572
:interval interval*
@@ -68,19 +75,26 @@
6875
(if (or (not target-coin)
6976
(not (request-active? active?-fn)))
7077
(js/Promise.resolve nil)
71-
(-> (request-candle-snapshot-fn target-coin
72-
:interval interval*
73-
:bars bars*
74-
:end-time-ms end-time-ms
75-
:active?-fn active?-fn)
76-
(.then (fn [rows]
77-
(when (request-active? active?-fn)
78-
(swap! store apply-candle-snapshot-success target-coin interval* rows))
79-
rows))
80-
(.catch (fn [err]
81-
(when (request-active? active?-fn)
82-
(swap! store apply-candle-snapshot-error target-coin interval* err))
83-
(js/Promise.reject err)))))))
78+
(do
79+
(when historical-backfill?
80+
(swap! store update-chart-history-backfill-pending 1))
81+
(-> (request-candle-snapshot-fn target-coin
82+
:interval interval*
83+
:bars bars*
84+
:end-time-ms end-time-ms
85+
:active?-fn active?-fn)
86+
(.then (fn [rows]
87+
(when (request-active? active?-fn)
88+
(swap! store apply-candle-snapshot-success target-coin interval* rows))
89+
(when historical-backfill?
90+
(swap! store update-chart-history-backfill-pending -1))
91+
rows))
92+
(.catch (fn [err]
93+
(when (request-active? active?-fn)
94+
(swap! store apply-candle-snapshot-error target-coin interval* err))
95+
(when historical-backfill?
96+
(swap! store update-chart-history-backfill-pending -1))
97+
(js/Promise.reject err))))))))
8498

8599
(defn init-websocket!
86100
[{:keys [ws-url

src/hyperopen/views/trading_chart/core.cljs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
volume-visible? (boolean (get chart-runtime-options :volume-visible? true))
7070
on-hide-volume-indicator (:on-hide-volume-indicator chart-runtime-options)
7171
on-history-backfill-request (:on-history-backfill-request chart-runtime-options)
72+
history-backfill-loading? (boolean (:history-backfill-loading? chart-runtime-options))
7273
main-series-markers (memoized-main-series-markers indicator-markers
7374
fill-markers
7475
(:entry-marker position-overlay))
@@ -97,7 +98,9 @@
9798
chart-accessible-label (str (or (:symbol legend-meta) "Asset")
9899
" price chart, "
99100
(or (:timeframe-label legend-meta) "selected")
100-
" timeframe")
101+
" timeframe"
102+
(when history-backfill-loading?
103+
", loading older candles"))
101104
on-render (runtime/chart-canvas-on-render
102105
{:candle-data candle-data
103106
:chart-type chart-type
@@ -119,10 +122,14 @@
119122
:context-menu-deps context-menu-deps
120123
:schedule-decoration-frame! *schedule-chart-decoration-frame!*
121124
:cancel-decoration-frame! *cancel-chart-decoration-frame!*})]
122-
[:div {:class ["w-full" "min-w-0" "relative" "flex-1" "min-h-[360px]" "overflow-hidden" "bg-base-100" "trading-chart-host"]
125+
[:div {:class (cond-> ["w-full" "min-w-0" "relative" "flex-1" "min-h-[360px]"
126+
"overflow-hidden" "bg-base-100" "trading-chart-host"]
127+
history-backfill-loading?
128+
(conj "trading-chart-host--history-loading"))
123129
:data-parity-id "chart-canvas"
124130
:data-role "trading-chart-canvas"
125131
:role "region"
132+
:aria-busy history-backfill-loading?
126133
:aria-label chart-accessible-label
127134
:tabindex 0
128135
:replicant/key (str "chart-" (hash active-indicators) "-" legend-key "-" volume-visible?)

src/hyperopen/views/trading_chart/runtime.cljs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@
125125
:selected-timeframe selected-timeframe
126126
:on-history-backfill-request on-history-backfill-request}))
127127

128+
(defn- restore-token-current?
129+
[node chart restore-token]
130+
(let [{:keys [chart-obj visible-range-restore-token]} (chart-runtime/get-state node)
131+
current-chart (when chart-obj (.-chart ^js chart-obj))]
132+
(and (= restore-token visible-range-restore-token)
133+
(identical? chart current-chart))))
134+
128135
(defn- schedule-decoration-frame-fn
129136
[context]
130137
(let [schedule! (:schedule-decoration-frame! context)]
@@ -168,8 +175,9 @@
168175
selected-timeframe
169176
(assoc persistence-deps
170177
:on-visible-range-change! (fn []
171-
(mark-visible-range-interaction! node)
172-
(maybe-request-history-backfill! node chart)))))
178+
(mark-visible-range-interaction! node))
179+
:on-visible-range-event! (fn []
180+
(maybe-request-history-backfill! node chart)))))
173181

174182
(defn- start-visible-range-restore!
175183
[node chart candles selected-timeframe persistence-deps]
@@ -193,6 +201,9 @@
193201
(:visible-range-restore-token runtime-state*))
194202
(= interaction-epoch
195203
(or (:visible-range-interaction-epoch runtime-state*) 0)))))))
204+
(.then (fn [_]
205+
(when (restore-token-current? node chart restore-token)
206+
(maybe-request-history-backfill! node chart))))
196207
(.catch (fn [error]
197208
(js/console.warn "Failed to restore persisted visible range:" error))))))
198209

@@ -373,7 +384,8 @@
373384
(ci/set-volume-data! volume-series candle-data))
374385
(when chart
375386
(sync-history-backfill-context! node context)
376-
(ensure-visible-range-lifecycle! node chart candle-data selected-timeframe persistence-deps))
387+
(ensure-visible-range-lifecycle! node chart candle-data selected-timeframe persistence-deps)
388+
(maybe-request-history-backfill! node chart))
377389
(when chart-obj
378390
(schedule-chart-decoration-pass! node context))
379391
(sync-indicator-series! chart-obj indicator-series-data)

src/hyperopen/views/trading_chart/utils/chart_interop/visible_range_persistence.cljs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,11 +416,13 @@
416416
persist-visible-range-fn
417417
set-timeout-fn
418418
clear-timeout-fn
419-
on-visible-range-change!]
419+
on-visible-range-change!
420+
on-visible-range-event!]
420421
:or {debounce-ms visible-range-write-debounce-ms
421422
set-timeout-fn platform/set-timeout!
422423
clear-timeout-fn platform/clear-timeout!
423-
on-visible-range-change! (fn [] nil)}}]
424+
on-visible-range-change! (fn [] nil)
425+
on-visible-range-event! (fn [] nil)}}]
424426
(let [persist-visible-range!* (or persist-visible-range-fn
425427
(if storage-set!
426428
(fn [asset* timeframe* range-data]
@@ -455,11 +457,13 @@
455457
(when-let [range-data (visible-range-from-time-scale time-scale)]
456458
(queue-persist! range-data)))
457459
logical-handler (fn [range]
460+
(on-visible-range-event!)
458461
(notify-visible-range-change!)
459462
(if-let [range-data (range-candidate->data :logical range)]
460463
(queue-persist! range-data)
461464
(persist-current!)))
462465
time-handler (fn [range]
466+
(on-visible-range-event!)
463467
(notify-visible-range-change!)
464468
(if-let [range-data (range-candidate->data :time range)]
465469
(queue-persist! range-data)

src/hyperopen/views/trading_chart/vm.cljs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
indicator-runtime-ready?
8989
on-hide-volume-indicator
9090
on-history-backfill-request
91+
history-backfill-loading?
9192
active-asset
9293
candle-data
9394
on-liquidation-drag-preview
@@ -103,6 +104,7 @@
103104
:show-fill-markers? show-fill-markers?
104105
:on-hide-volume-indicator on-hide-volume-indicator
105106
:on-history-backfill-request on-history-backfill-request
107+
:history-backfill-loading? history-backfill-loading?
106108
:persistence-deps {:asset active-asset
107109
:candles candle-data}
108110
:on-liquidation-drag-preview on-liquidation-drag-preview
@@ -214,6 +216,8 @@
214216
dispatch-fn
215217
active-asset
216218
selected-timeframe)
219+
history-backfill-loading? (pos? (or (get-in state [:chart-options :history-backfill-pending-count])
220+
0))
217221
symbol (or active-asset "")
218222
timeframe-label (str/upper-case (name selected-timeframe))
219223
price-decimals (or (:price-decimals active-market)
@@ -229,6 +233,7 @@
229233
indicator-runtime-ready?
230234
on-hide-volume-indicator
231235
on-history-backfill-request
236+
history-backfill-loading?
232237
active-asset
233238
candle-data
234239
on-liquidation-drag-preview

src/styles/surfaces/chart.css

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,74 @@
1212
cursor: crosshair;
1313
}
1414

15+
.trading-chart-host--history-loading::before {
16+
content: "";
17+
position: absolute;
18+
top: 0;
19+
bottom: 0;
20+
left: 0;
21+
width: 16px;
22+
z-index: 12;
23+
pointer-events: none;
24+
background:
25+
linear-gradient(
26+
180deg,
27+
transparent 0%,
28+
transparent 6%,
29+
rgba(153, 246, 228, 0.94) 20%,
30+
rgba(20, 184, 166, 0.96) 36%,
31+
rgba(153, 246, 228, 0.94) 52%,
32+
transparent 68%,
33+
transparent 100%
34+
) left 0 / 3px 170% no-repeat,
35+
linear-gradient(90deg, rgba(20, 184, 166, 0.13), transparent 100%) left top / 16px 100% no-repeat,
36+
linear-gradient(180deg, rgba(20, 184, 166, 0.24), rgba(20, 184, 166, 0.08)) left top / 1px 100% no-repeat;
37+
filter: drop-shadow(0 0 7px rgba(20, 184, 166, 0.4));
38+
animation: chart-history-load-edge 1.18s cubic-bezier(0.4, 0, 0.2, 1) infinite;
39+
}
40+
41+
.trading-chart-host--history-loading::after {
42+
content: "Loading older candles";
43+
position: absolute;
44+
top: 12px;
45+
left: 12px;
46+
z-index: 13;
47+
pointer-events: none;
48+
max-width: min(240px, calc(100% - 24px));
49+
padding: 5px 8px;
50+
border: 1px solid rgba(20, 184, 166, 0.28);
51+
border-radius: 6px;
52+
background: rgba(7, 17, 20, 0.82);
53+
color: rgb(153, 246, 228);
54+
font-size: 11px;
55+
font-weight: 600;
56+
line-height: 1.1;
57+
letter-spacing: 0;
58+
white-space: nowrap;
59+
overflow: hidden;
60+
text-overflow: ellipsis;
61+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);
62+
}
63+
64+
@keyframes chart-history-load-edge {
65+
0% {
66+
background-position: left -115%, left top, left top;
67+
opacity: 0.7;
68+
}
69+
70+
58% {
71+
opacity: 1;
72+
}
73+
74+
100% {
75+
background-position: left 155%, left top, left top;
76+
opacity: 0.7;
77+
}
78+
}
79+
80+
@media (prefers-reduced-motion: reduce) {
81+
.trading-chart-host--history-loading::before {
82+
animation: none;
83+
}
84+
}
1585
}

test/hyperopen/runtime/app_effects_test.cljs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,36 @@
5656
{:coin "SPY" :interval :1d :bars 50 :end-time-ms 123456789}]
5757
@calls))))
5858

59+
(deftest fetch-candle-snapshot-tracks-historical-backfill-pending-state-test
60+
(async done
61+
(let [resolve-request (atom nil)
62+
store (atom {:active-asset "BTC"
63+
:chart-options {}})
64+
request-fn (fn [& _]
65+
(js/Promise.
66+
(fn [resolve _reject]
67+
(reset! resolve-request resolve))))]
68+
(-> (app-effects/fetch-candle-snapshot!
69+
{:store store
70+
:coin "BTC"
71+
:interval :1d
72+
:bars 552
73+
:end-time-ms 1699999999999
74+
:log-fn (fn [& _] nil)
75+
:request-candle-snapshot-fn request-fn
76+
:apply-candle-snapshot-success (fn [state coin interval rows]
77+
(assoc-in state [:candles coin interval] rows))
78+
:apply-candle-snapshot-error (fn [state coin interval err]
79+
(assoc-in state [:candles coin interval :error] (str err)))})
80+
(.then (fn [_rows]
81+
(is (= 0 (get-in @store [:chart-options :history-backfill-pending-count])))
82+
(done)))
83+
(.catch (fn [err]
84+
(is false (str "Unexpected error: " err))
85+
(done))))
86+
(is (= 1 (get-in @store [:chart-options :history-backfill-pending-count])))
87+
(@resolve-request [{:t 1}]))))
88+
5989
(deftest fetch-candle-snapshot-skips-when-request-is-inactive-test
6090
(async done
6191
(let [calls (atom 0)

test/hyperopen/views/trading_chart/core_test.cljs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,25 @@
411411
(chart-core/chart-canvas candle-data-new-identity :candlestick active-indicators-b legend-meta :4h chart-runtime-options)
412412
(is (= 4 @calls*) "new candle data identity should recompute indicator output"))))
413413

414+
(deftest chart-canvas-renders-history-backfill-loading-affordance-test
415+
(let [candle-data [{:time 1700000000 :open 100 :high 101 :low 99 :close 100 :volume 10}]
416+
legend-meta {:symbol "BTC"
417+
:timeframe-label "1D"
418+
:venue "Hyperopen"
419+
:candle-data candle-data}
420+
canvas (chart-core/chart-canvas
421+
candle-data
422+
:candlestick
423+
{}
424+
legend-meta
425+
:1d
426+
{:history-backfill-loading? true})]
427+
(is (some #{"trading-chart-host--history-loading"}
428+
(collect-all-classes canvas)))
429+
(is (true? (get-in canvas [1 :aria-busy])))
430+
(is (= "BTC price chart, 1D timeframe, loading older candles"
431+
(get-in canvas [1 :aria-label])))))
432+
414433
(deftest chart-canvas-mount-uses-volume-chart-creation-when-no-indicators-test
415434
(let [node #js {}
416435
candle-data [{:time 1700000000 :open 100 :high 101 :low 99 :close 100 :volume 10}]

0 commit comments

Comments
 (0)