@sjcrh/proteinpaint-rust 2.150.0 → 2.157.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.
package/Cargo.toml CHANGED
@@ -19,6 +19,7 @@ ndarray = "0.16.1"
19
19
  hdf5 = { package = "hdf5-metno", version = "0.9.0" }
20
20
  nalgebra = {version = "0.32.2", features = ["serde-serialize"]}
21
21
  plotters = "0.3.4"
22
+ tiny-skia = "0.11"
22
23
  colorgrad = "0.6.2"
23
24
  statrs = "^0.16.0"
24
25
  fishers_exact="^1.0.1"
@@ -42,6 +43,7 @@ r2d2 = "0.8.10"
42
43
  rig-core = "0.22.0"
43
44
  url = "2.5.7"
44
45
  async-stream = "0.3.6"
46
+ base64 = "0.22.1"
45
47
 
46
48
  [profile.release]
47
49
  lto = "fat"
@@ -125,3 +127,6 @@ path="src/readH5.rs"
125
127
  name="aichatbot"
126
128
  path="src/aichatbot.rs"
127
129
 
130
+ [[bin]]
131
+ name="manhattan_plot"
132
+ path="src/manhattan_plot.rs"
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.150.0",
2
+ "version": "2.157.0",
3
3
  "name": "@sjcrh/proteinpaint-rust",
4
4
  "type": "module",
5
5
  "description": "Rust-based utilities for proteinpaint",
