Charts

StockChart

A full trading chart: candles, line or area, volume, range buttons, a crosshair with the OHLC readout, wheel zoom and drag pan, indicators and RSI and MACD panes.

#chart#data#trading#dashboard#interactive#keyboard

Demo

Preview

ACME · hourly

$203.42-0.93%past 3M · Jun 15, 10:00 – Sep 11, 15:00

Drag to pan and use the wheel to zoom; focused, ← → move the crosshair, + − zoom, Esc lets go.

ACME · hourly, the bars in view
TimeOpenHighLowCloseVolume
Jun 15, 10:00$205.33$207.41$203.75$206.00389,170
Jun 15, 13:00$205.43$206.98$202.64$205.43542,563
Jun 15, 16:00$205.81$208.04$204.42$205.69411,831
Jun 16, 12:00$205.31$207.84$203.70$205.30482,152
Jun 16, 15:00$204.83$208.02$203.87$206.82459,752
Jun 17, 11:00$207.18$209.02$204.99$207.47474,206
Jun 17, 14:00$207.94$212.68$206.38$210.74447,077
Jun 18, 10:00$210.13$212.77$209.48$210.39376,689
Jun 18, 13:00$210.17$212.43$206.94$208.86527,841
Jun 18, 16:00$208.34$209.58$204.69$205.52438,369
Jun 19, 12:00$205.10$207.78$203.56$206.89397,654
Jun 19, 15:00$206.37$208.35$203.94$207.48500,623
Jun 22, 11:00$207.26$209.65$204.68$205.83547,211
Jun 22, 14:00$206.17$210.10$204.71$208.64438,295
Jun 23, 10:00$208.42$211.57$206.74$209.33455,098
Jun 23, 13:00$208.85$209.06$205.18$207.02625,623
Jun 23, 16:00$206.97$210.06$205.36$208.86435,827
Jun 24, 12:00$208.96$210.74$207.08$209.82365,080
Jun 24, 15:00$210.00$210.67$207.84$208.15477,506
Jun 25, 11:00$208.01$210.75$206.53$210.31439,908
Jun 25, 14:00$210.21$211.84$208.85$210.63437,410
Jun 26, 10:00$210.66$213.74$208.73$211.93609,588
Jun 26, 13:00$212.20$213.04$209.21$210.43517,918
Jun 26, 16:00$210.53$212.47$209.29$210.48397,312
Jun 29, 12:00$210.10$211.73$207.67$209.42478,922
Jun 29, 15:00$209.62$210.75$207.00$208.25533,441
Jun 30, 11:00$208.73$212.01$207.80$210.97542,837
Jun 30, 14:00$211.07$212.81$208.12$210.20501,256
Jul 1, 10:00$210.60$211.49$207.74$209.87554,308
Jul 1, 13:00$209.62$211.22$208.68$208.73427,385
Jul 1, 16:00$208.36$208.75$205.24$207.40446,861
Jul 2, 12:00$207.53$208.67$203.92$205.93403,876
Jul 2, 15:00$206.28$207.95$204.66$206.78570,426
Jul 3, 11:00$206.26$208.44$205.05$205.93631,830
Jul 3, 14:00$205.52$208.62$204.02$207.64544,426
Jul 6, 10:00$207.47$209.87$205.28$208.74466,994
Jul 6, 13:00$208.23$210.36$205.77$208.44487,248
Jul 6, 16:00$208.97$211.43$206.45$209.54552,631
Jul 7, 12:00$208.92$210.71$206.15$207.79383,055
Jul 7, 15:00$207.28$208.55$205.22$207.04459,026
Jul 8, 11:00$207.26$209.72$205.03$208.04466,939
Jul 8, 14:00$208.19$209.06$205.52$207.19514,876
Jul 9, 10:00$206.68$210.70$205.12$210.00521,216
Jul 9, 13:00$210.27$214.04$208.67$212.64577,917
Jul 9, 16:00$212.57$213.60$209.23$210.08436,484
Jul 10, 12:00$209.93$211.29$207.35$209.81451,136
Jul 10, 15:00$210.12$211.22$206.82$208.39508,731
Jul 13, 11:00$207.83$210.54$205.93$209.46523,771
Jul 13, 14:00$209.77$211.52$207.33$209.54429,506
Jul 14, 10:00$209.34$210.58$205.62$207.32423,789
Jul 14, 13:00$207.67$209.71$206.05$206.84395,729
Jul 14, 16:00$207.01$208.66$205.63$205.99447,116
Jul 15, 12:00$205.51$208.08$204.62$207.06404,341
Jul 15, 15:00$207.01$208.28$205.23$206.97428,359
Jul 16, 11:00$207.29$209.00$205.02$206.95457,467
Jul 16, 14:00$207.10$208.61$206.01$207.70384,284
Jul 17, 10:00$207.67$210.10$206.86$208.96414,363
Jul 17, 13:00$208.37$209.19$206.20$208.72414,992
Jul 17, 16:00$209.03$210.18$207.39$209.58480,170
Jul 20, 12:00$209.46$211.26$206.36$208.35377,837
Jul 20, 15:00$208.92$211.42$207.04$208.49359,569
Jul 21, 11:00$208.47$209.98$205.14$207.07496,584
Jul 21, 14:00$207.66$208.38$204.74$206.67650,102
Jul 22, 10:00$206.06$208.52$205.38$206.74411,684
Jul 22, 13:00$206.75$209.30$204.83$205.98414,123
Jul 22, 16:00$205.39$207.52$202.95$204.45579,112
Jul 23, 12:00$204.04$205.96$201.39$203.70404,129
Jul 23, 15:00$203.89$205.92$202.49$204.66477,758
Jul 24, 11:00$205.17$207.04$203.43$205.63544,013
Jul 24, 14:00$205.46$208.55$203.70$205.61436,764
Jul 27, 10:00$205.41$207.93$203.71$205.67503,989
Jul 27, 13:00$205.81$207.15$203.19$204.80367,338
Jul 27, 16:00$204.51$204.97$201.68$202.88433,370
Jul 28, 12:00$202.53$203.15$199.60$201.12531,550
Jul 28, 15:00$201.56$204.69$201.00$203.95550,025
Jul 29, 11:00$204.09$205.57$202.62$203.65451,265
Jul 29, 14:00$204.11$207.39$203.04$203.81520,764
Jul 30, 10:00$203.33$203.87$201.00$202.71429,598
Jul 30, 13:00$202.19$205.18$200.51$202.87549,967
Jul 30, 16:00$202.54$202.61$198.82$200.11380,319
Jul 31, 12:00$200.52$202.77$198.80$200.23455,050
Jul 31, 15:00$200.40$203.32$198.81$201.10392,447
Aug 3, 11:00$200.75$203.31$198.91$201.20455,064
Aug 3, 14:00$201.43$203.31$199.97$201.21484,685
Aug 4, 10:00$200.66$202.04$198.37$200.47510,445
Aug 4, 13:00$200.00$201.76$198.63$199.88394,390
Aug 4, 16:00$200.39$201.48$197.53$199.38393,072
Aug 5, 12:00$199.71$203.57$197.79$201.76505,546
Aug 5, 15:00$201.67$203.99$200.07$203.12502,302
Aug 6, 11:00$203.51$204.41$200.01$201.97606,516
Aug 6, 14:00$202.02$202.77$199.83$201.56650,543
Aug 7, 10:00$200.96$203.83$199.55$200.23491,647
Aug 7, 13:00$200.42$201.28$198.64$199.27501,993
Aug 7, 16:00$199.71$201.38$198.14$199.89611,813
Aug 10, 12:00$199.84$201.92$198.18$198.80592,060
Aug 10, 15:00$198.42$199.23$196.09$198.70437,100
Aug 11, 11:00$199.07$200.69$197.01$198.80486,889
Aug 11, 14:00$199.07$200.09$195.04$196.13479,915
Aug 12, 10:00$195.78$197.56$193.10$194.48458,877
Aug 12, 13:00$194.95$196.97$194.14$195.21464,605
Aug 12, 16:00$195.70$198.18$194.51$195.66552,940
Aug 13, 12:00$195.42$196.38$192.24$195.26485,423
Aug 13, 15:00$195.56$197.43$193.89$195.54518,947
Aug 14, 11:00$196.11$196.13$192.00$193.73618,150
Aug 14, 14:00$193.63$194.49$190.36$193.11526,194
Aug 17, 10:00$193.35$195.77$191.71$193.43478,544
Aug 17, 13:00$193.60$194.98$192.22$193.26502,313
Aug 17, 16:00$193.71$196.48$192.48$194.72473,482
Aug 18, 12:00$194.36$198.13$192.97$196.48502,733
Aug 18, 15:00$196.56$198.62$195.82$196.49483,257
Aug 19, 11:00$196.39$198.53$195.23$198.14607,092
Aug 19, 14:00$198.52$201.13$197.05$198.16460,676
Aug 20, 10:00$197.60$199.19$195.94$198.27552,396
Aug 20, 13:00$198.33$199.91$194.40$195.34473,418
Aug 20, 16:00$195.19$196.81$193.53$195.39517,623
Aug 21, 12:00$195.32$197.18$193.36$194.36485,337
Aug 21, 15:00$193.89$195.46$193.10$193.97585,465
Aug 24, 11:00$194.39$194.69$192.04$193.37438,174
Aug 24, 14:00$193.68$196.03$192.61$193.99557,874
Aug 25, 10:00$193.77$196.83$191.98$193.98605,190
Aug 25, 13:00$194.54$197.51$192.71$195.05370,976
Aug 25, 16:00$195.00$196.10$192.31$194.86493,266
Aug 26, 12:00$195.00$195.40$190.51$192.25522,897
Aug 26, 15:00$192.33$195.42$192.11$194.60460,592
Aug 27, 11:00$194.66$198.63$193.19$197.37576,259
Aug 27, 14:00$197.79$199.60$194.87$198.69489,414
Aug 28, 10:00$199.10$201.01$196.65$199.83543,538
Aug 28, 13:00$199.48$201.05$198.17$199.29405,072
Aug 28, 16:00$199.76$200.27$195.46$197.14436,355
Aug 31, 12:00$197.45$201.61$195.78$200.05425,909
Aug 31, 15:00$200.33$202.22$197.79$199.22442,773
Sep 1, 11:00$199.32$201.26$198.18$200.88486,233
Sep 1, 14:00$201.32$203.50$198.36$199.87450,486
Sep 2, 10:00$199.68$202.29$197.73$200.22513,599
Sep 2, 13:00$199.68$201.95$199.12$201.08484,469
Sep 2, 16:00$201.16$203.99$200.29$202.22490,762
Sep 3, 12:00$202.03$205.73$201.04$203.75485,690
Sep 3, 15:00$204.31$205.46$200.76$202.08499,932
Sep 4, 11:00$201.65$204.23$200.29$202.18573,302
Sep 4, 14:00$201.94$205.62$201.12$204.34508,336
Sep 7, 10:00$204.36$206.39$202.77$203.32416,630
Sep 7, 13:00$203.10$204.51$201.23$203.75413,519
Sep 7, 16:00$203.35$206.07$201.80$203.82495,846
Sep 8, 12:00$203.56$206.87$201.67$205.42444,656
Sep 8, 15:00$205.34$208.24$203.84$206.30456,896
Sep 9, 11:00$206.75$209.89$205.13$208.22391,923
Sep 9, 14:00$208.58$209.89$205.80$206.87444,345
Sep 10, 10:00$206.61$206.95$203.50$205.02545,919
Sep 10, 13:00$204.76$206.17$202.93$205.09511,708
Sep 10, 16:00$205.12$206.79$200.40$202.99511,855
Sep 11, 12:00$203.45$205.25$201.76$202.81413,866
Sep 11, 15:00$202.77$204.14$200.60$203.42322,990

