Skip to content

Price scale

Use independent scales when two series have different units or magnitudes. This scene adds a comparison at ten times the sample price to the first chart. Both histories contain only positive prices.

Select Main or Comparison, then change its mode, invert it or adjust its top and bottom margins. Crop vertically makes that scale manual; Autoscale fits it again. Pan to see relative modes choose a new reference. The controls and setup below run in the widget-only docs package. Create PriceScale::new(&mut chart) once, then call its show(ui, &mut chart) before chart.show(ui) on each frame.

Two independent price scalesOpen full size

Change either scale's mode and margins, crop it, then restore autoscale.

The interactive example needs JavaScript and WebAssembly. You can still use the chart setup alongside it.

price_scale.rs
use eframe::egui;
use egui_charts::{
Chart,
model::{Bar, SeriesScale},
scales::PriceScaleMode,
widget::{SeriesId, SeriesOptions},
};
/// Edit either of two independent scales on the same plot.
pub struct PriceScale {
comparison: SeriesId,
selected: SeriesId,
}
impl PriceScale {
/// Add a positive-valued comparison with its own labelled price scale.
pub fn new(chart: &mut Chart) -> Self {
let bars = chart
.bars(SeriesId::MAIN)
.iter()
.map(|bar| Bar::from_value(bar.time, bar.close * 10.0))
.collect();
let comparison = chart.add_series(SeriesOptions {
name: "COMPARISON".into(),
scale: SeriesScale::Separate(1),
..Default::default()
});
chart.set_data(comparison, bars);
Self {
comparison,
selected: SeriesId::MAIN,
}
}
/// Change only the selected scale; keep user edits between frames.
pub fn show(&mut self, ui: &mut egui::Ui, chart: &mut Chart) {
ui.horizontal_wrapped(|ui| {
ui.label("Edit scale:");
ui.selectable_value(&mut self.selected, SeriesId::MAIN, "Main");
ui.selectable_value(&mut self.selected, self.comparison, "Comparison");
});
let scale = chart.series_price_scale_mut(self.selected).unwrap();
let previous = *scale.options();
let options = scale.options_mut();
ui.horizontal_wrapped(|ui| {
egui::ComboBox::from_id_salt("mode")
.selected_text(options.mode.to_string())
.show_ui(ui, |ui| {
for mode in [
PriceScaleMode::Normal,
PriceScaleMode::Logarithmic,
PriceScaleMode::Percentage,
PriceScaleMode::IndexedTo100,
] {
ui.selectable_value(&mut options.mode, mode, mode.to_string());
}
});
ui.checkbox(&mut options.invert_scale, "Invert");
});
ui.horizontal_wrapped(|ui| {
let margins = &mut options.scale_margins;
ui.label("Margins:");
ui.add(
egui::DragValue::new(&mut margins.top)
.range(0.0..=0.4)
.speed(0.005)
.prefix("Top "),
);
ui.add(
egui::DragValue::new(&mut margins.bottom)
.range(0.0..=0.4)
.speed(0.005)
.prefix("Bottom "),
);
});
// A manual range uses the previous mode's units; refit after an option edit.
if *options != previous {
scale.reset();
}
ui.horizontal_wrapped(|ui| {
if ui.button("Crop vertically").clicked() {
let range = scale.range();
let inset = range.length() * 0.25;
scale.set_range(range.min + inset, range.max - inset);
}
if ui.button("Autoscale").clicked() {
scale.reset();
}
ui.label(if scale.is_manual() {
"Manual range"
} else {
"Automatic range"
});
});
}
}

SeriesOptions::scale uses SeriesScale:

  • Right and Left share the pane’s corresponding axis.
  • Separate(id) creates an additional labelled axis.
  • Overlay(id) creates an independent scale without axis labels.

Series with the same assignment share a range and options. Distinct host-chosen IDs keep their scales independent. Use set_series_scale to reassign a present series, or series_price_scale_mut to edit its current scale. Below the main pane, the series uses its destination pane’s scale.

PriceScaleOptions::mode selects a PriceScaleMode. For a price p and the series’ first visible close b:

ModeScale unitsMeaning
NormalpEqual differences occupy equal heights.
Logarithmicln(p)Equal ratios occupy equal heights.
Percentage100 * (p - b) / abs(b)The reference reads 0%.
IndexedTo100100 + 100 * (p - b) / abs(b)The reference reads 100.

For example, a move from 100 to 110 reads +10% or 110 indexed. Prices 100, 200 and 400 have equal vertical spacing on a logarithmic scale because each step doubles the price. Logarithmic rendering requires strictly positive prices; zero and negative prices have no finite log coordinate. Choose Normal for data that can cross zero. A missing, non-finite or zero relative reference falls back to ordinary prices. Each series keeps its own first-visible-close reference, even when several share a relative scale.

After changing a mode through PriceScale::options_mut, call reset to fit the data in the new units. The scene does this after an option edit. invert_scale reverses the vertical direction without changing the prices.

scale_margins contains top and bottom fractions of the pane height. The defaults are 0.1 each, leaving the data in the middle 80%. Keep each fraction between 0 and 1 and their sum below 1; a sum of 1 or more leaves no drawable space, so the margins are ignored. With data from 100 to 120 and 10% margins, the total span is 20 / 0.8 = 25, so autoscale uses 97.5 to 122.5 in Normal mode.

set_range sets a manual range in scale units, including logarithms or percentages when that mode is active. Bounds must be finite and increasing with a finite span; invalid bounds leave the range unchanged. Margins apply to automatic fitting. Use reset to resume it after manual scaling or an axis drag.

For custom overlays, read series_price_map after Chart::show, then use price_to_y and y_to_price. Before the pane is first drawn, its map is None. See first-frame geometry.