@@ -0,0 +1,523 @@
1
+ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
2
+ use plotters::prelude::*;
3
+ use plotters::style::ShapeStyle;
4
+ use serde::{Deserialize, Serialize};
5
+ use serde_json;
6
+ use std::collections::HashMap;
7
+ use std::convert::TryInto;
8
+ use std::error::Error;
9
+ use std::fs::File;
10
+ use std::io::{self, BufRead, BufReader};
11
+ use tiny_skia::{FillRule, PathBuilder, Pixmap, Transform};
12
+
13
+ // Define the JSON input structure
14
+ #[derive(Deserialize, Debug)]
15
+ struct Input {
16
+ file: String,
17
+ #[serde(rename = "type")]
18
+ plot_type: String,
19
+ #[serde(rename = "chrSizes")]
20
+ chromosomelist: HashMap<String, u64>,
21
+ plot_width: u64,
22
+ plot_height: u64,
23
+ device_pixel_ratio: f64,
24
+ png_dot_radius: u64,
25
+ }
26
+
27
+ // chromosome info
28
+ #[derive(Serialize)]
29
+ struct ChromInfo {
30
+ start: u64,
31
+ size: u64,
32
+ center: u64,
33
+ }
34
+
35
+ #[derive(Serialize)]
36
+ struct PointDetail {
37
+ x: u64,
38
+ y: f64,
39
+ color: String,
40
+ r#type: String,
41
+ gene: String,
42
+ chrom: String,
43
+ start: u64,
44
+ end: u64,
45
+ pos: u64,
46
+ q_value: f64,
47
+ nsubj: Option<i64>,
48
+ pixel_x: f64,
49
+ pixel_y: f64,
50
+ }
51
+
52
+ #[derive(Serialize)]
53
+ struct InteractiveData {
54
+ points: Vec<PointDetail>,
55
+ chrom_data: HashMap<String, ChromInfo>,
56
+ total_genome_length: i64,
57
+ x_buffer: i64,
58
+ y_min: f64,
59
+ y_max: f64,
60
+ }
61
+
62
+ #[derive(Serialize)]
63
+ struct Output {
64
+ png: String,
65
+ plot_data: InteractiveData,
66
+ }
67
+
68
+ // Helper function to convert hex color to RGB
69
+ fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
70
+ let hex = hex.trim_start_matches('#');
71
+ if hex.len() != 6 {
72
+ return None;
73
+ }
74
+ let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
75
+ let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
76
+ let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
77
+ Some((r, g, b))
78
+ }
79
+
80
+ // Function to Build cumulative chromosome map
81
+ fn cumulative_chrom(
82
+ chrom_size: &HashMap<String, u64>,
83
+ ) -> Result<(HashMap<String, ChromInfo>, u64, Vec<String>), Box<dyn Error>> {
84
+ let mut chrom_data: HashMap<String, ChromInfo> = HashMap::new();
85
+ let mut cumulative_pos: u64 = 0;
86
+
87
+ // Sort chromosomes
88
+ let mut sorted_chroms: Vec<String> = chrom_size.keys().cloned().collect();
89
+ sorted_chroms.sort_by_key(|chr| {
90
+ let s = chr.trim_start_matches("chr");
91
+ match s.parse::<u32>() {
92
+ Ok(n) => (0, n),
93
+ Err(_) => match s {
94
+ "X" => (1, 23),
95
+ "Y" => (1, 24),
96
+ "M" | "MT" => (1, 100),
97
+ _ => (2, u32::MAX),
98
+ },
99
+ }
100
+ });
101
+
102
+ for chrom in &sorted_chroms {
103
+ if let Some(&size) = chrom_size.get(chrom) {
104
+ chrom_data.insert(
105
+ chrom.clone(),
106
+ ChromInfo {
107
+ start: cumulative_pos,
108
+ size: size,
109
+ center: cumulative_pos + size / 2,
110
+ },
111
+ );
112
+ cumulative_pos += size;
113
+ }
114
+ }
115
+ Ok((chrom_data, cumulative_pos, sorted_chroms))
116
+ }
117
+
118
+ // Function to read the GRIN2 file
119
+ fn grin2_file_read(
120
+ grin2_file: &str,
121
+ chrom_data: &HashMap<String, ChromInfo>,
122
+ ) -> Result<(Vec<u64>, Vec<f64>, Vec<String>, Vec<PointDetail>, Vec<usize>), Box<dyn Error>> {
123
+ // Default colours
124
+ let mut colors: HashMap<String, String> = HashMap::new();
125
+ colors.insert("gain".into(), "#FF4444".into());
126
+ colors.insert("loss".into(), "#4444FF".into());
127
+ colors.insert("mutation".into(), "#44AA44".into());
128
+ colors.insert("fusion".into(), "#FFA500".into());
129
+ colors.insert("sv".into(), "#9932CC".into());
130
+
131
+ let mut xs = Vec::new();
132
+ let mut ys = Vec::new();
133
+ let mut colors_vec = Vec::new();
134
+ let mut point_details = Vec::new();
135
+ let mut sig_indices: Vec<usize> = Vec::new();
136
+
137
+ let grin2_file = File::open(grin2_file).expect("Failed to open grin2_result_file");
138
+ let mut reader = BufReader::new(grin2_file);
139
+ // get the first line (header line)
140
+ let mut header_line = String::new();
141
+ reader
142
+ .read_line(&mut header_line)
143
+ .expect("Failed to read the first line of grin2_result_file");
144
+ let header: Vec<String> = header_line
145
+ .trim_end()
146
+ .split('\t')
147
+ .map(|s| s.trim().to_string())
148
+ .collect();
149
+
150
+ // define the mutation types from the header of grin2 result file
151
+ let mutation_types = ["gain", "loss", "mutation", "fusion", "sv"];
152
+ let mut mutation_indices: HashMap<&str, (usize, Option<usize>)> = HashMap::new();
153
+ for name in &mutation_types {
154
+ let q_col = format!("q.nsubj.{name}");
155
+ let n_col = format!("nsubj.{name}");
156
+ if let Some(q_idx) = header.iter().position(|h| h == &q_col) {
157
+ let n_idx = header.iter().position(|h| h == &n_col);
158
+ mutation_indices.insert(*name, (q_idx, n_idx));
159
+ }
160
+ }
161
+
162
+ // extract the index for each required info
163
+ let chrom_idx = header
164
+ .iter()
165
+ .position(|h| h == "chrom")
166
+ .expect("Missing 'chrom' column");
167
+ let gene_idx = header.iter().position(|h| h == "gene").expect("Missing 'gene' column");
168
+ let loc_start_idx = header
169
+ .iter()
170
+ .position(|h| h == "loc.start")
171
+ .expect("Missing 'loc.start' column");
172
+ let loc_end_idx = header
173
+ .iter()
174
+ .position(|h| h == "loc.end")
175
+ .expect("Missing 'loc.end' column");
176
+
177
+ // loop all lines
178
+ let mut mut_num: usize = 0;
179
+ for line_result in reader.lines() {
180
+ let line = match line_result {
181
+ Ok(l) => l,
182
+ Err(e) => {
183
+ eprintln!("Error reading line: {}", e);
184
+ continue;
185
+ }
186
+ };
187
+
188
+ let fields: Vec<&str> = line.trim_end().split('\t').collect();
189
+ let chrom = match fields.get(chrom_idx).map(|s| s.trim()) {
190
+ Some(s) if !s.is_empty() => s,
191
+ _ => continue,
192
+ };
193
+ let chrom_info = match chrom_data.get(chrom) {
194
+ Some(info) => info,
195
+ None => continue,
196
+ };
197
+ let gene_name = fields.get(gene_idx).unwrap_or(&"").to_string();
198
+ let loc_start_str = match fields.get(loc_start_idx).map(|s| s.trim()) {
199
+ Some(s) if !s.is_empty() => s,
200
+ _ => continue,
201
+ };
202
+ let gene_start: u64 = loc_start_str
203
+ .parse()
204
+ .unwrap_or_else(|_| panic!("Invalid integer for loc.start: '{}' in line: {}", loc_start_str, line));
205
+ let loc_end_str = match fields.get(loc_end_idx).map(|s| s.trim()) {
206
+ Some(s) if !s.is_empty() => s,
207
+ _ => continue,
208
+ };
209
+ let gene_end: u64 = loc_end_str
210
+ .parse()
211
+ .unwrap_or_else(|_| panic!("Invalid integer for loc.end: '{}' in line: {}", loc_end_str, line));
212
+ let x_pos = chrom_info.start + gene_start as u64;
213
+
214
+ for (mtype, (q_idx, n_idx_opt)) in &mutation_indices {
215
+ let q_val_str = match fields.get(*q_idx) {
216
+ Some(q) => q,
217
+ None => continue,
218
+ };
219
+ let q_val: f64 = match q_val_str.parse() {
220
+ Ok(v) if v > 0.0 => v,
221
+ _ => continue,
222
+ };
223
+ let neg_log10_q = -q_val.log10();
224
+ let n_subj_count: Option<i64> = n_idx_opt
225
+ .and_then(|i| fields.get(i))
226
+ .and_then(|s| s.parse::<i64>().ok());
227
+ let color = colors.get(*mtype).unwrap_or(&"#888888".to_string()).clone();
228
+ // Add to plotting vectors
229
+ xs.push(x_pos);
230
+ ys.push(neg_log10_q);
231
+ colors_vec.push(color.clone());
232
+
233
+ // only add significant points for interactivity
234
+ if q_val <= 0.05 {
235
+ point_details.push(PointDetail {
236
+ x: x_pos,
237
+ y: neg_log10_q,
238
+ color,
239
+ r#type: mtype.to_string(),
240
+ gene: gene_name.clone(),
241
+ chrom: chrom.to_string(),
242
+ start: gene_start,
243
+ end: gene_end,
244
+ pos: gene_start,
245
+ q_value: q_val,
246
+ nsubj: n_subj_count,
247
+ pixel_x: 0.0,
248
+ pixel_y: 0.0,
249
+ });
250
+ sig_indices.push(mut_num);
251
+ };
252
+ mut_num += 1;
253
+ }
254
+ }
255
+
256
+ Ok((xs, ys, colors_vec, point_details, sig_indices))
257
+ }
258
+
259
+ // Function to create the GRIN2 Manhattan plot
260
+ fn plot_grin2_manhattan(
261
+ grin2_result_file: String,
262
+ chrom_size: HashMap<String, u64>,
263
+ plot_width: u64,
264
+ plot_height: u64,
265
+ device_pixel_ratio: f64,
266
+ png_dot_radius: u64,
267
+ ) -> Result<(String, InteractiveData), Box<dyn Error>> {
268
+ // ------------------------------------------------
269
+ // 1. Build cumulative chromosome map
270
+ // ------------------------------------------------
271
+
272
+ let mut chrom_data: HashMap<String, ChromInfo> = HashMap::new();
273
+ let mut cumulative_pos: u64 = 0;
274
+ let mut sorted_chroms: Vec<String> = Vec::new();
275
+
276
+ if let Ok((chr_data, cum_pos, chrom_sort)) = cumulative_chrom(&chrom_size) {
277
+ chrom_data = chr_data;
278
+ cumulative_pos = cum_pos;
279
+ sorted_chroms = chrom_sort;
280
+ };
281
+ let total_genome_length: i64 = cumulative_pos.try_into().unwrap();
282
+ let x_buffer = (total_genome_length as f64 * 0.005) as i64; // 0.5 % buffer
283
+
284
+ // ------------------------------------------------
285
+ // 2. Read file & collect points
286
+ // ------------------------------------------------
287
+
288
+ // Declare all data
289
+ let mut xs = Vec::new();
290
+ let mut ys = Vec::new();
291
+ let mut colors_vec = Vec::new();
292
+ let mut point_details = Vec::new();
293
+ let mut sig_indices = Vec::new();
294
+
295
+ if let Ok((x, y, c, pd, si)) = grin2_file_read(&grin2_result_file, &chrom_data) {
296
+ xs = x;
297
+ ys = y;
298
+ colors_vec = c;
299
+ point_details = pd;
300
+ sig_indices = si;
301
+ }
302
+
303
+ // ------------------------------------------------
304
+ // 3. Y-axis scaling
305
+ // ------------------------------------------------
306
+ let y_padding = png_dot_radius as f64;
307
+ let y_min = 0.0 - y_padding;
308
+ let y_max = if !ys.is_empty() {
309
+ let max_y = ys.iter().cloned().fold(f64::MIN, f64::max);
310
+ if max_y > 40.0 {
311
+ let target = 40.0;
312
+ let scale_factor_y = target / max_y;
313
+
314
+ for y in ys.iter_mut() {
315
+ *y *= scale_factor_y;
316
+ }
317
+ for p in point_details.iter_mut() {
318
+ p.y *= scale_factor_y;
319
+ }
320
+ let scaled_max = ys.iter().cloned().fold(f64::MIN, f64::max);
321
+ scaled_max + 0.35 + y_padding
322
+ } else {
323
+ max_y + 0.35 + y_padding
324
+ }
325
+ } else {
326
+ 1.0 + y_padding
327
+ };
328
+
329
+ // ------------------------------------------------
330
+ // 4. Setup high-DPR bitmap dimensions
331
+ // ------------------------------------------------
332
+
333
+ let dpr = device_pixel_ratio.max(1.0);
334
+
335
+ let png_width = plot_width + 2 * png_dot_radius;
336
+ let png_height = plot_height + 2 * png_dot_radius;
337
+
338
+ let w: u32 = (png_width * device_pixel_ratio as u64)
339
+ .try_into()
340
+ .expect("PNG width too large for u32");
341
+ let h: u32 = (png_height * device_pixel_ratio as u64)
342
+ .try_into()
343
+ .expect("PNG height too large for u32");
344
+
345
+ // Create RGB buffer for Plotters
346
+ let mut buffer = vec![0u8; w as usize * h as usize * 3];
347
+
348
+ // Make Plotters backend that draws into the RGB buffer (scale-aware)
349
+
350
+ let mut pixel_positions: Vec<(f64, f64)> = Vec::with_capacity(xs.len());
351
+ {
352
+ let backend = BitMapBackend::with_buffer(&mut buffer, (w, h));
353
+ let root = backend.into_drawing_area();
354
+ root.fill(&WHITE)?;
355
+
356
+ // ------------------------------------------------
357
+ // 5. Build the chart (no axes, no margins)
358
+ // ------------------------------------------------
359
+ let mut chart = ChartBuilder::on(&root)
360
+ .margin(0)
361
+ .set_all_label_area_size(0)
362
+ .build_cartesian_2d((-x_buffer)..(total_genome_length + x_buffer), y_min..y_max)?;
363
+
364
+ chart
365
+ .configure_mesh()
366
+ .disable_x_mesh()
367
+ .disable_y_mesh()
368
+ .disable_axes()
369
+ .draw()?;
370
+
371
+ // ------------------------------------------------
372
+ // 6. Alternating chromosome backgrounds
373
+ // ------------------------------------------------
374
+ for (i, chrom) in sorted_chroms.iter().enumerate() {
375
+ if let Some(info) = chrom_data.get(chrom) {
376
+ let bg = if i % 2 == 0 { WHITE } else { RGBColor(211, 211, 211) };
377
+ let fill_style: ShapeStyle = bg.mix(0.5).filled();
378
+ let rect = Rectangle::new(
379
+ [
380
+ (info.start as i64, (y_min + y_padding)),
381
+ ((info.start + info.size) as i64, (y_max - y_padding)),
382
+ ],
383
+ fill_style,
384
+ );
385
+ chart.draw_series(vec![rect])?;
386
+ }
387
+ }
388
+
389
+ // ------------------------------------------------
390
+ // 7. capture high-DPR pixel mapping for the points
391
+ // we do not draw the points with plotters (will use tiny-skia for AA)
392
+ // but use charts.backend_coord to map data->pixel in the high-DPR backend
393
+ // ------------------------------------------------
394
+
395
+ if !xs.is_empty() {
396
+ for (x, y) in xs.iter().zip(ys.iter()) {
397
+ // convert data coords -> high-DPR pixel coords
398
+ let (px, py) = chart.backend_coord(&(*x as i64, *y));
399
+ pixel_positions.push((px as f64, py as f64));
400
+ }
401
+ };
402
+
403
+ for (i, p) in point_details.iter_mut().enumerate() {
404
+ let (px, py) = pixel_positions[*&sig_indices[i]];
405
+ p.pixel_x = px;
406
+ p.pixel_y = py;
407
+ }
408
+
409
+ // flush root drawing area
410
+ root.present()?;
411
+ }
412
+
413
+ // Convert Plotters RGB buffer into tiny-skia RGBA pixmap
414
+ let mut pixmap = Pixmap::new(w, h).ok_or("Failed to create pixmap")?;
415
+ {
416
+ let data = pixmap.data_mut();
417
+ let mut src_i = 0usize;
418
+ let mut dst_i = 0usize;
419
+ for _ in 0..(w as usize * h as usize) {
420
+ let r = buffer[src_i];
421
+ let g = buffer[src_i + 1];
422
+ let b = buffer[src_i + 2];
423
+ data[dst_i] = r;
424
+ data[dst_i + 1] = g;
425
+ data[dst_i + 2] = b;
426
+ data[dst_i + 3] = 255u8; // opaque alpha
427
+ src_i += 3;
428
+ dst_i += 4;
429
+ }
430
+ }
431
+
432
+ // Draw anti-aliased circles using tiny-skia into the pixmap
433
+ // radius in HIGH-DPR pixels:
434
+ let radius_high_dpr = (png_dot_radius as f32) * (dpr as f32);
435
+
436
+ // Paint template
437
+ let mut paint = tiny_skia::Paint::default();
438
+
439
+ // for perfomance: reuse a PathBuilder to create circles
440
+ // will create a small path per point
441
+ for i in 0..xs.len() {
442
+ let (px, py) = pixel_positions[i]; // pixel coordinates for this point
443
+ let color_hex = &colors_vec[i];
444
+
445
+ let (r_u8, g_u8, b_u8) = match hex_to_rgb(color_hex) {
446
+ Some(rgb) => rgb,
447
+ None => (136u8, 136u8, 136u8),
448
+ };
449
+ paint.set_color_rgba8(r_u8, g_u8, b_u8, 255u8);
450
+ let mut pb = PathBuilder::new();
451
+ pb.push_circle(px as f32, py as f32, radius_high_dpr);
452
+
453
+ if let Some(path) = pb.finish() {
454
+ pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
455
+ };
456
+ }
457
+
458
+ // encode pixmap to PNG bytes
459
+ let png_bytes = pixmap.encode_png()?;
460
+ let png_data = BASE64.encode(&png_bytes);
461
+
462
+ // ------------------------------------------------
463
+ // 8. Generate interactive data
464
+ // ------------------------------------------------
465
+ let interactive_data = InteractiveData {
466
+ points: point_details,
467
+ chrom_data,
468
+ total_genome_length,
469
+ x_buffer,
470
+ y_min,
471
+ y_max,
472
+ };
473
+ Ok((png_data, interactive_data))
474
+ }
475
+
476
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
477
+ let mut input = String::new();
478
+ match io::stdin().read_line(&mut input) {
479
+ Ok(_bytes_read) => {
480
+ let input_json: Input = match serde_json::from_str(&input) {
481
+ Ok(json) => json,
482
+ Err(_err) => {
483
+ panic!("Invalid JSON input");
484
+ }
485
+ };
486
+
487
+ // input data type
488
+ // *** might need to change the key later
489
+ let input_data = &input_json.plot_type;
490
+
491
+ if input_data == "grin2" {
492
+ let grin2_file = &input_json.file;
493
+ let chrom_size = &input_json.chromosomelist;
494
+ let plot_width = &input_json.plot_width;
495
+ let plot_height = &input_json.plot_height;
496
+ let device_pixel_ratio = &input_json.device_pixel_ratio;
497
+ let png_dot_radius = &input_json.png_dot_radius;
498
+ if let Ok((base64_string, plot_data)) = plot_grin2_manhattan(
499
+ grin2_file.clone(),
500
+ chrom_size.clone(),
501
+ plot_width.clone(),
502
+ plot_height.clone(),
503
+ device_pixel_ratio.clone(),
504
+ png_dot_radius.clone(),
505
+ ) {
506
+ let output = Output {
507
+ png: base64_string,
508
+ plot_data,
509
+ };
510
+ if let Ok(json) = serde_json::to_string(&output) {
511
+ println!("{}", json);
512
+ }
513
+ } else {
514
+ eprintln!("Failed to generate Manhattan plot");
515
+ };
516
+ }
517
+ }
518
+ Err(_err) => {
519
+ panic!("Error reading input JSON!");
520
+ }
521
+ }
522
+ Ok(())
523
+ }