@sjcrh/proteinpaint-rust 2.188.0 → 2.190.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 +0 -9
- package/package.json +1 -1
- package/src/manhattan_plot.rs +38 -76
- package/src/volcano.rs +89 -49
- package/src/aichatbot.rs +0 -1554
- package/src/query_classification.rs +0 -152
- package/src/summary_agent.rs +0 -201
- package/src/test_ai.rs +0 -193
package/Cargo.toml
CHANGED
|
@@ -41,7 +41,6 @@ num_cpus = "1.16.0"
|
|
|
41
41
|
memchr = "2"
|
|
42
42
|
r2d2_sqlite = "0.29.0"
|
|
43
43
|
r2d2 = "0.8.10"
|
|
44
|
-
rig-core = "0.22.0"
|
|
45
44
|
url = "2.5.7"
|
|
46
45
|
async-stream = "0.3.6"
|
|
47
46
|
base64 = "0.22.1"
|
|
@@ -124,14 +123,6 @@ path="src/readH5.rs"
|
|
|
124
123
|
name="manhattan_plot"
|
|
125
124
|
path="src/manhattan_plot.rs"
|
|
126
125
|
|
|
127
|
-
[[bin]]
|
|
128
|
-
name="query_classification"
|
|
129
|
-
path="src/query_classification.rs"
|
|
130
|
-
|
|
131
|
-
[[bin]]
|
|
132
|
-
name="summary_agent"
|
|
133
|
-
path="src/summary_agent.rs"
|
|
134
|
-
|
|
135
126
|
[[bin]]
|
|
136
127
|
name="dmrcate"
|
|
137
128
|
path="src/dmrcate.rs"
|
package/package.json
CHANGED
package/src/manhattan_plot.rs
CHANGED
|
@@ -7,7 +7,7 @@ use std::collections::{HashMap, HashSet};
|
|
|
7
7
|
use std::convert::TryInto;
|
|
8
8
|
use std::error::Error;
|
|
9
9
|
use std::fs::File;
|
|
10
|
-
use std::io::{self,
|
|
10
|
+
use std::io::{self, BufReader};
|
|
11
11
|
use tiny_skia::{FillRule, PathBuilder, Pixmap, Transform};
|
|
12
12
|
|
|
13
13
|
// Define the JSON input structure
|
|
@@ -250,7 +250,22 @@ fn cumulative_chrom(
|
|
|
250
250
|
Ok((chrom_data, cumulative_pos, sorted_chroms))
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
//
|
|
253
|
+
// Envelope JSON written by the server (Grin2Envelope). Rust only needs
|
|
254
|
+
// the geneHits rows from resultData; serde ignores other fields.
|
|
255
|
+
#[derive(Deserialize)]
|
|
256
|
+
struct Grin2Envelope {
|
|
257
|
+
#[serde(rename = "resultData")]
|
|
258
|
+
result_data: Grin2ResultData,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[derive(Deserialize)]
|
|
262
|
+
struct Grin2ResultData {
|
|
263
|
+
#[serde(rename = "geneHits")]
|
|
264
|
+
gene_hits: Vec<HashMap<String, serde_json::Value>>,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Function to read the GRIN2 envelope JSON (grin2/{cacheid}.json) and
|
|
268
|
+
// extract per-gene-per-mutation-type points for the Manhattan plot.
|
|
254
269
|
fn grin2_file_read(
|
|
255
270
|
grin2_file: &str,
|
|
256
271
|
chrom_data: &HashMap<String, ChromInfo>,
|
|
@@ -281,59 +296,16 @@ fn grin2_file_read(
|
|
|
281
296
|
let mut sig_indices: Vec<usize> = Vec::new();
|
|
282
297
|
let mut zero_q_indices: Vec<usize> = Vec::new();
|
|
283
298
|
|
|
284
|
-
let
|
|
285
|
-
let
|
|
286
|
-
|
|
287
|
-
let
|
|
288
|
-
reader
|
|
289
|
-
.read_line(&mut header_line)
|
|
290
|
-
.expect("Failed to read the first line of grin2_result_file");
|
|
291
|
-
let header: Vec<String> = header_line
|
|
292
|
-
.trim_end()
|
|
293
|
-
.split('\t')
|
|
294
|
-
.map(|s| s.trim().to_string())
|
|
295
|
-
.collect();
|
|
299
|
+
let f = File::open(grin2_file).expect("Failed to open grin2 envelope file");
|
|
300
|
+
let envelope: Grin2Envelope =
|
|
301
|
+
serde_json::from_reader(BufReader::new(f)).expect("Failed to parse grin2 envelope JSON");
|
|
302
|
+
let gene_hits = envelope.result_data.gene_hits;
|
|
296
303
|
|
|
297
|
-
// define the mutation types from the header of grin2 result file
|
|
298
304
|
let mutation_types = ["gain", "loss", "mutation", "fusion", "sv"];
|
|
299
|
-
let mut mutation_indices: HashMap<&str, (usize, Option<usize>)> = HashMap::new();
|
|
300
|
-
for name in &mutation_types {
|
|
301
|
-
let q_col = format!("q.nsubj.{name}");
|
|
302
|
-
let n_col = format!("nsubj.{name}");
|
|
303
|
-
if let Some(q_idx) = header.iter().position(|h| h == &q_col) {
|
|
304
|
-
let n_idx = header.iter().position(|h| h == &n_col);
|
|
305
|
-
mutation_indices.insert(*name, (q_idx, n_idx));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// extract the index for each required info
|
|
310
|
-
let chrom_idx = header
|
|
311
|
-
.iter()
|
|
312
|
-
.position(|h| h == "chrom")
|
|
313
|
-
.expect("Missing 'chrom' column");
|
|
314
|
-
let gene_idx = header.iter().position(|h| h == "gene").expect("Missing 'gene' column");
|
|
315
|
-
let loc_start_idx = header
|
|
316
|
-
.iter()
|
|
317
|
-
.position(|h| h == "loc.start")
|
|
318
|
-
.expect("Missing 'loc.start' column");
|
|
319
|
-
let loc_end_idx = header
|
|
320
|
-
.iter()
|
|
321
|
-
.position(|h| h == "loc.end")
|
|
322
|
-
.expect("Missing 'loc.end' column");
|
|
323
305
|
|
|
324
|
-
// loop all lines
|
|
325
306
|
let mut mut_num: usize = 0;
|
|
326
|
-
for
|
|
327
|
-
let
|
|
328
|
-
Ok(l) => l,
|
|
329
|
-
Err(e) => {
|
|
330
|
-
eprintln!("Error reading line: {}", e);
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
let fields: Vec<&str> = line.trim_end().split('\t').collect();
|
|
336
|
-
let chrom = match fields.get(chrom_idx).map(|s| s.trim()) {
|
|
307
|
+
for row in &gene_hits {
|
|
308
|
+
let chrom = match row.get("chrom").and_then(|v| v.as_str()) {
|
|
337
309
|
Some(s) if !s.is_empty() => s,
|
|
338
310
|
_ => continue,
|
|
339
311
|
};
|
|
@@ -341,30 +313,21 @@ fn grin2_file_read(
|
|
|
341
313
|
Some(info) => info,
|
|
342
314
|
None => continue,
|
|
343
315
|
};
|
|
344
|
-
let gene_name =
|
|
345
|
-
let
|
|
346
|
-
Some(
|
|
347
|
-
|
|
316
|
+
let gene_name = row.get("gene").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
|
317
|
+
let gene_start: u64 = match row.get("loc.start").and_then(|v| v.as_u64()) {
|
|
318
|
+
Some(n) => n,
|
|
319
|
+
None => continue,
|
|
348
320
|
};
|
|
349
|
-
let
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let loc_end_str = match fields.get(loc_end_idx).map(|s| s.trim()) {
|
|
353
|
-
Some(s) if !s.is_empty() => s,
|
|
354
|
-
_ => continue,
|
|
321
|
+
let gene_end: u64 = match row.get("loc.end").and_then(|v| v.as_u64()) {
|
|
322
|
+
Some(n) => n,
|
|
323
|
+
None => continue,
|
|
355
324
|
};
|
|
356
|
-
let
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let q_val_str = match fields.get(*q_idx) {
|
|
363
|
-
Some(q) => q,
|
|
364
|
-
None => continue,
|
|
365
|
-
};
|
|
366
|
-
let original_q_val: f64 = match q_val_str.parse() {
|
|
367
|
-
Ok(v) if v >= 0.0 => v,
|
|
325
|
+
let x_pos = chrom_info.start + gene_start;
|
|
326
|
+
|
|
327
|
+
for mtype in &mutation_types {
|
|
328
|
+
let q_key = format!("q.nsubj.{mtype}");
|
|
329
|
+
let original_q_val: f64 = match row.get(&q_key).and_then(|v| v.as_f64()) {
|
|
330
|
+
Some(v) if v >= 0.0 => v,
|
|
368
331
|
_ => continue,
|
|
369
332
|
};
|
|
370
333
|
|
|
@@ -377,9 +340,8 @@ fn grin2_file_read(
|
|
|
377
340
|
-original_q_val.log10()
|
|
378
341
|
};
|
|
379
342
|
|
|
380
|
-
let
|
|
381
|
-
|
|
382
|
-
.and_then(|s| s.parse::<i64>().ok());
|
|
343
|
+
let n_key = format!("nsubj.{mtype}");
|
|
344
|
+
let n_subj_count: Option<i64> = row.get(&n_key).and_then(|v| v.as_i64());
|
|
383
345
|
let color = colors.get(*mtype).unwrap_or(&"#888888".to_string()).clone();
|
|
384
346
|
// Add to plotting vectors
|
|
385
347
|
xs.push(x_pos);
|
package/src/volcano.rs
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
|
10
10
|
use plotters::prelude::*;
|
|
11
|
-
use plotters::style::ShapeStyle;
|
|
12
11
|
use serde::{Deserialize, Serialize};
|
|
13
12
|
use serde_json::Value;
|
|
14
13
|
use std::error::Error;
|
|
15
14
|
use std::io::{self, Read};
|
|
15
|
+
use tiny_skia::{Paint, PathBuilder, Pixmap, Stroke, Transform};
|
|
16
16
|
|
|
17
17
|
#[derive(Deserialize)]
|
|
18
18
|
struct Input {
|
|
@@ -36,6 +36,14 @@ struct Input {
|
|
|
36
36
|
/// only the overlay list is truncated to the most-significant N.
|
|
37
37
|
#[serde(default)]
|
|
38
38
|
max_interactive_dots: Option<usize>,
|
|
39
|
+
/// Hi-DPI scale factor (e.g. 2.0 on retina). Defaults to 1.0 when absent
|
|
40
|
+
/// so existing callers don't change behavior. The PNG is rasterized at
|
|
41
|
+
/// `(pixel_* + pad) * dpr` device pixels and is rendered at the CSS-space
|
|
42
|
+
/// dimensions reported in `plot_extent.pixel_*` — the browser uses the
|
|
43
|
+
/// extra resolution for sharpness on hi-DPI displays. Mirror of
|
|
44
|
+
/// manhattan_plot.rs's `device_pixel_ratio` handling.
|
|
45
|
+
#[serde(default)]
|
|
46
|
+
device_pixel_ratio: Option<f64>,
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
#[derive(Serialize)]
|
|
@@ -189,14 +197,23 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|
|
189
197
|
let x_max = x_max_unpadded + x_pad_data;
|
|
190
198
|
let y_min = y_min_unpadded - y_pad_data;
|
|
191
199
|
let y_max = y_max_unpadded + y_pad_data;
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
|
|
200
|
+
|
|
201
|
+
// Hi-DPR scaling. The buffer/chart are sized in device pixels (CSS * dpr)
|
|
202
|
+
// and the drawn radius/stroke are scaled the same way, so the PNG is
|
|
203
|
+
// sharper on retina. backend_coord returns device-pixel coords; we divide
|
|
204
|
+
// by dpr below to keep `pixel_x/pixel_y` in CSS-space (which is what the
|
|
205
|
+
// SVG overlay coordinate system uses). Mirror of manhattan_plot.rs.
|
|
206
|
+
let dpr = input.device_pixel_ratio.unwrap_or(1.0).max(1.0);
|
|
207
|
+
let w_hd = ((w as f64) * dpr) as u32;
|
|
208
|
+
let h_hd = ((h as f64) * dpr) as u32;
|
|
209
|
+
|
|
210
|
+
let mut buffer = vec![0u8; (w_hd as usize) * (h_hd as usize) * 3];
|
|
211
|
+
// Per-point pixel coords as plotters' chart maps them. Captured in device
|
|
212
|
+
// pixels so tiny-skia can draw the AA rings exactly at those positions;
|
|
213
|
+
// we keep a CSS-space copy below for the SVG overlay.
|
|
214
|
+
let mut pixel_coords_hd: Vec<(f64, f64)> = Vec::with_capacity(points.len());
|
|
198
215
|
{
|
|
199
|
-
let backend = BitMapBackend::with_buffer(&mut buffer, (
|
|
216
|
+
let backend = BitMapBackend::with_buffer(&mut buffer, (w_hd, h_hd));
|
|
200
217
|
let root = backend.into_drawing_area();
|
|
201
218
|
root.fill(&WHITE)?;
|
|
202
219
|
|
|
@@ -213,44 +230,77 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|
|
213
230
|
|
|
214
231
|
// Threshold guide lines are drawn by the SVG overlay on the client, not
|
|
215
232
|
// here — double-drawing them would add stray lines offset by axis padding.
|
|
233
|
+
// The dots themselves are drawn below with tiny-skia for true AA; here
|
|
234
|
+
// plotters just gives us a white-background buffer and the data-to-pixel
|
|
235
|
+
// mapping. Mirror of manhattan_plot.rs.
|
|
216
236
|
|
|
217
|
-
// Resolve colors once. Up/down fall back to `color_sig` when absent.
|
|
218
|
-
let color_sig = rgb(&input.color_significant, (214, 39, 40));
|
|
219
|
-
let color_non = rgb(&input.color_nonsignificant, (0, 0, 0));
|
|
220
|
-
let resolve = |o: &Option<String>| o.as_deref().map(|s| rgb(s, (214, 39, 40))).unwrap_or(color_sig);
|
|
221
|
-
let color_up = resolve(&input.color_significant_up);
|
|
222
|
-
let color_down = resolve(&input.color_significant_down);
|
|
223
|
-
|
|
224
|
-
// Stroke-only rings at full opacity so each ring is the exact configured
|
|
225
|
-
// group color — matching the hue the SVG overlay uses.
|
|
226
|
-
let ring = |c: RGBColor| ShapeStyle {
|
|
227
|
-
color: c.into(),
|
|
228
|
-
filled: false,
|
|
229
|
-
stroke_width: 1,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
// Draw non-significant first so significant rings overlay on top.
|
|
233
|
-
chart.draw_series(
|
|
234
|
-
points
|
|
235
|
-
.iter()
|
|
236
|
-
.filter(|p| !p.significant)
|
|
237
|
-
.map(|p| Circle::new((p.fc, p.y), radius_px, ring(color_non))),
|
|
238
|
-
)?;
|
|
239
|
-
chart.draw_series(points.iter().filter(|p| p.significant).map(|p| {
|
|
240
|
-
let c = if p.fc > 0.0 { color_up } else { color_down };
|
|
241
|
-
Circle::new((p.fc, p.y), radius_px, ring(c))
|
|
242
|
-
}))?;
|
|
243
|
-
|
|
244
|
-
// Mirror manhattan_plot.rs: capture the exact pixel coords plotters
|
|
245
|
-
// used for each point so the client overlay can land on them precisely.
|
|
246
237
|
for p in points.iter() {
|
|
247
238
|
let (px, py) = chart.backend_coord(&(p.fc, p.y));
|
|
248
|
-
|
|
239
|
+
pixel_coords_hd.push((px as f64, py as f64));
|
|
249
240
|
}
|
|
250
241
|
|
|
251
242
|
root.present()?;
|
|
252
243
|
}
|
|
253
244
|
|
|
245
|
+
// Convert plotters' RGB buffer to a tiny-skia RGBA pixmap, then stroke the
|
|
246
|
+
// dots on top with anti-aliasing — gives crisp rings even when the user
|
|
247
|
+
// zooms in past native DPR. Plotters' BitMapBackend has no AA on shapes,
|
|
248
|
+
// which is why ring edges looked chunky before this rewrite.
|
|
249
|
+
let mut pixmap = Pixmap::new(w_hd, h_hd).ok_or("failed to create pixmap")?;
|
|
250
|
+
{
|
|
251
|
+
let data = pixmap.data_mut();
|
|
252
|
+
for (src, dst) in buffer.chunks_exact(3).zip(data.chunks_exact_mut(4)) {
|
|
253
|
+
dst[..3].copy_from_slice(src);
|
|
254
|
+
dst[3] = 255;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Resolve colors once. Up/down fall back to `color_sig` when absent.
|
|
259
|
+
let color_sig = rgb(&input.color_significant, (214, 39, 40));
|
|
260
|
+
let color_non = rgb(&input.color_nonsignificant, (0, 0, 0));
|
|
261
|
+
let resolve = |o: &Option<String>| o.as_deref().map(|s| rgb(s, (214, 39, 40))).unwrap_or(color_sig);
|
|
262
|
+
let color_up = resolve(&input.color_significant_up);
|
|
263
|
+
let color_down = resolve(&input.color_significant_down);
|
|
264
|
+
|
|
265
|
+
let radius_hd_f = radius_px as f32 * dpr as f32;
|
|
266
|
+
// 1 CSS-pixel-wide stroke at hi-DPR. The stroke straddles the path, so the
|
|
267
|
+
// visible ring thickness is `stroke_width` device px ≈ 1 CSS px.
|
|
268
|
+
let mut stroke = Stroke::default();
|
|
269
|
+
stroke.width = dpr as f32;
|
|
270
|
+
let mut paint = Paint::default();
|
|
271
|
+
paint.anti_alias = true;
|
|
272
|
+
|
|
273
|
+
let stroke_ring = |pixmap: &mut Pixmap, paint: &mut Paint, color: RGBColor, px: f32, py: f32| {
|
|
274
|
+
paint.set_color_rgba8(color.0, color.1, color.2, 255);
|
|
275
|
+
let mut pb = PathBuilder::new();
|
|
276
|
+
pb.push_circle(px, py, radius_hd_f);
|
|
277
|
+
if let Some(path) = pb.finish() {
|
|
278
|
+
pixmap.stroke_path(&path, paint, &stroke, Transform::identity(), None);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Draw non-significant first so significant rings overlay on top.
|
|
283
|
+
for (i, p) in points.iter().enumerate() {
|
|
284
|
+
if p.significant {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
let (px, py) = pixel_coords_hd[i];
|
|
288
|
+
stroke_ring(&mut pixmap, &mut paint, color_non, px as f32, py as f32);
|
|
289
|
+
}
|
|
290
|
+
for (i, p) in points.iter().enumerate() {
|
|
291
|
+
if !p.significant {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
let (px, py) = pixel_coords_hd[i];
|
|
295
|
+
let c = if p.fc > 0.0 { color_up } else { color_down };
|
|
296
|
+
stroke_ring(&mut pixmap, &mut paint, c, px as f32, py as f32);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// CSS-space coords for the SVG overlay — divide the device-pixel positions
|
|
300
|
+
// by dpr. The overlay does not know about hi-DPR; the PNG sizing handles
|
|
301
|
+
// sharpness for us.
|
|
302
|
+
let all_pixel_coords: Vec<(f64, f64)> = pixel_coords_hd.iter().map(|(x, y)| (x / dpr, y / dpr)).collect();
|
|
303
|
+
|
|
254
304
|
// Build the interactive `dots` list: threshold-passers sorted asc by the
|
|
255
305
|
// chosen p-value column, optionally capped at `max_interactive_dots`.
|
|
256
306
|
let mut sig_points: Vec<&Point> = points.iter().filter(|p| p.significant).collect();
|
|
@@ -273,7 +323,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|
|
273
323
|
.collect();
|
|
274
324
|
|
|
275
325
|
let output = Output {
|
|
276
|
-
png: BASE64.encode(&
|
|
326
|
+
png: BASE64.encode(&pixmap.encode_png()?),
|
|
277
327
|
plot_extent: PlotExtent {
|
|
278
328
|
x_min,
|
|
279
329
|
x_max,
|
|
@@ -302,13 +352,3 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|
|
302
352
|
println!("{}", serde_json::to_string(&output)?);
|
|
303
353
|
Ok(())
|
|
304
354
|
}
|
|
305
|
-
|
|
306
|
-
/// Convert a plotters RGB buffer (3 bytes/px) to a PNG via tiny-skia (4 bytes/px).
|
|
307
|
-
fn encode_rgb_to_png(rgb: &[u8], w: u32, h: u32) -> Result<Vec<u8>, Box<dyn Error>> {
|
|
308
|
-
let mut pixmap = tiny_skia::Pixmap::new(w, h).ok_or("failed to create pixmap")?;
|
|
309
|
-
for (src, dst) in rgb.chunks_exact(3).zip(pixmap.data_mut().chunks_exact_mut(4)) {
|
|
310
|
-
dst[..3].copy_from_slice(src);
|
|
311
|
-
dst[3] = 255;
|
|
312
|
-
}
|
|
313
|
-
Ok(pixmap.encode_png()?)
|
|
314
|
-
}
|