1use ggplot_rs::prelude::*;
13
14const W: u32 = 720;
15const H: u32 = 500;
16const CONTRACT: f64 = 30.0; const TEAL: (u8, u8, u8) = (26, 153, 136);
20const CONTRACT_RED: (u8, u8, u8) = (200, 60, 60);
21const P90_BLUE: (u8, u8, u8) = (60, 90, 200);
22const MUTED: (u8, u8, u8) = (150, 150, 150);
23
24fn out(name: &str) -> String {
25 format!("assets/gallery/{name}.png")
26}
27
28struct Lcg(u64);
30impl Lcg {
31 fn u01(&mut self) -> f64 {
32 self.0 = self
33 .0
34 .wrapping_mul(6364136223846793005)
35 .wrapping_add(1442695040888963407);
36 ((self.0 >> 32) as u32) as f64 / (u64::from(u32::MAX) as f64 + 1.0)
37 }
38 fn normalish(&mut self) -> f64 {
40 (0..6).map(|_| self.u01()).sum::<f64>() - 3.0
41 }
42}
43
44struct Supplier {
45 name: &'static str,
46 mean: f64,
47 sd: f64,
48 seed: u64,
49 n: usize,
50}
51
52struct Po {
54 supplier: &'static str,
55 lead: f64,
56 attributable: bool,
57}
58
59fn simulate() -> Vec<Po> {
60 let suppliers = [
61 Supplier {
62 name: "Meridian Components",
63 mean: 26.0,
64 sd: 5.0,
65 seed: 11,
66 n: 200,
67 },
68 Supplier {
69 name: "Aurora Freight",
70 mean: 34.0,
71 sd: 8.0,
72 seed: 23,
73 n: 200,
74 },
75 Supplier {
76 name: "Boreal Supply",
77 mean: 28.0,
78 sd: 3.0,
79 seed: 37,
80 n: 200,
81 },
82 Supplier {
83 name: "Cobalt Metals",
84 mean: 30.0,
85 sd: 12.0,
86 seed: 51,
87 n: 200,
88 },
89 ];
90
91 let mut rows = Vec::new();
92 for s in suppliers {
93 let mut rng = Lcg(s.seed);
94 for _ in 0..s.n {
95 let excused = rng.u01() < 0.08;
98 let lead = if excused {
99 s.mean + s.sd * rng.normalish() + 20.0 + 15.0 * rng.u01()
100 } else {
101 s.mean + s.sd * rng.normalish()
102 }
103 .max(1.0);
104 rows.push(Po {
105 supplier: s.name,
106 lead,
107 attributable: !excused,
108 });
109 }
110 }
111 rows
112}
113
114fn percentile(mut xs: Vec<f64>, p: f64) -> f64 {
115 xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
116 if xs.is_empty() {
117 return 0.0;
118 }
119 let idx = ((xs.len() - 1) as f64 * p).round() as usize;
120 xs[idx]
121}
122
123fn main() -> Result<(), Box<dyn std::error::Error>> {
124 std::fs::create_dir_all("assets/gallery")?;
125 let rows = simulate();
126
127 detail(&rows)?;
128 ecdf(&rows)?;
129 compare(&rows)?;
130
131 println!("Supplier lead-time charts written to assets/gallery/");
132 Ok(())
133}
134
135fn detail(rows: &[Po]) -> Result<(), Box<dyn std::error::Error>> {
138 let name = "Meridian Components";
139 let mine: Vec<&Po> = rows.iter().filter(|p| p.supplier == name).collect();
140
141 let attributable: Vec<f64> = mine
142 .iter()
143 .filter(|p| p.attributable)
144 .map(|p| p.lead)
145 .collect();
146 let p90 = percentile(attributable.clone(), 0.90);
147
148 let dens_data: Vec<(String, Vec<Value>)> = vec![(
149 "lead".to_string(),
150 attributable.iter().map(|v| Value::Float(*v)).collect(),
151 )];
152 let excused_data: Vec<(String, Vec<Value>)> = vec![(
153 "lead".to_string(),
154 mine.iter()
155 .filter(|p| !p.attributable)
156 .map(|p| Value::Float(p.lead))
157 .collect(),
158 )];
159
160 GGPlot::new(dens_data)
161 .aes(Aes::new().x("lead"))
162 .geom_density_with(GeomDensity {
163 fill: TEAL,
164 color: (20, 110, 98),
165 alpha: 0.45,
166 line_width: 1.5,
167 })
168 .geom_vline_with(GeomVline {
170 xintercept: CONTRACT,
171 color: CONTRACT_RED,
172 width: 1.5,
173 linetype: Linetype::Dashed,
174 alpha: 1.0,
175 })
176 .geom_vline_with(GeomVline {
178 xintercept: p90,
179 color: P90_BLUE,
180 width: 1.5,
181 linetype: Linetype::Dashed,
182 alpha: 1.0,
183 })
184 .geom_rug_with(GeomRug {
186 color: MUTED,
187 alpha: 0.7,
188 length: 0.04,
189 sides: "b".to_string(),
190 })
191 .layer_data(excused_data)
192 .layer_aes(Aes::new().x("lead"))
193 .annotate_text(&format!("contract {CONTRACT:.0}d"), CONTRACT + 1.0, 0.075)
194 .annotate_text(&format!("p90 {p90:.0}d"), p90 + 1.0, 0.06)
195 .annotate_text("<- excused (external)", 48.0, 0.008)
196 .title(&format!("{name} — lead-time distribution"))
197 .subtitle("attributable deliveries only; excused shown as rug")
198 .xlab("Actual lead time (days)")
199 .ylab("Density")
200 .theme_minimal()
201 .save_with_size(&out("supplier_leadtime"), W, H)?;
202 Ok(())
203}
204
205fn ecdf(rows: &[Po]) -> Result<(), Box<dyn std::error::Error>> {
207 let name = "Meridian Components";
208 let attributable: Vec<f64> = rows
209 .iter()
210 .filter(|p| p.supplier == name && p.attributable)
211 .map(|p| p.lead)
212 .collect();
213 let on_time =
214 attributable.iter().filter(|&&l| l <= CONTRACT).count() as f64 / attributable.len() as f64;
215 let data: Vec<(String, Vec<Value>)> = vec![(
216 "lead".to_string(),
217 attributable.iter().map(|v| Value::Float(*v)).collect(),
218 )];
219
220 GGPlot::new(data)
221 .aes(Aes::new().x("lead"))
222 .geom_step()
223 .stat(StatEcdf)
224 .geom_vline_with(GeomVline {
225 xintercept: CONTRACT,
226 color: CONTRACT_RED,
227 width: 1.5,
228 linetype: Linetype::Dashed,
229 alpha: 1.0,
230 })
231 .annotate_text(
232 &format!("on-time rate {:.0}%", on_time * 100.0),
233 CONTRACT + 1.0,
234 0.15,
235 )
236 .title(&format!("{name} — on-time reliability (ECDF)"))
237 .subtitle("cumulative share at the contract line = P(lead <= 30d)")
238 .xlab("Actual lead time (days)")
239 .ylab("Cumulative share")
240 .theme_bw()
241 .save_with_size(&out("supplier_leadtime_ecdf"), W, H)?;
242 Ok(())
243}
244
245fn compare(rows: &[Po]) -> Result<(), Box<dyn std::error::Error>> {
247 let lead: Vec<Value> = rows
248 .iter()
249 .filter(|p| p.attributable)
250 .map(|p| Value::Float(p.lead))
251 .collect();
252 let supplier: Vec<Value> = rows
253 .iter()
254 .filter(|p| p.attributable)
255 .map(|p| Value::Str(p.supplier.to_string()))
256 .collect();
257 let data: Vec<(String, Vec<Value>)> = vec![
258 ("lead".to_string(), lead),
259 ("supplier".to_string(), supplier),
260 ];
261
262 GGPlot::new(data)
263 .aes(Aes::new().x("lead").fill("supplier").color("supplier"))
264 .geom_density_with(GeomDensity {
265 alpha: 0.35,
266 line_width: 1.2,
267 ..Default::default()
268 })
269 .geom_vline_with(GeomVline {
270 xintercept: CONTRACT,
271 color: CONTRACT_RED,
272 width: 1.2,
273 linetype: Linetype::Dashed,
274 alpha: 1.0,
275 })
276 .scale_fill_brewer(PaletteName::Dark2)
277 .scale_color_brewer(PaletteName::Dark2)
278 .annotate_text("contract", CONTRACT + 1.0, 0.005)
279 .title("Lead-time distributions by supplier")
280 .subtitle("attributable deliveries; dashed line = contracted lead time")
281 .xlab("Actual lead time (days)")
282 .ylab("Density")
283 .theme_minimal()
284 .save_with_size(&out("supplier_leadtime_compare"), W, H)?;
285 Ok(())
286}