Showing 3M.

Globex · daily, line with RSI and MACD

$56.20+16.94%in view · Oct 1, 2025 – Sep 28, 2026
RSI 14 39.5
MACD 12 26 9 -0.6467

Drag to pan and use the wheel to zoom; focused, ← → move the crosshair, + − zoom, Esc lets go.

Globex · daily, line with RSI and MACD, the bars in view
TimeOpenHighLowCloseVolume
Oct 1, 2025$48.06$48.73$47.62$47.814,003,400
Oct 3, 2025$47.93$48.19$47.50$47.942,842,421
Oct 7, 2025$47.82$48.27$47.42$47.732,868,087
Oct 9, 2025$47.74$48.70$47.32$47.784,441,731
Oct 13, 2025$47.70$48.71$47.33$47.823,102,422
Oct 15, 2025$47.92$48.09$47.51$47.773,424,771
Oct 17, 2025$47.78$48.12$47.22$47.573,738,316
Oct 21, 2025$47.57$48.74$47.30$48.624,446,059
Oct 23, 2025$48.51$49.29$48.16$48.983,175,256
Oct 27, 2025$48.88$49.33$47.98$48.153,641,353
Oct 29, 2025$48.20$48.41$46.93$47.254,091,924
Oct 31, 2025$47.33$48.22$47.12$48.193,382,236
Nov 4, 2025$48.18$48.44$47.57$48.343,072,533
Nov 6, 2025$48.32$48.68$47.62$48.093,585,453
Nov 10, 2025$48.22$49.36$47.86$48.964,120,348
Nov 12, 2025$48.91$49.77$48.42$49.623,071,517
Nov 14, 2025$49.54$50.01$48.39$48.752,492,919
Nov 18, 2025$48.80$49.13$47.98$48.422,918,577
Nov 20, 2025$48.49$48.51$47.72$47.842,855,397
Nov 24, 2025$47.80$48.35$47.73$48.273,312,269
Nov 26, 2025$48.25$48.89$47.88$48.612,453,326
Nov 28, 2025$48.72$48.83$47.41$47.752,946,453
Dec 2, 2025$47.73$48.14$47.21$47.693,623,788
Dec 4, 2025$47.64$47.98$46.76$47.033,332,626
Dec 8, 2025$47.00$47.60$46.34$47.243,778,052
Dec 10, 2025$47.10$47.51$46.02$46.083,373,279
Dec 12, 2025$46.09$46.31$44.60$44.784,895,950
Dec 16, 2025$44.85$46.28$44.69$45.863,593,573
Dec 18, 2025$45.99$46.43$44.88$45.503,881,614
Dec 22, 2025$45.40$46.31$45.04$45.263,991,818
Dec 24, 2025$45.25$46.09$45.03$45.263,566,202
Dec 26, 2025$45.34$46.31$44.91$45.943,437,960
Dec 30, 2025$45.82$46.89$45.55$46.453,268,652
Jan 1, 2026$46.39$47.43$46.28$46.953,105,655
Jan 5, 2026$46.91$47.18$46.22$46.503,294,038
Jan 7, 2026$46.52$46.63$45.75$46.084,184,454
Jan 9, 2026$45.98$46.15$44.99$45.103,613,736
Jan 13, 2026$45.09$45.13$44.36$44.854,154,950
Jan 15, 2026$44.97$45.15$44.45$44.823,664,768
Jan 19, 2026$44.93$45.68$44.68$45.113,531,391
Jan 21, 2026$45.23$45.36$43.82$44.014,194,944
Jan 23, 2026$43.89$44.23$43.41$43.682,748,345
Jan 27, 2026$43.62$43.74$43.21$43.463,456,250
Jan 29, 2026$43.44$43.83$42.88$43.263,720,010
Feb 2, 2026$43.36$44.71$43.06$44.292,693,097
Feb 4, 2026$44.17$45.04$43.83$44.913,523,597
Feb 6, 2026$44.79$44.90$44.22$44.482,667,761
Feb 10, 2026$44.50$45.00$44.11$44.693,692,994
Feb 12, 2026$44.72$45.81$44.54$45.704,112,354
Feb 16, 2026$45.70$46.29$45.12$45.493,778,287
Feb 18, 2026$45.38$46.81$45.30$46.443,150,073
Feb 20, 2026$46.47$47.85$46.42$47.563,632,196
Feb 24, 2026$47.48$48.08$47.32$47.533,293,943
Feb 26, 2026$47.40$48.43$47.07$47.744,470,325
Mar 2, 2026$47.77$48.44$47.37$48.023,147,931
Mar 4, 2026$47.93$48.53$47.61$48.402,324,389
Mar 6, 2026$48.38$48.56$46.67$46.833,743,224
Mar 10, 2026$46.75$47.27$46.07$46.282,743,590
Mar 12, 2026$46.23$47.16$45.54$46.823,565,180
Mar 16, 2026$46.77$47.64$46.51$47.332,563,098
Mar 18, 2026$47.25$47.61$46.40$47.083,230,180
Mar 20, 2026$47.08$47.38$46.39$46.473,525,049
Mar 24, 2026$46.42$47.88$46.34$47.434,152,610
Mar 26, 2026$47.33$47.92$47.11$47.374,474,808
Mar 30, 2026$47.51$47.69$46.54$47.283,878,900
Apr 1, 2026$47.17$47.88$46.83$47.833,303,289
Apr 3, 2026$47.85$48.18$47.41$47.573,135,197
Apr 7, 2026$47.60$48.03$46.53$46.823,837,456
Apr 9, 2026$46.95$47.49$46.79$47.263,556,314
Apr 13, 2026$47.25$48.20$46.84$47.843,606,917
Apr 15, 2026$47.71$48.08$47.06$47.843,680,880
Apr 17, 2026$47.77$49.08$47.52$48.712,703,485
Apr 21, 2026$48.76$49.68$48.44$48.633,659,139
Apr 23, 2026$48.68$48.99$48.32$48.904,119,642
Apr 27, 2026$49.02$49.77$48.97$49.523,337,790
Apr 29, 2026$49.38$50.20$49.23$49.893,259,150
May 1, 2026$49.79$50.34$49.54$49.953,009,964
May 5, 2026$49.88$50.89$49.87$50.373,123,092
May 7, 2026$50.46$51.19$50.08$51.012,963,583
May 11, 2026$51.02$51.78$50.25$51.343,339,406
May 13, 2026$51.19$52.41$50.99$52.383,769,254
May 15, 2026$52.25$53.90$52.04$53.583,835,103
May 19, 2026$53.60$54.03$52.86$53.293,065,713
May 21, 2026$53.26$54.54$53.00$54.063,335,218
May 25, 2026$54.10$55.17$53.70$54.693,617,439
May 27, 2026$54.70$55.54$54.17$55.312,988,318
May 29, 2026$55.38$56.46$54.98$56.444,173,303
Jun 2, 2026$56.33$57.99$55.78$56.894,621,580
Jun 4, 2026$57.03$57.11$55.64$56.232,869,670
Jun 8, 2026$56.39$57.63$56.04$56.543,737,876
Jun 10, 2026$56.69$57.33$56.16$56.572,812,886
Jun 12, 2026$56.41$57.17$56.08$56.533,152,256
Jun 16, 2026$56.44$56.94$54.72$55.164,037,230
Jun 18, 2026$55.02$55.62$54.69$54.832,846,949
Jun 22, 2026$54.67$56.58$54.61$56.144,071,706
Jun 24, 2026$56.26$56.40$54.93$55.342,677,140
Jun 26, 2026$55.39$56.26$55.05$56.214,771,589
Jun 30, 2026$56.32$56.80$55.49$55.853,797,111
Jul 2, 2026$55.93$56.63$55.58$56.413,337,880
Jul 6, 2026$56.27$56.73$54.79$54.963,604,465
Jul 8, 2026$55.03$56.90$54.90$56.803,325,856
Jul 10, 2026$56.70$57.24$55.44$55.523,200,763
Jul 14, 2026$55.64$56.06$54.55$55.714,168,877
Jul 16, 2026$55.61$56.80$55.34$55.914,916,578
Jul 20, 2026$55.90$56.15$54.82$55.723,400,282
Jul 22, 2026$55.77$56.99$55.72$56.083,542,425
Jul 24, 2026$56.14$57.09$55.85$56.752,930,121
Jul 28, 2026$56.71$58.15$56.62$57.962,992,583
Jul 30, 2026$57.83$58.47$57.20$57.644,259,940
Aug 3, 2026$57.63$59.60$57.42$59.183,235,493
Aug 5, 2026$59.06$60.77$58.82$60.553,868,656
Aug 7, 2026$60.59$61.59$60.07$60.512,959,913
Aug 11, 2026$60.48$62.04$60.18$60.683,339,835
Aug 13, 2026$60.82$61.78$60.22$61.633,627,353
Aug 17, 2026$61.62$62.07$59.81$60.373,783,811
Aug 19, 2026$60.42$60.95$59.34$60.494,543,698
Aug 21, 2026$60.63$60.98$59.71$60.533,403,106
Aug 25, 2026$60.40$61.38$59.87$60.293,424,215
Aug 27, 2026$60.42$61.07$60.04$60.612,347,049
Aug 31, 2026$60.79$61.13$59.62$59.763,712,492
Sep 2, 2026$59.63$60.43$59.27$59.833,633,156
Sep 4, 2026$59.67$59.99$57.95$58.294,139,036
Sep 8, 2026$58.15$58.15$56.26$56.762,944,749
Sep 10, 2026$56.92$57.30$55.07$55.592,807,156
Sep 14, 2026$55.49$57.44$55.16$57.033,431,098
Sep 16, 2026$57.00$58.16$56.72$56.974,052,518
Sep 18, 2026$56.87$57.29$55.97$57.293,043,195
Sep 22, 2026$57.42$57.95$57.07$57.303,398,372
Sep 24, 2026$57.37$57.67$56.15$56.883,125,174
Sep 28, 2026$57.01$57.58$55.84$56.203,210,502

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/stock_chart.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/stock_chart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/stock_chart.qwe
// Prices from a seeded random walk (Park–Miller), the same on every render: `count`// candles from the day `from` on, skipping weekends, daily or hourly (10:00 to 16:00),// rounded to `places` decimals.fn stock_chart_demo_walk(seed: int, count: int, start: num, from: str, hourly: bool = false, places: int = 2) -> list {    let s = seed    let rnd = () => {        s = s * 16807 % 2147483647        s / 2147483647    }    let scale = pow(10, places)    let cut = (v) => round(v * scale) / scale    let p = start    let day = from    let hour = 10    let out = []    while len(out) < count {        if date.weekday(day) <= 5 {            let open = cut(p * (1 + (rnd() - 0.5) * 0.006))            let close = cut(open * (1 + (rnd() - 0.485) * (hourly ? 0.012 : 0.035)))            let high = cut(max([open, close]) * (1 + rnd() * 0.01))            let low = cut(min([open, close]) * (1 - rnd() * 0.01))            let volume = round((hourly ? 90000 : 900000) * (1 + rnd() * 1.5 + abs(close - open) * 30 / open))            out = out | push({ time: hourly ? `${day}T${hour < 10 ? "0" : ""}${hour}:00:00Z` : day, open, high, low, close, volume })            p = close        }        if hourly && hour < 16 && date.weekday(day) <= 5 {            hour += 1        } else {            hour = 10            day = date.add_days(day, 1)        }    }    out}// A year and a quarter of hourly candles (314 trading days of 7), made in the browser as on the server, so the// page does not carry them.component StockChartDemoLive() {    let hourly = stock_chart_demo_walk(77, 2198, 141.2, "2025-07-01", true)    state chosen = "3M"    <div.demo-stack id="stock_chart-demo-main">        <StockChart label="ACME · hourly" candles={hourly} prefix="$" indicators={["sma20", "ema50"]}                    on_range={(r) => { chosen = r }}/>        <p.demo-note>{chosen == "" ? "Zoomed or panned by hand." : `Showing ${chosen}.`}</p>    </div>}let stock_chart_demo_daily = stock_chart_demo_walk(5, 260, 48.2, "2025-10-01")view StockChartDemo() =    <div.demo-stack>        <StockChartDemoLive/>        <div id="stock_chart-demo-daily">            <StockChart label="Globex · daily, line with RSI and MACD" candles={stock_chart_demo_daily} kind="line" period="All"                        indicators={["bb", "rsi", "macd"]} prefix="$" height={240}/>        </div>    </div>

