react-native-vroom-chart 0.11.1 → 0.13.0

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.
@@ -63,6 +63,7 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
63
63
  out.push_back(jsi::PropNameID::forAscii(rt, "setBollinger"));
64
64
  out.push_back(jsi::PropNameID::forAscii(rt, "setVolume"));
65
65
  out.push_back(jsi::PropNameID::forAscii(rt, "setVolumeCollapse"));
66
+ out.push_back(jsi::PropNameID::forAscii(rt, "setAxisCollapse"));
66
67
  out.push_back(jsi::PropNameID::forAscii(rt, "coordAt"));
67
68
  out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLines"));
68
69
  out.push_back(jsi::PropNameID::forAscii(rt, "hitTestPriceLine"));
@@ -919,6 +920,27 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
919
920
  });
920
921
  }
921
922
 
923
+ if (name == "setAxisCollapse") {
924
+ // setAxisCollapse(yT, xT) — hides the price / time axis strips. Both take
925
+ // EASED progress (0 shown, 1 hidden); there is no per-element stagger for
926
+ // the core to distribute, so the host owns the curve.
927
+ return jsi::Function::createFromHostFunction(
928
+ rt,
929
+ jsi::PropNameID::forAscii(rt, "setAxisCollapse"),
930
+ 2,
931
+ [this](jsi::Runtime& /*rt2*/,
932
+ const jsi::Value& /*thisVal*/,
933
+ const jsi::Value* args,
934
+ size_t count) -> jsi::Value {
935
+ if (count < 2) return jsi::Value::undefined();
936
+ vroom_chart_set_axis_collapse(
937
+ chart_,
938
+ static_cast<float>(args[0].asNumber()),
939
+ static_cast<float>(args[1].asNumber()));
940
+ return jsi::Value::undefined();
941
+ });
942
+ }
943
+
922
944
  if (name == "coordAt") {
923
945
  // coordAt(x, y) -> { timeMs, price } | null. The continuous data coordinate
924
946
  // at a pixel — not snapped to a candle slot. Null when there are no candles
@@ -282,8 +282,12 @@ typedef enum {
282
282
  VROOM_COLOR_GRID,
283
283
  VROOM_COLOR_AXIS_TEXT,
284
284
  VROOM_COLOR_CROSSHAIR,
285
- VROOM_COLOR_TOOLTIP_BG,
286
- VROOM_COLOR_TOOLTIP_TEXT,
285
+ // Retired (was VROOM_COLOR_TOOLTIP_BG, never rendered). Held open because
286
+ // the TypeScript theme maps hardcode the indices below it.
287
+ VROOM_COLOR_RESERVED_7_,
288
+ // Text drawn on a filled badge: the current-price indicator, the crosshair's
289
+ // axis badges, and price-line pills. Defaults to white.
290
+ VROOM_COLOR_BADGE_TEXT,
287
291
  VROOM_COLOR_CROSSHAIR_TARGET, // the hollow ring/dot at the intersection
288
292
  VROOM_COLOR_BORDER_BULL, // bull body 1px outline; 0 alpha => inherit BULL fill
289
293
  VROOM_COLOR_BORDER_BEAR, // bear body 1px outline; 0 alpha => inherit BEAR fill
@@ -564,6 +568,19 @@ void vroom_chart_set_volume(VroomChart* chart, const VroomVolume* cfg);
564
568
  // `enabled`, so the host only needs it while animating.
565
569
  void vroom_chart_set_volume_collapse(VroomChart* chart, float t, int32_t easing);
566
570
 
571
+ // Collapses the axis strips: 0 leaves a strip at full size, 1 hides it. Driven
572
+ // per-frame by the host animation loop, which owns the eased clock — unlike the
573
+ // volume collapse there is no per-element stagger, so these take *eased*
574
+ // progress directly.
575
+ //
576
+ // Hiding is a collapse rather than a zeroed dimension because the y-axis width
577
+ // is measured from the widest price label and recomputed on most data changes,
578
+ // which would overwrite a zero. The price pane grows into whatever the strips
579
+ // give up, and each strip's contents (labels, price badge, crosshair and
580
+ // price-line badges) fade out over the first half of the collapse so text is
581
+ // never squeezed into a strip too narrow to hold it.
582
+ void vroom_chart_set_axis_collapse(VroomChart* chart, float y_t, float x_t);
583
+
567
584
  // ---- Drawings (line annotations) ------------------------------------------
568
585
 
569
586
  // Replaces the full set of committed line drawings (data-anchored, so they track
@@ -64,13 +64,16 @@ vroom::Layout VroomChart::layout() const {
64
64
  return vroom::Layout{
65
65
  width_px,
66
66
  height_px,
67
- axis_w,
68
- theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
67
+ vroom::axis_extent(axis_w, y_axis_collapse_t),
68
+ vroom::axis_extent(theme.floats[VROOM_FLOAT_X_AXIS_HEIGHT_PX],
69
+ x_axis_collapse_t),
69
70
  theme.floats[VROOM_FLOAT_RIGHT_PADDING_PX],
70
71
  theme.floats[VROOM_FLOAT_CANDLE_WIDTH_RATIO],
71
72
  0.05f,
72
73
  0.05f,
73
74
  indicator_h,
75
+ vroom::axis_opacity(y_axis_collapse_t),
76
+ vroom::axis_opacity(x_axis_collapse_t),
74
77
  };
75
78
  }
76
79
 
@@ -191,6 +191,13 @@ struct VroomChart {
191
191
  float volume_collapse_t = 0.f;
192
192
  int32_t volume_collapse_easing = VROOM_EASING_IN_OUT;
193
193
 
194
+ // Collapse of the axis strips, driven per-frame by the host animation loop:
195
+ // 0 = full strip, 1 = hidden. Unlike the volume collapse these change the
196
+ // layout, so the price pane grows into whatever the strips give up. The host
197
+ // pre-eases them (there is no per-element stagger to distribute here).
198
+ float y_axis_collapse_t = 0.f;
199
+ float x_axis_collapse_t = 0.f;
200
+
194
201
  // --- drawings (annotations) --------------------------------------------
195
202
  // Committed drawings, anchored in data space so they track the candles on
196
203
  // pan/zoom. Drawn on the price pane above candles/overlays.
@@ -1401,6 +1401,15 @@ extern "C" void vroom_chart_set_volume_collapse(VroomChart* chart, float t,
1401
1401
  chart->mark_dirty();
1402
1402
  }
1403
1403
 
1404
+ extern "C" void vroom_chart_set_axis_collapse(VroomChart* chart, float y_t,
1405
+ float x_t) {
1406
+ if (!chart) return;
1407
+ chart->y_axis_collapse_t = std::clamp(y_t, 0.f, 1.f);
1408
+ chart->x_axis_collapse_t = std::clamp(x_t, 0.f, 1.f);
1409
+ // Changes the layout, so every cached pane geometry has to be re-derived.
1410
+ chart->mark_dirty();
1411
+ }
1412
+
1404
1413
  // ---- Direct draw (used by hosts that don't need the SkPicture cache) ------
1405
1414
 
1406
1415
  extern "C" void vroom_chart_draw(VroomChart* chart, SkCanvas* canvas) {
@@ -42,7 +42,9 @@ void draw_badge(SkCanvas* canvas,
42
42
  float cx,
43
43
  float cy,
44
44
  SkColor fill,
45
- SkColor text_color) {
45
+ SkColor text_color,
46
+ float opacity) {
47
+ if (opacity <= 0.f) return;
46
48
  const size_t len = std::strlen(text);
47
49
  SkRect tb;
48
50
  const float text_w = font.measureText(text, len, SkTextEncoding::kUTF8, &tb);
@@ -54,11 +56,13 @@ void draw_badge(SkCanvas* canvas,
54
56
  SkPaint box;
55
57
  box.setAntiAlias(true);
56
58
  box.setColor(fill);
59
+ box.setAlphaf(box.getAlphaf() * opacity);
57
60
  canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
58
61
 
59
62
  SkPaint text_paint;
60
63
  text_paint.setAntiAlias(true);
61
64
  text_paint.setColor(text_color);
65
+ text_paint.setAlphaf(text_paint.getAlphaf() * opacity);
62
66
  canvas->drawString(text, cx - text_w * 0.5f,
63
67
  cy - (tb.fTop + tb.fBottom) * 0.5f, font, text_paint);
64
68
  }
@@ -108,8 +112,8 @@ void draw(SkCanvas* canvas,
108
112
  ring.setStrokeWidth(2.f); // thicker border so the dot reads clearly
109
113
  canvas->drawCircle(cx, cy, kRingRadius, ring);
110
114
 
111
- // Axis badges (neutral grey, white text) sit on top of the axis labels and
112
- // the current-price indicator since the crosshair is the last draw step.
115
+ // Axis badges sit on top of the axis labels and the current-price indicator
116
+ // since the crosshair is the last draw step.
113
117
  // They need the axis typeface; if it isn't loaded the lines alone suffice.
114
118
  auto tf = vroom::axis_typeface();
115
119
  if (!tf) return;
@@ -119,6 +123,7 @@ void draw(SkCanvas* canvas,
119
123
  font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
120
124
 
121
125
  const SkColor badge_fill = chart.theme.colors[VROOM_COLOR_CROSSHAIR_TARGET];
126
+ const SkColor badge_text = chart.theme.colors[VROOM_COLOR_BADGE_TEXT];
122
127
 
123
128
  // Date/time badge over the x-axis strip, centered on the vertical line and
124
129
  // vertically aligned with the time labels (centered in the strip).
@@ -140,7 +145,7 @@ void draw(SkCanvas* canvas,
140
145
  const float badge_cx =
141
146
  std::clamp(cx, half_w, std::max(half_w, candle_right - half_w));
142
147
  draw_badge(canvas, font, buf, badge_cx, strip_center_y, badge_fill,
143
- SK_ColorWHITE);
148
+ badge_text, lay.x_axis_opacity);
144
149
  }
145
150
 
146
151
  // Price badge over the y-axis strip, centered on the horizontal line and
@@ -155,7 +160,7 @@ void draw(SkCanvas* canvas,
155
160
  vroom::format_price(buf, sizeof(buf), price, fmt);
156
161
  const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
157
162
  draw_badge(canvas, font, buf, axis_center_x, cy, badge_fill,
158
- SK_ColorWHITE);
163
+ badge_text, lay.y_axis_opacity);
159
164
  }
160
165
  }
161
166
 
@@ -147,6 +147,9 @@ void draw_y_labels(SkCanvas* canvas,
147
147
  const PriceBounds& bounds) {
148
148
  auto tf = vroom::axis_typeface();
149
149
  if (!tf) return;
150
+ // The fade state still advances in update_y_fades, so a hidden axis picks up
151
+ // wherever the ticks moved to once it is revealed again.
152
+ if (lay.y_axis_opacity <= 0.f) return;
150
153
 
151
154
  const float candle_area_h = vroom::price_pane_bottom(lay);
152
155
 
@@ -186,7 +189,7 @@ void draw_y_labels(SkCanvas* canvas,
186
189
  if (baseline_y - cap_h < 0.f) continue;
187
190
  if (baseline_y > candle_area_h) continue;
188
191
 
189
- text_paint.setAlphaf(f.opacity);
192
+ text_paint.setAlphaf(f.opacity * lay.y_axis_opacity);
190
193
  canvas->drawString(buf, text_x, baseline_y, font, text_paint);
191
194
  }
192
195
  }
@@ -279,6 +282,8 @@ void draw_x_labels(SkCanvas* canvas,
279
282
  auto tf = vroom::axis_typeface();
280
283
  if (!tf || chart.candles.empty()) return;
281
284
  if (end_ms <= start_ms) return;
285
+ // See draw_y_labels: update_x_fades keeps running while the strip is hidden.
286
+ if (lay.x_axis_opacity <= 0.f) return;
282
287
 
283
288
  // X-axis labels live in the bottom strip, which stays anchored regardless
284
289
  // of any indicator pane above it.
@@ -345,7 +350,7 @@ void draw_x_labels(SkCanvas* canvas,
345
350
  if (text_x < 0.f) continue;
346
351
  if (text_x + text_w > candle_area_w) continue;
347
352
 
348
- text_paint.setAlphaf(f.opacity);
353
+ text_paint.setAlphaf(f.opacity * lay.x_axis_opacity);
349
354
  canvas->drawString(buf, text_x, baseline_y, font, text_paint);
350
355
  }
351
356
  }
@@ -211,14 +211,15 @@ void draw(SkCanvas* canvas,
211
211
  }
212
212
  canvas->restore();
213
213
 
214
- // "0" label in the y-axis strip at the zero line.
215
- if (tf) {
214
+ // "0" label in the y-axis strip at the zero line — hidden with that strip.
215
+ if (tf && lay.y_axis_opacity > 0.f) {
216
216
  SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
217
217
  font.setSubpixel(true);
218
218
  font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
219
219
  SkPaint text_paint;
220
220
  text_paint.setAntiAlias(true);
221
221
  text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
222
+ text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
222
223
  SkRect tb;
223
224
  const float tw =
224
225
  font.measureText("0", 1, SkTextEncoding::kUTF8, &tb);
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <algorithm>
4
4
  #include <cmath>
5
+ #include <cstddef>
5
6
  #include <cstdio>
6
7
 
7
8
  namespace vroom {
@@ -15,6 +16,38 @@ constexpr size_t kPlainCap = 340;
15
16
  constexpr char kGroupSeparator = ',';
16
17
  constexpr int kGroupSize = 3;
17
18
 
19
+ constexpr double kMillion = 1e6;
20
+ constexpr double kBillion = 1e9;
21
+ constexpr double kTrillion = 1e12;
22
+
23
+ double compact_divisor(CompactScale scale) {
24
+ switch (scale) {
25
+ case CompactScale::Million: return kMillion;
26
+ case CompactScale::Billion: return kBillion;
27
+ case CompactScale::Trillion: return kTrillion;
28
+ case CompactScale::None: return 1.0;
29
+ }
30
+ return 1.0;
31
+ }
32
+
33
+ char compact_suffix(CompactScale scale) {
34
+ switch (scale) {
35
+ case CompactScale::Million: return 'M';
36
+ case CompactScale::Billion: return 'B';
37
+ case CompactScale::Trillion: return 'T';
38
+ case CompactScale::None: return '\0';
39
+ }
40
+ return '\0';
41
+ }
42
+
43
+ CompactScale compact_scale_for(double reference) {
44
+ const double r = std::fabs(reference);
45
+ if (!std::isfinite(r) || r < kMillion) return CompactScale::None;
46
+ if (r >= kTrillion) return CompactScale::Trillion;
47
+ if (r >= kBillion) return CompactScale::Billion;
48
+ return CompactScale::Million;
49
+ }
50
+
18
51
  } // namespace
19
52
 
20
53
  int significant_decimals(double reference) {
@@ -28,7 +61,8 @@ int significant_decimals(double reference) {
28
61
  }
29
62
 
30
63
  PriceFormat price_format_for(double reference) {
31
- return PriceFormat{significant_decimals(reference), true};
64
+ return PriceFormat{significant_decimals(reference), true,
65
+ compact_scale_for(reference)};
32
66
  }
33
67
 
34
68
  int price_decimals(double interval) {
@@ -40,8 +74,13 @@ int price_decimals(double interval) {
40
74
  }
41
75
 
42
76
  PriceFormat with_tick_guard(const PriceFormat& fmt, double interval) {
43
- return PriceFormat{std::max(fmt.decimals, price_decimals(interval)),
44
- fmt.group};
77
+ const double divisor = compact_divisor(fmt.compact);
78
+ const double display_interval =
79
+ (divisor != 1.0 && interval > 0.0 && std::isfinite(interval))
80
+ ? interval / divisor
81
+ : interval;
82
+ return PriceFormat{std::max(fmt.decimals, price_decimals(display_interval)),
83
+ fmt.group, fmt.compact};
45
84
  }
46
85
 
47
86
  void format_price(char* buf, size_t buf_size, double price,
@@ -49,11 +88,23 @@ void format_price(char* buf, size_t buf_size, double price,
49
88
  if (!buf || buf_size == 0) return;
50
89
  const int decimals = std::clamp(fmt.decimals, 0, kPriceMaxDecimals);
51
90
 
91
+ const double divisor = compact_divisor(fmt.compact);
92
+ const double display = (divisor != 1.0) ? price / divisor : price;
93
+
52
94
  char plain[kPlainCap];
53
- if (std::snprintf(plain, sizeof(plain), "%.*f", decimals, price) < 0) {
95
+ if (std::snprintf(plain, sizeof(plain), "%.*f", decimals, display) < 0) {
54
96
  buf[0] = '\0';
55
97
  return;
56
98
  }
99
+
100
+ const char suffix = compact_suffix(fmt.compact);
101
+ if (suffix != '\0') {
102
+ // Compact form is already short; grouping would only fire if a tick
103
+ // wandered across a suffix boundary (1000.00M). Skip it.
104
+ std::snprintf(buf, buf_size, "%s%c", plain, suffix);
105
+ return;
106
+ }
107
+
57
108
  if (!fmt.group) {
58
109
  std::snprintf(buf, buf_size, "%s", plain);
59
110
  return;
@@ -13,6 +13,16 @@
13
13
  // 0.023397 -> 6 decimals 0.023397
14
14
  // 80285.20 -> 2 decimals 80,285.20 (floor)
15
15
  // 2513.92 -> 2 decimals 2,513.92 (floor)
16
+ // 1.59e6 -> compact 1.59M
17
+ // 3.219e7 -> compact 32.19M
18
+ // 3.454e10 -> compact 34.54B
19
+ // 1.2e12 -> compact 1.20T
20
+ //
21
+ // Past a million the integer digits eat the axis, so the format switches to a
22
+ // suffix (M / B / T) with two decimal places. The suffix is keyed off the
23
+ // asset's own price, not each tick, so every label on a market-cap chart
24
+ // shares a column and a zoomed-in 32.1M–32.2M range does not mix "32.15M"
25
+ // with "32,150,000.00".
16
26
  //
17
27
  // The reference is the asset's own price, not the visible range, so the
18
28
  // precision holds steady while the user pans and zooms. A range-derived count
@@ -32,18 +42,31 @@ namespace vroom {
32
42
  inline constexpr int kPriceSigDigits = 5;
33
43
 
34
44
  // Prices at or above ~1 unit read as currency, where fewer than two decimals
35
- // looks broken (a "$84,000" axis next to a "$80,285.20" last price).
45
+ // looks broken (a "$84,000" axis next to a "$80,285.20" last price). Compact
46
+ // suffixes use the same floor: "32.19M", not "32.2M".
36
47
  inline constexpr int kPriceMinDecimals = 2;
37
48
 
38
49
  // Past this the digits are noise, and %.*f starts printing the binary
39
50
  // representation's tail rather than anything the feed meant.
40
51
  inline constexpr int kPriceMaxDecimals = 12;
41
52
 
53
+ // Compact suffix for assets priced in the millions and up. Keyed off the
54
+ // reference, then applied to every label so the column stays uniform. Nothing
55
+ // below a million is abbreviated — no K.
56
+ enum class CompactScale {
57
+ None,
58
+ Million, // |ref| >= 1e6
59
+ Billion, // |ref| >= 1e9
60
+ Trillion, // |ref| >= 1e12
61
+ };
62
+
42
63
  // How to render a price. `group` inserts thousands separators, which only ever
43
- // affects the integer part.
64
+ // affects the integer part of a non-compact format. `compact` scales the value
65
+ // and appends M / B / T; grouping is skipped in that path.
44
66
  struct PriceFormat {
45
- int decimals = kPriceMinDecimals;
46
- bool group = true;
67
+ int decimals = kPriceMinDecimals;
68
+ bool group = true;
69
+ CompactScale compact = CompactScale::None;
47
70
  };
48
71
 
49
72
  // Decimals giving kPriceSigDigits significant digits for an asset priced around
@@ -51,7 +74,8 @@ struct PriceFormat {
51
74
  // non-finite reference falls back to the floor.
52
75
  int significant_decimals(double reference);
53
76
 
54
- // The format for an asset priced around `reference`.
77
+ // The format for an asset priced around `reference`, including the compact
78
+ // suffix when |reference| is at least a million.
55
79
  PriceFormat price_format_for(double reference);
56
80
 
57
81
  // Decimal places needed so adjacent ticks `interval` apart don't collapse to
@@ -60,6 +84,9 @@ int price_decimals(double interval);
60
84
 
61
85
  // `fmt` raised, if need be, so ticks `interval` apart stay distinct. The asset's
62
86
  // precision is the floor; only a zoom deeper than that precision adds to it.
87
+ // `interval` is in the data's units; when `fmt` is compact the guard converts
88
+ // it into display units (interval / 1e6 for millions, etc.) so a 1,000-wide
89
+ // step on a 32M chart still gets enough decimals to print as 32.192M vs 32.193M.
63
90
  PriceFormat with_tick_guard(const PriceFormat& fmt, double interval);
64
91
 
65
92
  // Writes `price` into `buf` per `fmt`, truncating rather than overrunning.
@@ -59,9 +59,11 @@ void draw(SkCanvas* canvas,
59
59
  canvas->drawLine(0.f, y, candle_right, y, line);
60
60
 
61
61
  // Price box in the y-axis strip. Needs the axis typeface; if it isn't
62
- // loaded yet, the line alone still conveys the level.
62
+ // loaded yet, the line alone still conveys the level. Same when the y-axis
63
+ // is hidden — the level stays readable, it just loses its badge.
63
64
  auto tf = vroom::axis_typeface();
64
65
  if (!tf) return;
66
+ if (lay.y_axis_opacity <= 0.f) return;
65
67
 
66
68
  SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
67
69
  font.setSubpixel(true);
@@ -95,14 +97,16 @@ void draw(SkCanvas* canvas,
95
97
  SkPaint box;
96
98
  box.setAntiAlias(true);
97
99
  box.setColor(color);
100
+ box.setAlphaf(box.getAlphaf() * lay.y_axis_opacity);
98
101
  canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
99
102
 
100
- // White price text, horizontally centered on the same axis center, and
101
- // vertically centered: shift the baseline so the glyph bounds' midpoint
102
- // lands on `y` (= the box center and the dotted line).
103
+ // Price text horizontally centered on the same axis center, and vertically
104
+ // centered: shift the baseline so the glyph bounds' midpoint lands on `y`
105
+ // (= the box center and the dotted line).
103
106
  SkPaint text_paint;
104
107
  text_paint.setAntiAlias(true);
105
- text_paint.setColor(SK_ColorWHITE);
108
+ text_paint.setColor(chart.theme.colors[VROOM_COLOR_BADGE_TEXT]);
109
+ text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
106
110
  const float text_x = axis_center_x - text_w * 0.5f;
107
111
  const float baseline_y = y - (tb.fTop + tb.fBottom) * 0.5f;
108
112
  canvas->drawString(buf, text_x, baseline_y, font, text_paint);
@@ -215,8 +215,10 @@ void draw_axis_badge(SkCanvas* canvas,
215
215
  double price,
216
216
  float y,
217
217
  SkColor fill,
218
+ SkColor text_color,
218
219
  const PriceFormat& fmt) {
219
220
  if (lay.y_axis_width_px <= 0.f) return;
221
+ if (lay.y_axis_opacity <= 0.f) return;
220
222
 
221
223
  char buf[48];
222
224
  vroom::format_price(buf, sizeof(buf), price, fmt);
@@ -233,11 +235,13 @@ void draw_axis_badge(SkCanvas* canvas,
233
235
  SkPaint box;
234
236
  box.setAntiAlias(true);
235
237
  box.setColor(fill);
238
+ box.setAlphaf(box.getAlphaf() * lay.y_axis_opacity);
236
239
  canvas->drawRRect(SkRRect::MakeRectXY(rect, kCorner, kCorner), box);
237
240
 
238
241
  SkPaint text;
239
242
  text.setAntiAlias(true);
240
- text.setColor(SK_ColorWHITE);
243
+ text.setColor(text_color);
244
+ text.setAlphaf(text.getAlphaf() * lay.y_axis_opacity);
241
245
  canvas->drawString(buf, cx - text_w * 0.5f,
242
246
  y - (tb.fTop + tb.fBottom) * 0.5f, font, text);
243
247
  }
@@ -254,6 +258,7 @@ void draw(SkCanvas* canvas,
254
258
  if (chart.price_lines.empty()) return;
255
259
 
256
260
  const VroomPriceLineStyle& style = chart.price_line_style;
261
+ const SkColor badge_text = chart.theme.colors[VROOM_COLOR_BADGE_TEXT];
257
262
  SkFont font;
258
263
  const bool has_font = label_font(chart, &font);
259
264
  const vroom::PriceFormat fmt = vroom::with_tick_guard(
@@ -336,12 +341,12 @@ void draw(SkCanvas* canvas,
336
341
  line_color);
337
342
  }
338
343
  if (!group.quantity.empty()) {
339
- // Solid fill + white text, so size reads at a glance against the
344
+ // Solid fill + badge text, so size reads at a glance against the
340
345
  // translucent body.
341
346
  draw_pill(canvas, group.quantity, first == &group.quantity,
342
347
  last == &group.quantity, line_color, SK_ColorTRANSPARENT);
343
348
  draw_centered_text(canvas, font, pl.quantity, group.quantity, y,
344
- SK_ColorWHITE);
349
+ badge_text);
345
350
  }
346
351
  if (!group.close.empty()) {
347
352
  const SkColor close_color =
@@ -357,7 +362,7 @@ void draw(SkCanvas* canvas,
357
362
 
358
363
  if (has_font && (pl.flags & VROOM_PRICE_LINE_AXIS_LABEL) != 0) {
359
364
  draw_axis_badge(canvas, font, lay, render_price(chart, i), y,
360
- line_color, fmt);
365
+ line_color, badge_text, fmt);
361
366
  }
362
367
  }
363
368
  }
@@ -158,13 +158,15 @@ void draw(SkCanvas* canvas,
158
158
  // 70 / 30 labels in the y-axis strip (drawn after the clip; the strip's
159
159
  // background was already masked by draw_chart before this call). They label
160
160
  // the band rules, so they go with them.
161
- if (tf && cfg.bands_visible) {
161
+ // The band labels live in the y-axis strip, so they go with it when it hides.
162
+ if (tf && cfg.bands_visible && lay.y_axis_opacity > 0.f) {
162
163
  SkFont font(tf, chart.theme.floats[VROOM_FLOAT_AXIS_FONT_SIZE_PX]);
163
164
  font.setSubpixel(true);
164
165
  font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
165
166
  SkPaint text_paint;
166
167
  text_paint.setAntiAlias(true);
167
168
  text_paint.setColor(chart.theme.colors[VROOM_COLOR_AXIS_TEXT]);
169
+ text_paint.setAlphaf(text_paint.getAlphaf() * lay.y_axis_opacity);
168
170
 
169
171
  const float axis_center_x = lay.width_px - lay.y_axis_width_px * 0.5f;
170
172
  auto label = [&](const char* s, float y) {
@@ -13,8 +13,8 @@ constexpr uint32_t kDefaultColors[VROOM_COLOR_COUNT_] = {
13
13
  0xff1a1e24, // GRID — ~20% darker than the crosshair so it recedes
14
14
  0xffc9d1d9, // AXIS_TEXT
15
15
  0xff303741, // CROSSHAIR — grid tone lightened ~44%; reads above GRID
16
- 0xff161b22, // TOOLTIP_BG
17
- 0xffc9d1d9, // TOOLTIP_TEXT
16
+ 0x00000000, // RESERVED_7_ — retired slot, never read
17
+ 0xffffffff, // BADGE_TEXT — text on filled badges/pills
18
18
  0xff3e4855, // CROSSHAIR_TARGET — CROSSHAIR lightened 30% (prior derived ring)
19
19
  0x00000000, // BORDER_BULL — transparent sentinel: inherit BULL fill
20
20
  0x00000000, // BORDER_BEAR — transparent sentinel: inherit BEAR fill
@@ -20,8 +20,40 @@ struct Layout {
20
20
  float top_padding_frac; // fraction of candle area; keeps prices off edges
21
21
  float bottom_padding_frac;
22
22
  float indicator_area_h; // height reserved for below-chart indicator panes
23
+ // Opacity of everything drawn *inside* an axis strip, 1 = shown, 0 = hidden.
24
+ // Separate from the strip's extent because the two ramp at different rates
25
+ // while an axis is being hidden or revealed (see axis_opacity).
26
+ float y_axis_opacity;
27
+ float x_axis_opacity;
23
28
  };
24
29
 
30
+ // --- Axis show/hide ---------------------------------------------------------
31
+ // An axis is hidden by collapsing its strip to nothing rather than by zeroing
32
+ // the underlying dimension: the y-axis width is *measured* from the widest price
33
+ // label (labels::recompute_axis_width) and gets recomputed constantly, so a zero
34
+ // written there would not survive. `t` is the collapse progress the host eases,
35
+ // 0 = fully shown, 1 = fully hidden.
36
+
37
+ inline float clamp_collapse(float t) {
38
+ return t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
39
+ }
40
+
41
+ // The strip's on-screen extent. Everything downstream (candle_area_width,
42
+ // price_pane_bottom) is derived from this, so the plot reclaims the space as
43
+ // the strip shrinks.
44
+ inline float axis_extent(float base, float t) {
45
+ return base * (1.f - clamp_collapse(t));
46
+ }
47
+
48
+ // Opacity of the strip's contents. Deliberately front-loaded: the labels are
49
+ // fully gone by the time the strip is halfway collapsed, so text never sits in
50
+ // a strip too narrow to hold it.
51
+ inline float axis_opacity(float t) {
52
+ constexpr float kFadeBy = 0.5f; // fraction of the collapse spent fading
53
+ const float p = clamp_collapse(t) / kFadeBy;
54
+ return p >= 1.f ? 0.f : 1.f - p;
55
+ }
56
+
25
57
  // Bottom of the price (candle) pane = top of the indicator band. Shrinks when
26
58
  // an indicator pane is present so candles, volume, and price labels reflow.
27
59
  inline float price_pane_bottom(const Layout& l) {
package/lib/index.d.mts CHANGED
@@ -93,6 +93,13 @@ type VroomTheme = {
93
93
  grid?: VroomColor;
94
94
  /** Axis label text (price + time). */
95
95
  axisText?: VroomColor;
96
+ /**
97
+ * Text drawn on a filled badge — the current-price indicator, the crosshair's
98
+ * price and time badges, and price-line pills. Defaults to white, which reads
99
+ * against the saturated fills those badges use on a dark theme; light themes
100
+ * generally want a dark value here.
101
+ */
102
+ badgeText?: VroomColor;
96
103
  /** Crosshair dashed lines. */
97
104
  crosshair?: VroomColor;
98
105
  /** Crosshair target — the hollow ring/dot at the intersection. */
@@ -134,6 +141,26 @@ type VroomTheme = {
134
141
  * continuously — leave it off for charts that should be able to go idle.
135
142
  */
136
143
  lineTipPulse?: boolean;
144
+ /**
145
+ * Show the price (y) axis strip down the right edge. Defaults to `true`.
146
+ *
147
+ * Hiding it collapses the strip and hands the reclaimed width to the plot, so
148
+ * the candles widen to fill it. Its contents — price labels, the current-price
149
+ * badge, the crosshair price badge, price-line axis badges — fade out ahead of
150
+ * the collapse; gridlines and the price indicator's dotted line stay. Animated
151
+ * over `transitionMs` unless the OS asks for reduced motion.
152
+ *
153
+ * The axis drag-to-scale region goes with the strip, so a hidden axis can't be
154
+ * scaled by dragging it.
155
+ */
156
+ showYAxis?: boolean;
157
+ /**
158
+ * Show the time (x) axis strip along the bottom. Defaults to `true`.
159
+ *
160
+ * Behaves like {@link VroomTheme.showYAxis}: the strip collapses, the plot
161
+ * grows into it, and the time labels and crosshair time badge fade first.
162
+ */
163
+ showXAxis?: boolean;
137
164
  };
138
165
  /** A time window over the candle data, as Unix epoch milliseconds. */
139
166
  type VisibleRange = {
package/lib/index.d.ts CHANGED
@@ -93,6 +93,13 @@ type VroomTheme = {
93
93
  grid?: VroomColor;
94
94
  /** Axis label text (price + time). */
95
95
  axisText?: VroomColor;
96
+ /**
97
+ * Text drawn on a filled badge — the current-price indicator, the crosshair's
98
+ * price and time badges, and price-line pills. Defaults to white, which reads
99
+ * against the saturated fills those badges use on a dark theme; light themes
100
+ * generally want a dark value here.
101
+ */
102
+ badgeText?: VroomColor;
96
103
  /** Crosshair dashed lines. */
97
104
  crosshair?: VroomColor;
98
105
  /** Crosshair target — the hollow ring/dot at the intersection. */
@@ -134,6 +141,26 @@ type VroomTheme = {
134
141
  * continuously — leave it off for charts that should be able to go idle.
135
142
  */
136
143
  lineTipPulse?: boolean;
144
+ /**
145
+ * Show the price (y) axis strip down the right edge. Defaults to `true`.
146
+ *
147
+ * Hiding it collapses the strip and hands the reclaimed width to the plot, so
148
+ * the candles widen to fill it. Its contents — price labels, the current-price
149
+ * badge, the crosshair price badge, price-line axis badges — fade out ahead of
150
+ * the collapse; gridlines and the price indicator's dotted line stay. Animated
151
+ * over `transitionMs` unless the OS asks for reduced motion.
152
+ *
153
+ * The axis drag-to-scale region goes with the strip, so a hidden axis can't be
154
+ * scaled by dragging it.
155
+ */
156
+ showYAxis?: boolean;
157
+ /**
158
+ * Show the time (x) axis strip along the bottom. Defaults to `true`.
159
+ *
160
+ * Behaves like {@link VroomTheme.showYAxis}: the strip collapses, the plot
161
+ * grows into it, and the time labels and crosshair time badge fade first.
162
+ */
163
+ showXAxis?: boolean;
137
164
  };
138
165
  /** A time window over the candle data, as Unix epoch milliseconds. */
139
166
  type VisibleRange = {