Skip to content

Commit 9bf35c2

Browse files
committed
add rangebreak to axis
- add series with weekends and non-business hours gaps examples based on Python examples - add examples to mdbook Signed-off-by: Andrei Gherghescu <8067229+andrei-ng@users.noreply.github.com>
1 parent 63ac1dd commit 9bf35c2

File tree

10 files changed

+301
-1
lines changed

10 files changed

+301
-1
lines changed

docs/book/src/SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [Jupyter Support](./fundamentals/jupyter_support.md)
77
- [ndarray Support](./fundamentals/ndarray_support.md)
88
- [Shapes](./fundamentals/shapes.md)
9+
- [Themes](./fundamentals/themes.md)
910
- [Recipes](./recipes.md)
1011
- [Basic Charts](./recipes/basic_charts.md)
1112
- [Scatter Plots](./recipes/basic_charts/scatter_plots.md)
@@ -24,9 +25,9 @@
2425
- [Time Series and Date Axes](./recipes/financial_charts/time_series_and_date_axes.md)
2526
- [Candlestick Charts](./recipes/financial_charts/candlestick_charts.md)
2627
- [OHLC Charts](./recipes/financial_charts/ohlc_charts.md)
28+
- [Rangebreaks](./recipes/financial_charts/rangebreaks.md)
2729
- [3D Charts](./recipes/3dcharts.md)
2830
- [Scatter 3D](./recipes/3dcharts/3dcharts.md)
2931
- [Subplots](./recipes/subplots.md)
3032
- [Subplots](./recipes/subplots/subplots.md)
3133
- [Multiple Axes](./recipes/subplots/multiple_axes.md)
32-
- [Themes](./recipes/themes.md)
File renamed without changes.

