Skip to content

Commit fed49e0

Browse files
committed
Add more control to log scales
1 parent ebc2307 commit fed49e0

File tree

6 files changed

+198
-93
lines changed

6 files changed

+198
-93
lines changed

examples/tick_control.rs

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ use plotters::prelude::*;
33
use std::fs::File;
44
use std::io::BufReader;
55

6-
use chrono::NaiveDate;
7-
use std::str::FromStr;
8-
96
#[derive(serde_derive::Deserialize)]
107
struct DailyData {
11-
date: String,
128
#[serde(default)]
13-
new_cases_smoothed_per_million: f64,
9+
new_cases: f64,
1410
#[serde(default)]
15-
total_cases_per_million: f64,
11+
total_cases: f64,
1612
}
1713

1814
#[derive(serde_derive::Deserialize)]
@@ -22,68 +18,65 @@ struct CountryData {
2218

2319
fn main() -> Result<(), Box<dyn std::error::Error>> {
2420
let root =
25-
BitMapBackend::gif("/tmp/tick_control.gif", (800, 600), 100)?.into_drawing_area();
26-
27-
for a in 0..200 {
28-
29-
root.fill(&WHITE)?;
30-
21+
SVGBackend::new("plotters-doc-data/tick_control.svg", (1024, 768)).into_drawing_area();
3122

32-
let mut chart = ChartBuilder::on(&root)
33-
.set_label_area_size(LabelAreaPosition::Left, (8).percent())
34-
.set_label_area_size(LabelAreaPosition::Bottom, (6).percent())
35-
.margin((1).percent())
36-
.build_cartesian_3d(
37-
(20u32..10_0000u32)
38-
.log_scale()
39-
.with_key_points(vec![50, 100, 200, 500, 1000, 10000]),
40-
(0u32..1000u32)
41-
.log_scale()
42-
.with_key_points(vec![2, 5, 10, 20, 50, 100, 200]),
43-
NaiveDate::from_ymd(2020, 1, 1)..NaiveDate::from_ymd(2020, 9, 5),
44-
)?;
23+
root.fill(&WHITE)?;
4524

46-
chart.with_projection(|mut pb| {
47-
pb.yaw = (1.57 - a as f64 / 100.0 * 1.57).abs();
48-
pb.into_matrix()
49-
});
25+
let (upper, lower) = root.split_vertically(750);
5026

51-
chart
52-
.configure_axes()
53-
.draw()?;
27+
lower.titled(
28+
"Data Source: https://covid.ourworldindata.org/data/owid-covid-data.json",
29+
("sans-serif", 10).into_font().color(&BLACK.mix(0.5)),
30+
)?;
5431

55-
let data: std::collections::HashMap<String, CountryData> = serde_json::from_reader(
56-
BufReader::new(File::open("plotters-doc-data/covid-data.json")?),
32+
let mut chart = ChartBuilder::on(&upper)
33+
.caption("World COVID-19 Cases", ("sans-serif", (5).percent_height()))
34+
.set_label_area_size(LabelAreaPosition::Left, (8).percent())
35+
.set_label_area_size(LabelAreaPosition::Bottom, (4).percent())
36+
.margin((1).percent())
37+
.build_cartesian_2d(
38+
(20u32..5000_0000u32)
39+
.log_scale()
40+
.with_key_points(vec![50, 100, 1000, 10000, 100000, 1000000, 10000000]),
41+
(0u32..50_0000u32)
42+
.log_scale()
43+
.with_key_points(vec![10, 50, 100, 1000, 10000, 100000, 200000]),
5744
)?;
5845

59-
for (idx, &series) in ["USA", "CHN"]
60-
.iter()
61-
.enumerate()
62-
{
63-
let color = Palette99::pick(idx).mix(1.0);
64-
chart
65-
.draw_series(LineSeries::new(
66-
data[series].data.iter().map(
67-
|DailyData {
68-
date,
69-
new_cases_smoothed_per_million,
70-
total_cases_per_million,
71-
..
72-
}| (*total_cases_per_million as u32, *new_cases_smoothed_per_million as u32, chrono::NaiveDate::from_str(date).unwrap(),),
73-
),
74-
color.stroke_width(1),
75-
))?
76-
.label(series)
77-
.legend(move |(x, y)| Rectangle::new([(x, y - 5), (x + 10, y + 5)], color.filled()));
78-
}
46+
chart
47+
.configure_mesh()
48+
.x_desc("Total Cases")
49+
.y_desc("New Cases")
50+
.draw()?;
7951

80-
chart
81-
.configure_series_labels()
82-
.border_style(&BLACK)
83-
.draw()?;
52+
let data: std::collections::HashMap<String, CountryData> = serde_json::from_reader(
53+
BufReader::new(File::open("plotters-doc-data/covid-data.json")?),
54+
)?;
8455

85-
root.present()?;
56+
for (idx, &series) in ["CHN", "USA", "RUS", "JPN", "DEU", "IND", "OWID_WRL"]
57+
.iter()
58+
.enumerate()
59+
{
60+
let color = Palette99::pick(idx).mix(0.9);
61+
chart
62+
.draw_series(LineSeries::new(
63+
data[series].data.iter().map(
64+
|&DailyData {
65+
new_cases,
66+
total_cases,
67+
..
68+
}| (total_cases as u32, new_cases as u32),
69+
),
70+
color.stroke_width(3),
71+
))?
72+
.label(series)
73+
.legend(move |(x, y)| Rectangle::new([(x, y - 5), (x + 10, y + 5)], color.filled()));
8674
}
8775

76+
chart
77+
.configure_series_labels()
78+
.border_style(&BLACK)
79+
.draw()?;
80+
8881
Ok(())
8982
}

src/coord/ranged1d/combinators/logarithmic.rs

Lines changed: 134 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,23 @@ impl_log_scalable!(i, u8);
4646
impl_log_scalable!(i, u16);
4747
impl_log_scalable!(i, u32);
4848
impl_log_scalable!(i, u64);
49+
4950
impl_log_scalable!(f, f32);
5051
impl_log_scalable!(f, f64);
5152

5253
pub trait IntoLogRange {
5354
type ValueType: LogScalable;
54-
fn log_scale(self) -> LogRange<Self::ValueType>;
55+
fn log_scale(self) -> LogRangeExt<Self::ValueType>;
5556
}
5657

5758
impl<T: LogScalable> IntoLogRange for Range<T> {
5859
type ValueType = T;
59-
fn log_scale(self) -> LogRange<T> {
60-
LogRange(self)
60+
fn log_scale(self) -> LogRangeExt<T> {
61+
LogRangeExt {
62+
range: self,
63+
zero: 0.0,
64+
base: 10.0,
65+
}
6166
}
6267
}
6368

@@ -66,11 +71,70 @@ impl<T: LogScalable> IntoLogRange for Range<T> {
6671
#[derive(Clone)]
6772
pub struct LogRange<V: LogScalable>(pub Range<V>);
6873

74+
#[derive(Clone)]
75+
pub struct LogRangeExt<V: LogScalable> {
76+
range: Range<V>,
77+
zero: f64,
78+
base: f64,
79+
}
80+
81+
impl<V: LogScalable> LogRangeExt<V> {
82+
pub fn zero_point(mut self, value: V) -> Self
83+
where
84+
V: PartialEq,
85+
{
86+
self.zero = if V::from_f64(0.0) == value {
87+
0.0
88+
} else {
89+
value.as_f64()
90+
};
91+
92+
self
93+
}
94+
95+
pub fn base(mut self, base: f64) -> Self {
96+
if self.base > 1.0 {
97+
self.base = base;
98+
}
99+
self
100+
}
101+
}
102+
69103
impl<V: LogScalable> From<LogRange<V>> for LogCoord<V> {
70104
fn from(range: LogRange<V>) -> LogCoord<V> {
105+
range.0.log_scale().into()
106+
}
107+
}
108+
109+
impl<V: LogScalable> From<LogRangeExt<V>> for LogCoord<V> {
110+
fn from(spec: LogRangeExt<V>) -> LogCoord<V> {
111+
let zero_point = spec.zero;
112+
let mut start = spec.range.start.as_f64() - zero_point;
113+
let mut end = spec.range.end.as_f64() - zero_point;
114+
let negative = if start < 0.0 || end < 0.0 {
115+
start = -start;
116+
end = -end;
117+
true
118+
} else {
119+
false
120+
};
121+
122+
if start < end {
123+
if start == 0.0 {
124+
start = start.max(end * 1e-5);
125+
}
126+
} else {
127+
if end == 0.0 {
128+
end = end.max(start * 1e-5);
129+
}
130+
}
71131
LogCoord {
72-
linear: (range.0.start.as_f64().ln()..range.0.end.as_f64().ln()).into(),
73-
logic: range.0,
132+
linear: (start.ln()..end.ln()).into(),
133+
logic: spec.range,
134+
normalized: start..end,
135+
base: spec.base,
136+
zero_point,
137+
negative,
74138
marker: PhantomData,
75139
}
76140
}
@@ -81,62 +145,107 @@ impl<V: LogScalable> AsRangedCoord for LogRange<V> {
81145
type Value = V;
82146
}
83147

148+
impl<V: LogScalable> AsRangedCoord for LogRangeExt<V> {
149+
type CoordDescType = LogCoord<V>;
150+
type Value = V;
151+
}
152+
84153
/// A log scaled coordinate axis
85154
pub struct LogCoord<V: LogScalable> {
86155
linear: RangedCoordf64,
87156
logic: Range<V>,
157+
normalized: Range<f64>,
158+
base: f64,
159+
zero_point: f64,
160+
negative: bool,
88161
marker: PhantomData<V>,
89162
}
90163

164+
impl<V: LogScalable> LogCoord<V> {
165+
fn value_to_f64(&self, value: &V) -> f64 {
166+
let fv = value.as_f64() - self.zero_point;
167+
if self.negative {
168+
-fv
169+
} else {
170+
fv
171+
}
172+
}
173+
174+
fn f64_to_value(&self, fv: f64) -> V {
175+
let fv = if self.negative { -fv } else { fv };
176+
V::from_f64(fv + self.zero_point)
177+
}
178+
179+
fn is_inf(&self, fv: f64) -> bool {
180+
let fv = if self.negative { -fv } else { fv };
181+
let a = V::from_f64(fv + self.zero_point);
182+
let b = V::from_f64(self.zero_point);
183+
184+
V::as_f64(&a) == V::as_f64(&b)
185+
}
186+
}
187+
91188
impl<V: LogScalable> Ranged for LogCoord<V> {
92189
type FormatOption = DefaultFormatting;
93190
type ValueType = V;
94191

95192
fn map(&self, value: &V, limit: (i32, i32)) -> i32 {
96-
let value = value.as_f64();
97-
let value = value.max(self.logic.start.as_f64()).ln();
98-
self.linear.map(&value, limit)
193+
let fv = self.value_to_f64(value);
194+
let value_ln = fv.ln();
195+
self.linear.map(&value_ln, limit)
99196
}
100197

101198
fn key_points<Hint: KeyPointHint>(&self, hint: Hint) -> Vec<Self::ValueType> {
102199
let max_points = hint.max_num_points();
103-
let tier_1 = (self.logic.end.as_f64() / self.logic.start.as_f64())
104-
.log10()
105-
.abs()
106-
.floor()
107-
.max(1.0) as usize;
108200

109-
let tier_2_density = if max_points < tier_1 {
201+
let base = self.base;
202+
let base_ln = base.ln();
203+
204+
/*let mut start = self.//self.value_to_f64(&self.logic.start);
205+
let mut end = self.value_to_f64(&self.logic.end);*/
206+
let Range { mut start, mut end } = self.normalized;
207+
208+
if start > end {
209+
std::mem::swap(&mut start, &mut end);
210+
}
211+
212+
let bold_count = ((end / start).ln().abs() / base_ln).floor().max(1.0) as usize;
213+
214+
let light_density = if max_points < bold_count {
110215
0
111216
} else {
112-
let density = 1 + (max_points - tier_1) / tier_1;
217+
let density = 1 + (max_points - bold_count) / bold_count;
113218
let mut exp = 1;
114219
while exp * 10 <= density {
115220
exp *= 10;
116221
}
117222
exp - 1
118223
};
119224

120-
let mut multiplier = 10.0;
225+
let mut multiplier = base;
121226
let mut cnt = 1;
122-
while max_points < tier_1 / cnt {
123-
multiplier *= 10.0;
227+
while max_points < bold_count / cnt {
228+
multiplier *= base;
124229
cnt += 1;
125230
}
126231

127232
let mut ret = vec![];
128-
let mut val = (10f64).powf(self.logic.start.as_f64().log10().ceil());
233+
let mut val = (base).powf((start.ln() / base_ln).ceil());
129234

130-
while val <= self.logic.end.as_f64() {
131-
ret.push(V::from_f64(val));
132-
for i in 1..=tier_2_density {
235+
while val <= end {
236+
if !self.is_inf(val) {
237+
ret.push(self.f64_to_value(val));
238+
}
239+
for i in 1..=light_density {
133240
let v = val
134241
* (1.0
135-
+ multiplier / f64::from(tier_2_density as u32 + 1) * f64::from(i as u32));
136-
if v > self.logic.end.as_f64() {
242+
+ multiplier / f64::from(light_density as u32 + 1) * f64::from(i as u32));
243+
if v > end {
137244
break;
138245
}
139-
ret.push(V::from_f64(v));
246+
if !self.is_inf(val) {
247+
ret.push(self.f64_to_value(v));
248+
}
140249
}
141250
val *= multiplier;
142251
}

src/coord/ranged1d/types/numeric.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ macro_rules! make_numeric_coord {
7979
return (limit.1 - limit.0) / 2;
8080
}
8181

82-
let logic_length = (*v - self.0) as f64 / (self.1 - self.0) as f64;
82+
83+
let logic_length = (*v as f64 - self.0 as f64) / (self.1 as f64 - self.0 as f64);
8384

8485
let actual_length = limit.1 - limit.0;
8586

src/data/float.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ pub struct FloatPrettyPrinter {
6464

6565
impl FloatPrettyPrinter {
6666
pub fn print(&self, n: f64) -> String {
67-
let (n, p) = find_minimal_repr(n, (10f64).powi(-self.max_decimal));
68-
let d_repr = float_to_string(n, p, self.min_decimal as usize);
67+
let (tn, p) = find_minimal_repr(n, (10f64).powi(-self.max_decimal));
68+
let d_repr = float_to_string(tn, p, self.min_decimal as usize);
6969
if !self.allow_scientific {
7070
d_repr
7171
} else {
@@ -91,7 +91,7 @@ impl FloatPrettyPrinter {
9191
float_to_string(sn, sp, self.min_decimal as usize),
9292
float_to_string(idx, 0, 0)
9393
);
94-
if s_repr.len() + 1 < d_repr.len() {
94+
if s_repr.len() + 1 < d_repr.len() || (tn == 0.0 && n != 0.0) {
9595
s_repr
9696
} else {
9797
d_repr

0 commit comments

Comments
 (0)