@sjcrh/proteinpaint-rust 2.188.0 → 2.189.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/package.json +1 -1
- package/src/volcano.rs +89 -49
package/package.json
CHANGED
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
|
-
}
|