Source

ui/stock_chart.qwe
// StockChart: a full trading chart. Candles, a line or an area (a switch), volume// under the price, range buttons (1D 1W 1M 3M 1Y All), a crosshair that follows the// pointer with the open, high, low, close, change and volume where it is, the wheel to// zoom and a drag to pan, indicators to turn on and off (SMA 20, EMA 50, Bollinger// bands, an RSI pane and a MACD pane), and the last price as a line and a label. The// keyboard does it all too: with the chart focused, the arrow keys move the crosshair// (and pan at the ends), + and − zoom, Home and End go to the first and last candle,// and Escape lets go; screen readers hear the candle the crosshair is on.////   <StockChart label="ACME" candles={bars} prefix="$"/>//   <StockChart label="BTC-USD" candles={hourly} kind="line" period="1W" indicators={["ema50", "rsi"]}//               decimals={0} on_range={(r) => { picked = r }}/>//// candles: a list of { time, open, high, low, close, volume }, oldest first; time is a// date ("2026-03-02") or a date-time ("2026-03-02T14:00:00Z", shown as written).// When more candles are in view than fit (about 160), neighbours are drawn as one// (the first open, the highest high, the lowest low, the last close, the volumes// added), and the readout says how many it holds. kind: "candles" (default), "line"// or "area" to start with. period: the range to start with ("3M"); a range that the// candles cannot fill (1D of daily candles) is disabled. indicators: those on to start// with, of "sma20", "ema50", "bb", "rsi" and "macd". height: the price pane's height// in pixels (340). prefix and suffix: around every price ("$"); decimals: digits after// the point of prices (2). label: what the chart shows, for screen readers and the// heading. on_range(range): called with the range picked ("1M"), or "" when the wheel,// a drag or the keyboard leaves the ranges.import ui/theme.qwe use ui_clslet stock_chart_css = css`.ui-stk { --ui-stk-1: light-dark(#2a78d6, #3987e5); --ui-stk-2: light-dark(#eb6834, #d95926);  --ui-stk-3: light-dark(#1baf7a, #199e70); --ui-stk-7: light-dark(#6250d6, #9085e9);  display: grid; gap: 10px; min-width: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-stk-up { --k: var(--ui-success); } .ui-stk-down { --k: var(--ui-danger); }.ui-stk-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }.ui-stk-name { margin: 0; font-size: 15px; font-weight: 650; }.ui-stk-last { font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-stk-move { color: var(--k); font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-stk-span { color: var(--ui-muted); font-size: 12px; }.ui-stk-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }.ui-stk-group { display: inline-flex; flex-wrap: wrap; gap: 2px; padding: 2px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-stk-group button { height: 26px; min-width: 34px; padding: 0 9px; border: 0; border-radius: 4px; background: none; color: var(--ui-muted);  font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-stk-group button:hover:not(:disabled) { color: var(--ui-fg); }.ui-stk-group button[aria-pressed="true"] { background: var(--ui-surface); color: var(--ui-fg); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }.ui-stk-group button:disabled { opacity: .4; cursor: not-allowed; }.ui-stk-group button:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-stk-ind button[aria-pressed="true"] { color: var(--c, var(--ui-fg)); }.ui-stk-chart { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; }.ui-stk-panes { position: relative; touch-action: none; cursor: crosshair; user-select: none; border-radius: 2px; }.ui-stk-panes:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-stk-dragging { cursor: grabbing; }.ui-stk-pane { position: relative; height: var(--ph); border-bottom: 1px solid var(--ui-border); }.ui-stk-pane + .ui-stk-pane { border-top: 0; }.ui-stk-pname { position: absolute; z-index: 3; left: 6px; top: 4px; color: var(--ui-muted); font-size: 11px; font-weight: 600; pointer-events: none; }.ui-stk-grid { position: absolute; left: 0; right: 0; height: 1px; background: color-mix(in srgb, var(--ui-border) 65%, transparent); }.ui-stk-zero { background: var(--ui-strong); }.ui-stk-level { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--ui-strong); }.ui-stk-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }.ui-stk-svg path { fill: none; stroke-linejoin: round; stroke-linecap: round; }.ui-stk-svg .ui-stk-body { fill: var(--k); stroke: none; }.ui-stk-svg .ui-stk-wick { stroke: var(--k); stroke-width: 1; }.ui-stk-svg .ui-stk-vol { fill: var(--k); stroke: none; opacity: .28; }.ui-stk-svg .ui-stk-close { stroke: var(--ui-stk-1); stroke-width: 2; }.ui-stk-svg .ui-stk-fill { fill: var(--ui-stk-1); opacity: .1; stroke: none; }.ui-stk-svg .ui-stk-sma { stroke: var(--ui-stk-2); stroke-width: 1.5; }.ui-stk-svg .ui-stk-ema { stroke: var(--ui-stk-7); stroke-width: 1.5; }.ui-stk-svg .ui-stk-bb { stroke: var(--ui-stk-3); stroke-width: 1.25; }.ui-stk-svg .ui-stk-bbmid { stroke: var(--ui-stk-3); stroke-width: 1; stroke-dasharray: 4 3; }.ui-stk-svg .ui-stk-band { fill: var(--ui-stk-3); opacity: .09; stroke: none; }.ui-stk-svg .ui-stk-rsi { stroke: var(--ui-stk-7); stroke-width: 1.5; }.ui-stk-svg .ui-stk-macd { stroke: var(--ui-stk-1); stroke-width: 1.5; }.ui-stk-svg .ui-stk-signal { stroke: var(--ui-stk-2); stroke-width: 1.5; }.ui-stk-svg .ui-stk-hist { fill: var(--k); stroke: none; opacity: .55; }.ui-stk-band-rsi { position: absolute; left: 0; right: 0; background: color-mix(in srgb, var(--ui-stk-7) 8%, transparent); }.ui-stk-lastline { position: absolute; z-index: 1; left: 0; right: 0; border-top: 1px dashed var(--k); opacity: .8; pointer-events: none; }.ui-stk-vline { position: absolute; z-index: 4; top: 0; bottom: 0; width: 0; border-left: 1px dashed var(--ui-muted); pointer-events: none; }.ui-stk-hline { position: absolute; z-index: 4; left: 0; right: 0; height: 0; border-top: 1px dashed var(--ui-muted); pointer-events: none; }.ui-stk-read { position: absolute; z-index: 5; top: 6px; left: 6px; right: 6px; display: flex; flex-wrap: wrap; gap: 2px 10px; pointer-events: none;  color: var(--ui-muted); font-size: 12px; font-variant-numeric: tabular-nums; }.ui-stk-read > span { padding: 1px 0; background: color-mix(in srgb, var(--ui-bg) 75%, transparent); white-space: nowrap; }.ui-stk-read b { color: var(--ui-fg); font-weight: 600; }.ui-stk-read .ui-stk-up b, .ui-stk-read .ui-stk-down b { color: var(--k); }.ui-stk-read .ui-stk-when { color: var(--ui-fg); font-weight: 600; }.ui-stk-axis { position: relative; }.ui-stk-ax { position: relative; height: var(--ph); }.ui-stk-ax span { position: absolute; left: 0; transform: translateY(-50%); color: var(--ui-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-stk-ax .ui-stk-tag { z-index: 2; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--k); color: var(--ui-bg); font-weight: 600; }.ui-stk-ax .ui-stk-cross { z-index: 3; left: -4px; padding: 1px 4px; border-radius: 3px; background: var(--ui-fg); color: var(--ui-bg); font-weight: 600; }.ui-stk-x { position: relative; height: 18px; margin-top: 4px; }.ui-stk-x span { position: absolute; top: 0; transform: translateX(-50%); color: var(--ui-muted); white-space: nowrap; }.ui-stk-x .ui-stk-first { transform: none; }.ui-stk-x .ui-stk-end { transform: translateX(-100%); }.ui-stk-x .ui-stk-cross { z-index: 2; padding: 1px 5px; border-radius: 3px; background: var(--ui-fg); color: var(--ui-bg); font-weight: 600; }.ui-stk-help { margin: 0; color: var(--ui-muted); font-size: 12px; }.ui-stk-help kbd { padding: 0 4px; border: 1px solid var(--ui-border); border-radius: 3px; background: var(--ui-subtle); font: 11px/1.4 var(--ui-mono); }`// n (a whole number, 0 or more) with at least `width` digits.fn stock_chart_pad(n: int, width: int) -> str {    let s = str(n)    len(s) < width ? repeat("0", width - len(s)) + s : s}// A whole number with commas: 1234567 is "1,234,567".fn stock_chart_group(n: int) -> str =    n < 1000 ? str(n) : stock_chart_group(floor(n / 1000)) + "," + stock_chart_pad(n % 1000, 3)// A value as it reads, with commas and `places` digits after the point (up to two as// needed when that is null): 48210.5 is "48,210.50" with 2. Worked out in whole// numbers, so the server and the browser write the same text.fn stock_chart_num(v: num, places: int? = null) -> str {    let digits = places == null ? 2 : (places < 0 ? 0 : (places > 6 ? 6 : places))    let scale = [1, 10, 100, 1000, 10000, 100000, 1000000][digits]    let units = round(abs(v) * scale)    let frac = units % scale    let tail = digits == 0 || (places == null && frac == 0) ? ""        : (places == null && frac % 10 == 0 ? `.${floor(frac / 10)}` : `.${stock_chart_pad(frac, digits)}`)    `${v < 0 && units > 0 ? "-" : ""}${stock_chart_group(floor(units / scale))}${tail}`}// A large number, short: 1500 is "1.5k", 2000000 is "2M", 0.25 is "0.25".fn stock_chart_short(v: num) -> str {    let a = abs(v)    let unit = a >= 1000000000 ? 1000000000 : (a >= 1000000 ? 1000000 : (a >= 1000 ? 1000 : 1))    let tenths = round(a * 10 / unit)    let mark = unit == 1000000000 ? "B" : (unit == 1000000 ? "M" : "k")    unit == 1 ? stock_chart_num(v)        : `${v < 0 ? "-" : ""}${stock_chart_group(floor(tenths / 10))}${tenths % 10 != 0 ? `.${tenths % 10}` : ""}${mark}`}// A change in percent, signed, to two decimals: "+1.25%", "-0.40%".fn stock_chart_signed(p: num) -> str = `${p > 0 ? "+" : (p < 0 && round(abs(p) * 100) > 0 ? "-" : "")}${stock_chart_num(abs(p), 2)}%`// A coordinate out of 100 (or a length), to two decimals.fn stock_chart_pct(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// The axis from lo to hi, widened to round ticks: about `steps` steps of 1, 2, 2.5 or 5 x 10^n.fn stock_chart_scale(lo: num, hi: num, steps: int = 5) -> map {    let span = hi - lo == 0 ? (lo == 0 ? 1 : abs(lo) / 10) : hi - lo    let raw = span / steps    let mag = 1    while mag * 10 <= raw { mag = mag * 10 }    while mag > raw { mag = mag / 10 }    let r = raw / mag    let step = (r <= 1 ? 1 : (r <= 2 ? 2 : (r <= 2.5 ? 2.5 : (r <= 5 ? 5 : 10)))) * mag    let a = floor(lo / step + 0.000001)    let b0 = ceil(hi / step - 0.000001)    let b = b0 == a ? a + 1 : b0    { lo: a * step, hi: b * step, ticks: for i in range(b - a + 1) => (a + i) * step }}// A candle's time as it reads: "Mar 2, 2026" for a date, "Mar 2, 14:30" for a date-time.fn stock_chart_when(t) -> str =    type_of(t) != "str" || !date.valid(t) ? str(t) : (len(t) > 10 ? date.format(t, "MMM D, HH:mm") : date.format(t, "MMM D, YYYY"))// Where the time axis is labelled: about `limit` of { i, text }, at the starts of years,// months or days when the times cross them ("2026", "Mar", "Mar 2"), else every few// candles ("14:30", "Mar 2").fn stock_chart_ticks(times: list, limit: int) -> list {    let n = len(times)    let thin = (idx: list) => {        let step = ceil(len(idx) / limit)        range(len(idx)) | filter((k) => k % step == 0) | map((k) => idx[k])    }    let spaced = (fmt_at) => {        let step = ceil(n / limit)        range(n) | filter((i) => i % step == floor(step / 2)) | map((i) => { i, text: fmt_at(i) })    }    if n == 0 { return [] }    if !all(times, (t) => type_of(t) == "str" && date.valid(t)) { return spaced((i) => str(times[i])) }    let days = times | map((t) => slice(t, 0, 10))    let changes = (keys: list) => range(1, n) | filter((i) => keys[i] != keys[i - 1])    let day_starts = changes(days)    let month_starts = changes(days | map((d) => slice(d, 0, 7)))    let year_starts = changes(days | map((d) => slice(d, 0, 4)))    let picked = if any(times, (t) => len(t) > 10) {        len(day_starts) >= 3            ? thin(day_starts) | map((i) => { i, text: date.format(days[i], "MMM D") })            : spaced((i) => index_of(day_starts, i) != null ? date.format(days[i], "MMM D") : date.format(times[i], "HH:mm"))    } else if len(year_starts) >= 3 && len(month_starts) > limit * 2 {        thin(year_starts) | map((i) => { i, text: slice(days[i], 0, 4) })    } else if len(month_starts) >= 2 {        thin(month_starts) | map((i) => { i, text: index_of(year_starts, i) != null ? slice(days[i], 0, 4) : date.format(days[i], "MMM") })    } else {        spaced((i) => date.format(days[i], "MMM D"))    }    // Starts that fall close together (a short month) keep the first.    let gap = n / (limit * 1.5)    reduce(picked, [], (kept, t) => len(kept) == 0 || t.i - kept[-1].i >= gap ? kept + [t] : kept)}// The simple moving average of `period` values, null until there are that many.fn stock_chart_sma(xs: list, period: int) -> list {    let out = []    let total = 0    for i in range(len(xs)) {        total += xs[i]        if i >= period { total -= xs[i - period] }        out = out | push(i + 1 >= period ? total / period : null)    }    out}// The exponential moving average of `period` values (k = 2 / (period + 1)), started// from the simple average of the first `period`; null before that.fn stock_chart_ema(xs: list, period: int) -> list {    let k = 2 / (period + 1)    let out = []    let prev = null    for i in range(len(xs)) {        if i + 1 == period {            prev = sum(take(xs, period)) / period        } else if i + 1 > period {            prev = xs[i] * k + prev * (1 - k)        }        out = out | push(prev)    }    out}// The RSI of each close (Wilder's smoothing), null for the first `period`.fn stock_chart_rsi(xs: list, period: int) -> list {    let out = []    let gain = 0    let loss = 0    for i in range(len(xs)) {        let d = i == 0 ? 0 : xs[i] - xs[i - 1]        let up = d > 0 ? d : 0        let down = d < 0 ? 0 - d : 0        if i > 0 && i <= period {            gain += up / period            loss += down / period        } else if i > period {            gain = (gain * (period - 1) + up) / period            loss = (loss * (period - 1) + down) / period        }        out = out | push(i < period ? null : (loss == 0 ? (gain == 0 ? 50 : 100) : 100 - 100 / (1 + gain / loss)))    }    out}// Bollinger bands: { mid, upper, lower }, `k` population deviations of `period` closes.fn stock_chart_bands(xs: list, period: int, k: num) -> map {    let mid = stock_chart_sma(xs, period)    let dev = for i in range(len(xs)) => mid[i] == null ? null        : sqrt(sum(slice(xs, i + 1 - period, i + 1) | map((v) => (v - mid[i]) * (v - mid[i]))) / period)    {        mid,        upper: for i in range(len(xs)) => mid[i] == null ? null : mid[i] + k * dev[i],        lower: for i in range(len(xs)) => mid[i] == null ? null : mid[i] - k * dev[i],    }}// MACD: { macd, signal, hist }, the fast EMA less the slow, its EMA, and their gap.fn stock_chart_macd(xs: list, fast: int, slow: int, signal: int) -> map {    let a = stock_chart_ema(xs, fast)    let b = stock_chart_ema(xs, slow)    let macd = for i in range(len(xs)) => a[i] == null || b[i] == null ? null : a[i] - b[i]    let from = index_of(macd | map((v) => v != null), true)    let sig0 = from == null ? [] : stock_chart_ema(drop(macd, from), signal)    let sig = for i in range(len(xs)) => from == null || i < from ? null : sig0[i - from]    { macd, signal: sig, hist: for i in range(len(xs)) => macd[i] == null || sig[i] == null ? null : macd[i] - sig[i] }}// A candle's time in milliseconds since 1970 (0 when it is not a date).fn stock_chart_ms(t) -> num = type_of(t) == "str" && date.valid(t) ? date.ms(t) : (type_of(t) == "int" || type_of(t) == "float" ? t : 0)// The candles of the last `days` days, as { start, count }; null when that is fewer than two// candles or every one of them (the range adds nothing).fn stock_chart_window(ms: list, days: num) -> map? {    let n = len(ms)    if n < 2 { return null }    let from = ms[n - 1] - days * 86400000    let start = index_of(ms | map((m) => m > from), true) ?? n    let count = n - start    count < 2 || count >= n ? null : { start, count }}// The candles from `start`, `count` of them, as at most about `most` bars: neighbours// drawn as one when there are more, each { i0, i1, time, open, high, low, close, volume }.fn stock_chart_buckets(candles: list, start: int, count: int, most: int) -> list {    let k = count > most ? ceil(count / most) : 1    for g in range(ceil(count / k)) => {        let i0 = start + g * k        let i1 = (g + 1) * k > count ? start + count - 1 : i0 + k - 1        let part = slice(candles, i0, i1 + 1)        { i0, i1, time: candles[i0].time, open: candles[i0].open, close: candles[i1].close,          high: max(part | map((c) => c.high)), low: min(part | map((c) => c.low)),          volume: sum(part | map((c) => c.volume ?? 0)) }    }}component StockChart(candles: list = [], kind: str = "candles", period: str = "3M", indicators: list = [],                     height: int = 340, prefix: str = "", suffix: str = "", decimals: int = 2,                     label: str = "Stock chart", on_range = null, class: str? = null) {    style global stock_chart_css    let n = len(candles)    let ms = candles | map((c) => stock_chart_ms(c.time))    let closes = candles | map((c) => c.close)    let sma20 = stock_chart_sma(closes, 20)    let ema50 = stock_chart_ema(closes, 50)    let bb = stock_chart_bands(closes, 20, 2)    let rsi14 = stock_chart_rsi(closes, 14)    let macd = stock_chart_macd(closes, 12, 26, 9)    let ranges = [{ name: "1D", days: 1 }, { name: "1W", days: 7 }, { name: "1M", days: 30 }, { name: "3M", days: 91 },                  { name: "1Y", days: 365 }, { name: "All", days: 0 }]    let window_of = (name: str) => {        let r = find(ranges, (x) => x.name == name)        r == null ? null : (r.days == 0 ? { start: 0, count: n } : stock_chart_window(ms, r.days))    }    let kinds = [{ id: "candles", name: "Candles" }, { id: "line", name: "Line" }, { id: "area", name: "Area" }]    let studies = [{ id: "sma20", name: "SMA 20", hue: 2 }, { id: "ema50", name: "EMA 50", hue: 7 }, { id: "bb", name: "Bollinger", hue: 3 },                   { id: "rsi", name: "RSI", hue: 7 }, { id: "macd", name: "MACD", hue: 1 }]    state shape = kind    state picked = window_of(period) != null ? period : "All"    state start = (window_of(period) ?? { start: 0 }).start    state count = (window_of(period) ?? { count: n }).count    state on = indicators    state hover = null    state at_y = null    state drag = null    state keyed = false    let fewest = n < 5 ? n : 5    let c = count < fewest ? fewest : (count > n ? n : count)    let s = start < 0 ? 0 : (start > n - c ? n - c : start)    let bars = stock_chart_buckets(candles, s, c, 160)    let nb = len(bars)    let per = c > 160 ? ceil(c / 160) : 1    let has = (id: str) => index_of(on, id) != null    let at = (vals: list) => bars | map((b) => vals[b.i1])    let known = (vals: list) => vals | filter((v) => v != null)    let in_view = flatten(bars | map((b) => shape == "candles" ? [b.low, b.high] : [b.close]))        + (has("sma20") ? known(at(sma20)) : []) + (has("ema50") ? known(at(ema50)) : [])        + (has("bb") ? known(at(bb.upper)) + known(at(bb.lower)) : [])    let sc = stock_chart_scale(nb == 0 ? 0 : min(in_view), nb == 0 ? 1 : max(in_view), 6)    // Prices take the pane from 3% to 79% down; the volume, the bottom 18%.    let y = (v) => 3 + (sc.hi - v) * 76 / (sc.hi - sc.lo)    let x = (j) => (j + 0.5) * 100 / nb    let w = nb == 0 ? 1 : 100 / nb    let pct = (v) => stock_chart_pct(v)    let price = (v) => v == null ? "—" : `${v < 0 ? "-" : ""}${prefix}${stock_chart_num(abs(v), decimals)}${suffix}`    let tick = (t) => `${t < 0 ? "-" : ""}${prefix}${stock_chart_num(abs(t))}`    let up = (b) => b.close >= b.open    let side = (ok: bool) => ok ? "ui-stk-up" : "ui-stk-down"    // The price pane's paths: bodies, wicks and volume of up or down bars, and lines.    let bodies = (rise: bool) => join(range(nb) | filter((j) => up(bars[j]) == rise) | map((j) => {        let b = bars[j]        let tall = abs(y(b.open) - y(b.close))        `M${pct(x(j) - w * 0.36)},${pct(y(max([b.open, b.close])))} h${pct(w * 0.72)} v${pct(tall < 0.4 ? 0.4 : tall)} h-${pct(w * 0.72)} Z`    }), " ")    let wicks = (rise: bool) => join(range(nb) | filter((j) => up(bars[j]) == rise)        | map((j) => `M${pct(x(j))},${pct(y(bars[j].high))} V${pct(y(bars[j].low))}`), " ")    let vtop = max([1] + (bars | map((b) => b.volume)))    let vols = (rise: bool) => join(range(nb) | filter((j) => up(bars[j]) == rise) | map((j) => {        let tall = bars[j].volume * 18 / vtop        `M${pct(x(j) - w * 0.36)},100 h${pct(w * 0.72)} v-${pct(tall)} h-${pct(w * 0.72)} Z`    }), " ")    let line_in = (vals: list, to) => {        let js = range(nb) | filter((j) => vals[j] != null)        len(js) == 0 ? "" : "M" + join(js | map((j) => `${pct(x(j))},${pct(to(vals[j]))}`), " L")    }    let close_line = line_in(bars | map((b) => b.close), y)    let band = () => {        let hi = at(bb.upper)        let lo = at(bb.lower)        let js = range(nb) | filter((j) => hi[j] != null)        len(js) == 0 ? "" : "M" + join(js | map((j) => `${pct(x(j))},${pct(y(hi[j]))}`), " L") + " L"            + join(reverse(js) | map((j) => `${pct(x(j))},${pct(y(lo[j]))}`), " L") + " Z"    }    // The RSI and MACD panes.    let ri = (v) => 100 - v    let mvals = known(at(macd.macd) + at(macd.signal) + at(macd.hist))    let reach = max([0.000001] + (mvals | map((v) => abs(v))))    let msc = stock_chart_scale(0 - reach, reach, 4)    let my = (v) => (msc.hi - v) * 100 / (msc.hi - msc.lo)    let hists = (rise: bool) => join(range(nb) | filter((j) => macd.hist[bars[j].i1] != null && (macd.hist[bars[j].i1] >= 0) == rise) | map((j) => {        let v = macd.hist[bars[j].i1]        `M${pct(x(j) - w * 0.32)},${pct(my(v > 0 ? v : 0))} h${pct(w * 0.64)} v${pct(abs(my(v) - my(0)))} h-${pct(w * 0.64)} Z`    }), " ")    let panes = [{ id: "price", h: height }] + (has("rsi") ? [{ id: "rsi", h: 90 }] : []) + (has("macd") ? [{ id: "macd", h: 100 }] : [])    // Where the crosshair is: the bar hovered, else the last in view.    let focus = hover != null && hover < nb ? hover : null    let shown = focus != null ? bars[focus] : (nb > 0 ? bars[nb - 1] : null)    let before = (b) => b.i0 > 0 ? candles[b.i0 - 1].close : b.open    let change = (b) => before(b) != 0 ? (b.close - before(b)) * 100 / before(b) : 0    let when = (b) => stock_chart_when(b.time)    let last = n > 0 ? candles[-1] : null    let last_up = n < 2 || last.close >= candles[n - 2].close    let first_view = nb > 0 ? bars[0].open : 0    let view_change = nb > 0 && first_view != 0 ? (bars[nb - 1].close - first_view) * 100 / first_view : 0    let cross_price = at_y != null && focus != null && at_y * 100 >= 3 && at_y * 100 <= 79 ? sc.hi - (at_y * 100 - 3) * (sc.hi - sc.lo) / 76 : null    let axis_width = max([6] + (sc.ticks | map((t) => len(tick(t)))) + [last != null ? len(price(last.close)) + 1 : 0])    let said = (b) => `${when(b)}: open ${price(b.open)}, high ${price(b.high)}, low ${price(b.low)}, close ${price(b.close)}, ${stock_chart_signed(change(b))}, volume ${stock_chart_short(b.volume)}`    // Moving the view: to a first candle, by a factor of zoom around a point (0 to 1 across).    let leave_range = () => {        if picked != "" {            picked = ""            on_range && on_range("")        }    }    let move_to = (first: int) => {        let next = first < 0 ? 0 : (first > n - c ? n - c : first)        if next != s {            start = next            leave_range()        }    }    let zoom = (factor: num, fx: num) => {        let anchor = s + fx * c        let want = round(c * factor)        let next = want < fewest ? fewest : (want > n ? n : want)        if next != c {            let first = round(anchor - fx * next)            count = next            start = first < 0 ? 0 : (first > n - next ? n - next : first)            leave_range()        }    }    let locate = (e) => {        let r = e.target.closest(".ui-stk-panes").getBoundingClientRect()        { fx: r.width > 0 ? (e.clientX - r.left) / r.width : 0.5, py: (e.clientY - r.top) / height, width: r.width }    }    <div class={ui_cls(["ui-stk", class])} style={`--ui-stk-h: ${height}px`}>        <div.ui-stk-head>            <h3.ui-stk-name>{label}</h3>            {last != null && <span.ui-stk-last>{price(last.close)}</span>}            {nb > 0 && <span class={`ui-stk-move ${side(view_change >= 0)}`}>{stock_chart_signed(view_change)}</span>}            {nb > 0 && <span.ui-stk-span>{picked != "" && picked != "All" ? `past ${picked}` : "in view"} · {when(bars[0])} – {when(bars[nb - 1])}</span>}        </div>        <div.ui-stk-tools>            <span.ui-stk-group role="group" aria-label="Range">                {for r in ranges => <button type="button" aria-pressed={picked == r.name ? "true" : "false"} disabled={window_of(r.name) == null}                        @click={() => {                            let wnd = window_of(r.name)                            if wnd != null {                                start = wnd.start                                count = wnd.count                                picked = r.name                                hover = null                                on_range && on_range(r.name)                            }                        }}>{r.name}</button>}            </span>            <span.ui-stk-group role="group" aria-label="Chart type">                {for k in kinds => <button type="button" aria-pressed={shape == k.id ? "true" : "false"} @click={() => { shape = k.id }}>{k.name}</button>}            </span>            <span.ui-stk-group.ui-stk-ind role="group" aria-label="Indicators">                {for t in studies => <button type="button" aria-pressed={has(t.id) ? "true" : "false"} style={`--c: var(--ui-stk-${t.hue})`}                        @click={() => { on = has(t.id) ? (on | filter((x) => x != t.id)) : on + [t.id] }}>{t.name}</button>}            </span>        </div>        <div.ui-stk-chart>            <div class={ui_cls(["ui-stk-panes", drag != null && "ui-stk-dragging"])} tabindex="0" role="application"                 aria-label={`${label}, ${nb > 0 ? `${when(bars[0])} to ${when(bars[nb - 1])}` : "no data"}`}                 aria-roledescription="chart"                 @pointermove={(e) => {                     // Read before the call below, which can pause the handler: a pointerup that                     // comes meanwhile must not cut this move short.                     let held = drag                     let p = locate(e)                     if held != null && p.width > 0 { move_to(held.from + round((held.x - e.clientX) * c / p.width)) }                     let j = floor(p.fx * nb)                     hover = nb == 0 ? null : (j < 0 ? 0 : (j >= nb ? nb - 1 : j))                     at_y = p.py >= 0 && p.py <= 1 ? p.py : null                     keyed = false                 }}                 @pointerleave={() => {                     if drag == null {                         hover = null                         at_y = null                     }                 }}                 @pointerdown={(e) => {                     if e.button == 0 {                         // Set first: the handler can pause here, and a move that comes meanwhile                         // must find the drag begun.                         drag = { x: e.clientX, from: s }                         e.target.closest(".ui-stk-panes").setPointerCapture(e.pointerId)                     }                 }}                 @pointerup={() => { drag = null }}                 @pointercancel={() => { drag = null }}                 @wheel={(e) => {                     e.preventDefault()                     let p = locate(e)                     zoom(e.deltaY > 0 ? 1.25 : 0.8, p.fx < 0 ? 0 : (p.fx > 1 ? 1 : p.fx))                 }}                 @keydown={(e) => {                     let k = e.key                     if k == "ArrowRight" || k == "ArrowLeft" {                         e.preventDefault()                         let step = k == "ArrowRight" ? 1 : -1                         let next = focus == null ? nb - 1 : focus + step                         if next < 0 {                             move_to(s - per)                             next = 0                         } else if next >= nb {                             move_to(s + per)                             next = nb - 1                         }                         hover = next                         at_y = null                         keyed = true                     } else if k == "+" || k == "=" || k == "-" || k == "_" || k == "−" {                         e.preventDefault()                         zoom(k == "+" || k == "=" ? 0.8 : 1.25, focus != null && nb > 0 ? (focus + 0.5) / nb : 0.5)                         keyed = true                     } else if k == "Home" || k == "End" {                         e.preventDefault()                         move_to(k == "Home" ? 0 : n - c)                         hover = k == "Home" ? 0 : nb - 1                         keyed = true                     } else if k == "Escape" {                         hover = null                         keyed = false                     }                 }}>                {for p in panes => <div.ui-stk-pane data-pane={p.id} style={`--ph: ${p.h}px`}>                    {match p.id {                        "price" => [                            for t in sc.ticks => <span.ui-stk-grid style={`top: ${pct(y(t))}%`}></span>,                            <svg.ui-stk-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">                                <path.ui-stk-vol.ui-stk-up d={vols(true)}/>                                <path.ui-stk-vol.ui-stk-down d={vols(false)}/>                                {has("bb") && [<path.ui-stk-band d={band()}/>,                                    <path.ui-stk-bb d={line_in(at(bb.upper), y)} vector-effect="non-scaling-stroke"/>,                                    <path.ui-stk-bb d={line_in(at(bb.lower), y)} vector-effect="non-scaling-stroke"/>,                                    <path.ui-stk-bbmid d={line_in(at(bb.mid), y)} vector-effect="non-scaling-stroke"/>]}                                {shape == "candles" ? [                                    <path.ui-stk-wick.ui-stk-up d={wicks(true)} vector-effect="non-scaling-stroke"/>,                                    <path.ui-stk-wick.ui-stk-down d={wicks(false)} vector-effect="non-scaling-stroke"/>,                                    <path.ui-stk-body.ui-stk-up d={bodies(true)}/>,                                    <path.ui-stk-body.ui-stk-down d={bodies(false)}/>,                                ] : [                                    shape == "area" && nb > 0 && <path.ui-stk-fill d={`${close_line} L${pct(x(nb - 1))},100 L${pct(x(0))},100 Z`}/>,                                    <path.ui-stk-close d={close_line} vector-effect="non-scaling-stroke"/>,                                ]}                                {has("sma20") && <path.ui-stk-sma d={line_in(at(sma20), y)} vector-effect="non-scaling-stroke"/>}                                {has("ema50") && <path.ui-stk-ema d={line_in(at(ema50), y)} vector-effect="non-scaling-stroke"/>}                            </svg>,                            last != null && last.close >= sc.lo && last.close <= sc.hi &&                                <span class={`ui-stk-lastline ${side(last_up)}`} style={`top: ${pct(y(last.close))}%`}></span>,                            cross_price != null && <span.ui-stk-hline style={`top: ${pct(at_y * 100)}%`}></span>,                            shown != null && <div.ui-stk-read aria-hidden="true">                                <span.ui-stk-when>{when(shown)}{shown.i1 > shown.i0 ? ` (${shown.i1 - shown.i0 + 1} bars)` : ""}</span>                                <span>O <b>{price(shown.open)}</b></span>                                <span>H <b>{price(shown.high)}</b></span>                                <span>L <b>{price(shown.low)}</b></span>                                <span class={side(change(shown) >= 0)}>C <b>{price(shown.close)}</b> <b>{stock_chart_signed(change(shown))}</b></span>                                <span>Vol <b>{stock_chart_short(shown.volume)}</b></span>                                {has("sma20") && <span>SMA 20 <b>{price(sma20[shown.i1])}</b></span>}                                {has("ema50") && <span>EMA 50 <b>{price(ema50[shown.i1])}</b></span>}                                {has("bb") && <span>BB <b>{price(bb.lower[shown.i1])}</b> – <b>{price(bb.upper[shown.i1])}</b></span>}                            </div>,                        ]                        "rsi" => [                            <span.ui-stk-pname>RSI 14 {shown != null && rsi14[shown.i1] != null ? stock_chart_num(rsi14[shown.i1], 1) : ""}</span>,                            <span.ui-stk-band-rsi style="top: 30%; height: 40%"></span>,                            <span.ui-stk-level style="top: 30%"></span>,                            <span.ui-stk-level style="top: 70%"></span>,                            <svg.ui-stk-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">                                <path.ui-stk-rsi d={line_in(at(rsi14), ri)} vector-effect="non-scaling-stroke"/>                            </svg>,                        ]                        _ => [                            <span.ui-stk-pname>MACD 12 26 9 {shown != null && macd.macd[shown.i1] != null ? stock_chart_num(macd.macd[shown.i1], decimals + 2) : ""}</span>,                            <span.ui-stk-grid.ui-stk-zero style={`top: ${pct(my(0))}%`}></span>,                            <svg.ui-stk-svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">                                <path.ui-stk-hist.ui-stk-up d={hists(true)}/>                                <path.ui-stk-hist.ui-stk-down d={hists(false)}/>                                <path.ui-stk-macd d={line_in(at(macd.macd), my)} vector-effect="non-scaling-stroke"/>                                <path.ui-stk-signal d={line_in(at(macd.signal), my)} vector-effect="non-scaling-stroke"/>                            </svg>,                        ]                    }}                </div>}                {focus != null && <span.ui-stk-vline style={`left: ${pct(x(focus))}%`}></span>}                <span.ui-sr aria-live="polite">{keyed && shown != null ? said(shown) : ""}</span>            </div>            <div.ui-stk-axis style={`width: ${axis_width}ch`} aria-hidden="true">                {for p in panes => <div.ui-stk-ax style={`--ph: ${p.h}px`}>                    {match p.id {                        "price" => [                            for t in sc.ticks => !(last != null && abs(y(t) - y(last.close)) * height < 1400) && <span style={`top: ${pct(y(t))}%`}>{tick(t)}</span>,                            last != null && last.close >= sc.lo && last.close <= sc.hi &&                                <span class={`ui-stk-tag ${side(last_up)}`} style={`top: ${pct(y(last.close))}%`}>{price(last.close)}</span>,                            cross_price != null && <span.ui-stk-cross style={`top: ${pct(at_y * 100)}%`}>{price(cross_price)}</span>,                        ]                        "rsi" => for t in [70, 30] => <span style={`top: ${pct(ri(t))}%`}>{t}</span>                        _ => for t in msc.ticks => <span style={`top: ${pct(my(t))}%`}>{t < 0 ? "-" : ""}{stock_chart_num(abs(t))}</span>                    }}                </div>}            </div>            <div.ui-stk-x aria-hidden="true">                {for t in stock_chart_ticks(bars | map((b) => b.time), 7) => !(focus != null && abs(x(t.i) - x(focus)) < 9) && <span                    class={ui_cls([x(t.i) < 4 && "ui-stk-first", x(t.i) > 96 && "ui-stk-end"])} style={`left: ${pct(x(t.i))}%`}>{t.text}</span>}                {focus != null && <span class={ui_cls(["ui-stk-cross", x(focus) < 8 && "ui-stk-first", x(focus) > 92 && "ui-stk-end"])}                                        style={`left: ${pct(x(focus))}%`}>{when(bars[focus])}</span>}            </div>            <span></span>        </div>        <p.ui-stk-help>Drag to pan and use the wheel to zoom; focused, <kbd>←</kbd> <kbd>→</kbd> move the crosshair, <kbd>+</kbd> <kbd>−</kbd> zoom, <kbd>Esc</kbd> lets go.</p>        <table.ui-sr>            <caption>{label}, the bars in view</caption>            <thead><tr><th scope="col">Time</th><th scope="col">Open</th><th scope="col">High</th><th scope="col">Low</th>                <th scope="col">Close</th><th scope="col">Volume</th></tr></thead>            <tbody>                {for b in bars => <tr>                    <th scope="row">{when(b)}</th><td>{price(b.open)}</td><td>{price(b.high)}</td><td>{price(b.low)}</td>                    <td>{price(b.close)}</td><td>{stock_chart_num(b.volume, 0)}</td>                </tr>}            </tbody>        </table>    </div>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

On StockChart: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.