docs/book/src/recipes/financial_charts.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Kind | Link
77
Time Series and Date Axes |[![Time Series and Date Axes](./img/time_series_and_date_axes.png)](./financial_charts/time_series_and_date_axes.md)
88
Candlestick Charts | [![Candlestick Charts](./img/candlestick_chart.png)](./financial_charts/candlestick_charts.md)
99
OHLC Charts | [![OHLC Charts](./img/ohlc_chart.png)](./financial_charts/ohlc_charts.md)
10+
Rangebreaks | [![Rangebreaks](./img/rangebreaks.png)](./financial_charts/rangebreaks.md)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Rangebreaks
2+
3+
The following imports have been used to produce the plots below:
4+
5+
```rust,no_run
6+
use plotly::common::{Mode, Title};
7+
use plotly::layout::{Axis, RangeBreak};
8+
use plotly::{Layout, Plot, Scatter};
9+
use chrono::{DateTime, Duration};
10+
use rand::{Rng, SeedableRng};
11+
use rand_chacha::ChaCha8Rng;
12+
```
13+
14+
The `to_inline_html` method is used to produce the html plot displayed in this page.
15+
16+
## Series with Weekend and Holiday Gaps
17+
```rust,no_run
18+
{{#include ../../../../../examples/financial_charts/src/main.rs:series_with_gaps_for_weekends_and_holidays}}
19+
```
20+
21+
{{#include ../../../../../examples/financial_charts/output/inline_series_with_gaps_for_weekends_and_holidays.html}}
22+
23+
24+
## Hiding Weekend and Holiday Gaps with Rangebreaks
25+
```rust,no_run
26+
{{#include ../../../../../examples/financial_charts/src/main.rs:hiding_weekends_and_holidays_with_rangebreaks}}
27+
```
28+
29+
{{#include ../../../../../examples/financial_charts/output/inline_hiding_weekends_and_holidays_with_rangebreaks.html}}
30+
31+
32+
## Series with Non-Business Hours Gaps
33+
```rust,no_run
34+
{{#include ../../../../../examples/financial_charts/src/main.rs:series_with_non_business_hours_gaps}}
35+
```
36+
37+
{{#include ../../../../../examples/financial_charts/output/inline_series_with_non_business_hours_gaps.html}}
38+
39+
40+
## Hiding Non-Business Hours Gaps with Rangebreaks
41+
```rust,no_run
42+
{{#include ../../../../../examples/financial_charts/src/main.rs:hiding_non_business_hours_with_rangebreaks}}
43+
```
44+
45+
{{#include ../../../../../examples/financial_charts/output/inline_hiding_non_business_hours_with_rangebreaks.html}}
41.8 KB
Loading

examples/financial_charts/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ csv = "1.1"
99
plotly = { path = "../../plotly" }
1010
plotly_utils = { path = "../plotly_utils" }
1111
serde = "1.0"
12+
chrono = "0.4"
13+
rand = "0.8"
14+
rand_chacha = "0.3"

examples/financial_charts/src/main.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::env;
44
use std::path::PathBuf;
55

6+
use chrono::{DateTime, Duration};
67
use plotly::common::TickFormatStop;
78
use plotly::layout::{Axis, RangeSelector, RangeSlider, SelectorButton, SelectorStep, StepMode};
89
use plotly::{Candlestick, Layout, Ohlc, Plot, Scatter};
@@ -320,6 +321,169 @@ fn simple_ohlc_chart(show: bool, file_name: &str) {
320321
}
321322
// ANCHOR_END: simple_ohlc_chart
322323

324+
// ANCHOR: series_with_gaps_for_weekends_and_holidays
325+
fn series_with_gaps_for_weekends_and_holidays(show: bool, file_name: &str) {
326+
let data = load_apple_data();
327+
328+
// Filter data for the specific date range as in the Python example
329+
let filtered_data: Vec<&FinData> = data
330+
.iter()
331+
.filter(|d| d.date.as_str() >= "2015-12-01" && d.date.as_str() <= "2016-01-15")
332+
.collect();
333+
334+
let date: Vec<String> = filtered_data.iter().map(|d| d.date.clone()).collect();
335+
let high: Vec<f64> = filtered_data.iter().map(|d| d.high).collect();
336+
337+
let trace = Scatter::new(date, high).mode(plotly::common::Mode::Markers);
338+
339+
let mut plot = Plot::new();
340+
plot.add_trace(trace);
341+
342+
let layout = Layout::new()
343+
.title("Series with Weekend and Holiday Gaps")
344+
.x_axis(
345+
Axis::new()
346+
.range(vec!["2015-12-01", "2016-01-15"])
347+
.title("Date"),
348+
)
349+
.y_axis(Axis::new().title("Price"));
350+
plot.set_layout(layout);
351+
352+
let path = write_example_to_html(&plot, file_name);
353+
if show {
354+
plot.show_html(path);
355+
}
356+
}
357+
// ANCHOR_END: series_with_gaps_for_weekends_and_holidays
358+
359+
// ANCHOR: hiding_weekends_and_holidays_with_rangebreaks
360+
fn hiding_weekends_and_holidays_with_rangebreaks(show: bool, file_name: &str) {
361+
let data = load_apple_data();
362+
363+
// Filter data for the specific date range as in the Python example
364+
let filtered_data: Vec<&FinData> = data
365+
.iter()
366+
.filter(|d| d.date.as_str() >= "2015-12-01" && d.date.as_str() <= "2016-01-15")
367+
.collect();
368+
369+
let date: Vec<String> = filtered_data.iter().map(|d| d.date.clone()).collect();
370+
let high: Vec<f64> = filtered_data.iter().map(|d| d.high).collect();
371+
372+
let trace = Scatter::new(date, high).mode(plotly::common::Mode::Markers);
373+
374+
let mut plot = Plot::new();
375+
plot.add_trace(trace);
376+
377+
let layout = Layout::new()
378+
.title("Hide Weekend and Holiday Gaps with rangebreaks")
379+
.x_axis(
380+
Axis::new()
381+
.range(vec!["2015-12-01", "2016-01-15"])
382+
.title("Date")
383+
.range_breaks(vec![
384+
plotly::layout::RangeBreak::new()
385+
.bounds("sat", "mon"), // hide weekends
386+
plotly::layout::RangeBreak::new()
387+
.values(vec!["2015-12-25", "2016-01-01"]), // hide Christmas and New Year's
388+
]),
389+
)
390+
.y_axis(Axis::new().title("Price"));
391+
plot.set_layout(layout);
392+
393+
let path = write_example_to_html(&plot, file_name);
394+
if show {
395+
plot.show_html(path);
396+
}
397+
}
398+
// ANCHOR_END: hiding_weekends_and_holidays_with_rangebreaks
399+
400+
// Helper to generate random walk data for all hours in a week
401+
fn generate_business_hours_data() -> (Vec<String>, Vec<f64>) {
402+
use rand::Rng;
403+
use rand::SeedableRng;
404+
use rand_chacha::ChaCha8Rng;
405+
406+
let mut dates = Vec::new();
407+
let mut values = Vec::new();
408+
let mut current_value = 0.0;
409+
let mut rng = ChaCha8Rng::seed_from_u64(42);
410+
let start_date = DateTime::parse_from_rfc3339("2020-03-02T00:00:00Z").unwrap();
411+
for day in 0..5 {
412+
// Monday to Friday
413+
for hour in 0..24 {
414+
let current_date = start_date + Duration::days(day) + Duration::hours(hour);
415+
dates.push(current_date.format("%Y-%m-%d %H:%M:%S").to_string());
416+
current_value += (rng.gen::<f64>() - 0.5) * 2.0;
417+
values.push(current_value);
418+
}
419+
}
420+
(dates, values)
421+
}
422+
423+
// ANCHOR: series_with_non_business_hours_gaps
424+
fn series_with_non_business_hours_gaps(show: bool, file_name: &str) {
425+
use chrono::NaiveDateTime;
426+
use chrono::Timelike;
427+
let (dates, all_values) = generate_business_hours_data();
428+
let mut values = Vec::with_capacity(all_values.len());
429+
430+
for (date_str, v) in dates.iter().zip(all_values.iter()) {
431+
// Parse the date string to extract hour
432+
// Format is "2020-03-02 09:00:00"
433+
if let Ok(datetime) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S") {
434+
let hour = datetime.hour();
435+
if (9..17).contains(&hour) {
436+
values.push(*v);
437+
} else {
438+
values.push(f64::NAN);
439+
}
440+
} else {
441+
values.push(f64::NAN);
442+
}
443+
}
444+
445+
let trace = Scatter::new(dates, values).mode(plotly::common::Mode::Markers);
446+
let mut plot = Plot::new();
447+
plot.add_trace(trace);
448+
let layout = Layout::new()
449+
.title("Series with Non-Business Hour Gaps")
450+
.x_axis(Axis::new().title("Time").tick_format("%b %d, %Y %H:%M"))
451+
.y_axis(Axis::new().title("Value"));
452+
plot.set_layout(layout);
453+
let path = write_example_to_html(&plot, file_name);
454+
if show {
455+
plot.show_html(path);
456+
}
457+
}
458+
// ANCHOR_END: series_with_non_business_hours_gaps
459+
460+
// ANCHOR: hiding_non_business_hours_with_rangebreaks
461+
fn hiding_non_business_hours_with_rangebreaks(show: bool, file_name: &str) {
462+
let (dates, values) = generate_business_hours_data();
463+
let trace = Scatter::new(dates, values).mode(plotly::common::Mode::Markers);
464+
let mut plot = Plot::new();
465+
plot.add_trace(trace);
466+
let layout = Layout::new()
467+
.title("Hide Non-Business Hour Gaps with rangebreaks")
468+
.x_axis(
469+
Axis::new()
470+
.title("Time")
471+
.tick_format("%b %d, %Y %H:%M")
472+
.range_breaks(vec![
473+
plotly::layout::RangeBreak::new()
474+
.bounds("17", "9")
475+
.pattern("hour"), // hide hours outside of 9am-5pm
476+
]),
477+
)
478+
.y_axis(Axis::new().title("Value"));
479+
plot.set_layout(layout);
480+
let path = write_example_to_html(&plot, file_name);
481+
if show {
482+
plot.show_html(path);
483+
}
484+
}
485+
// ANCHOR_END: hiding_non_business_hours_with_rangebreaks
486+
323487
fn main() {
324488
// Change false to true on any of these lines to display the example.
325489

@@ -341,4 +505,13 @@ fn main() {
341505

342506
// OHLC Charts
343507
simple_ohlc_chart(false, "simple_ohlc_chart");
508+
509+
// Rangebreaks usage
510+
series_with_gaps_for_weekends_and_holidays(false, "series_with_gaps_for_weekends_and_holidays");
511+
hiding_weekends_and_holidays_with_rangebreaks(
512+
false,
513+
"hiding_weekends_and_holidays_with_rangebreaks",
514+
);
515+
series_with_non_business_hours_gaps(false, "series_with_non_business_hours_gaps");
516+
hiding_non_business_hours_with_rangebreaks(false, "hiding_non_business_hours_with_rangebreaks");
344517
}

plotly/src/layout/axis.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::common::{
66
Anchor, AxisSide, Calendar, ColorBar, ColorScale, DashType, ExponentFormat, Font,
77
TickFormatStop, TickMode, Title,
88
};
9+
use crate::layout::RangeBreak;
910
use crate::private::NumOrStringCollection;
1011

1112
#[derive(Serialize, Debug, Clone)]
@@ -304,6 +305,8 @@ pub struct Axis {
304305
r#type: Option<AxisType>,
305306
#[serde(rename = "autorange")]
306307
auto_range: Option<bool>,
308+
#[serde(rename = "rangebreaks")]
309+
range_breaks: Option<Vec<RangeBreak>>,
307310
#[serde(rename = "rangemode")]
308311
range_mode: Option<RangeMode>,
309312
range: Option<NumOrStringCollection>,

plotly/src/layout/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod grid;
1818
mod legend;
1919
mod mapbox;
2020
mod modes;
21+
mod rangebreaks;
2122
mod scene;
2223
mod shape;
2324

@@ -35,6 +36,7 @@ pub use self::mapbox::{Center, Mapbox, MapboxStyle};
3536
pub use self::modes::{
3637
AspectMode, BarMode, BarNorm, BoxMode, ClickMode, UniformTextMode, ViolinMode, WaterfallMode,
3738
};
39+
pub use self::rangebreaks::RangeBreak;
3840
pub use self::scene::{
3941
Camera, CameraCenter, DragMode, DragMode3D, HoverMode, LayoutScene, Projection, ProjectionType,
4042
Rotation,

plotly/src/layout/rangebreaks.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
/// Struct representing a rangebreak for Plotly axes.
4+
/// See: https://plotly.com/python/reference/layout/xaxis/#layout-xaxis-rangebreaks
5+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6+
pub struct RangeBreak {
7+
/// Sets the lower and upper bounds for this range break, e.g. ["sat",
8+
/// "mon"]
9+
#[serde(skip_serializing_if = "Option::is_none")]
10+
pub bounds: Option<[String; 2]>,
11+
12+
/// Sets the pattern by which this range break is generated, e.g. "day of
13+
/// week"
14+
#[serde(skip_serializing_if = "Option::is_none")]
15+
pub pattern: Option<String>,
16+
17+
/// Sets the values at which this range break occurs.
18+
/// See Plotly.js docs for details.
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub values: Option<Vec<String>>,
21+
22+
/// Sets the size of each range break in milliseconds (for time axes).
23+
#[serde(skip_serializing_if = "Option::is_none")]
24+
pub dvalue: Option<u64>,
25+
26+
/// Sets whether this range break is enabled.
27+
#[serde(skip_serializing_if = "Option::is_none")]
28+
pub enabled: Option<bool>,
29+
}
30+
31+
impl Default for RangeBreak {
32+
fn default() -> Self {
33+
Self::new()
34+
}
35+
}
36+
37+
impl RangeBreak {
38+
pub fn new() -> Self {
39+
Self {
40+
bounds: None,
41+
pattern: None,
42+
values: None,
43+
dvalue: None,
44+
enabled: None,
45+
}
46+
}
47+
48+
pub fn bounds(mut self, lower: impl Into<String>, upper: impl Into<String>) -> Self {
49+
self.bounds = Some([lower.into(), upper.into()]);
50+
self
51+
}
52+
53+
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
54+
self.pattern = Some(pattern.into());
55+
self
56+
}
57+
58+
pub fn values(mut self, values: Vec<impl Into<String>>) -> Self {
59+
self.values = Some(values.into_iter().map(|v| v.into()).collect());
60+
self
61+
}
62+
63+
pub fn dvalue(mut self, dvalue: u64) -> Self {
64+
self.dvalue = Some(dvalue);
65+
self
66+
}
67+
68+
pub fn enabled(mut self, enabled: bool) -> Self {
69+
self.enabled = Some(enabled);
70+
self
71+
}
72+
}

0 commit comments

Comments
 (0)