Compare commits

...

8 Commits

Author SHA1 Message Date
Emilia Allison 08f647cd01
Utility for copying plates as image 2023-12-29 18:51:54 -05:00
Emilia Allison 3456be2e9a
Change wording in options menu
Father suggests that this wording is more clear to the end user.
I agree!
2023-12-29 18:51:54 -05:00
Emilia Allison 4c79cc0b4d
Set default plate format to 96 well 2023-12-29 18:51:54 -05:00
Emilia Allison 056688c4ec
Implement in_transfer hashes toggle in plates 2023-12-29 18:51:54 -05:00
Emilia Allison 4937d4ad28
Preferences menu and toggle for in_transfer hashes 2023-12-29 18:51:54 -05:00
Emilia Allison 0101846b52
Add preferences struct to main state 2023-12-29 18:51:54 -05:00
Emilia Allison ec37887c2f
Squashed commit of the following:
commit 5e1137c460
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:03:00 2023 -0500

    Fix: indexing error w.r.t. logarithm argument

commit 535b14a586
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:02:00 2023 -0500

    Space colors evenly, consistently, etc

    Colors should now:
    	- Not change if new transfers are added
    	- Be evenly spaced throughout the palette
    	- Be persistent across refreshes

commit 6e08f47955
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:01:00 2023 -0500

    Add palette function for ordered ids

    Given an id and a list of sorted ids, yields a color

commit 88e838e102
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Dec 29 18:00:00 2023 -0500

    Switch to v7 UUIDs from v4

    v7 UUIDs are timestamp based and thus we can establish a useful
    total ordering over them; will base colors on this
2023-12-29 18:51:54 -05:00
Emilia Allison 85d4b30d47
Update README.md
Updated info about import/export, including the new Import Transfer from CSV feature.
2023-10-24 21:18:10 -04:00
15 changed files with 7977 additions and 26 deletions

15
Cargo.lock generated
View File

@ -34,6 +34,12 @@ dependencies = [
"syn 2.0.16", "syn 2.0.16",
] ]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -885,10 +891,11 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.3.3" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [ dependencies = [
"atomic",
"getrandom", "getrandom",
"rand", "rand",
"serde", "serde",
@ -898,9 +905,9 @@ dependencies = [
[[package]] [[package]]
name = "uuid-macro-internal" name = "uuid-macro-internal"
version = "1.3.3" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da" checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -18,7 +18,7 @@ log = "0.4"
wasm-logger = "0.2" wasm-logger = "0.2"
regex = "1" regex = "1"
lazy_static = "1.4" lazy_static = "1.4"
uuid = { version = "1.3", features = ["v4", "fast-rng", "macro-diagnostics", "js", "serde"] } uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
csv = "1.2" csv = "1.2"

View File

@ -54,6 +54,7 @@ To add a new plate, click the "New Plate" button:
### Importing and Exporting ### Importing and Exporting
#### Export as CSV
Exporting the transfers we have created to a CSV format is the primary (if not sole) usage of Plate Tool. Exporting the transfers we have created to a CSV format is the primary (if not sole) usage of Plate Tool.
To do so, first note the "File" tab at the top-left of the screen (above the list pane). To do so, first note the "File" tab at the top-left of the screen (above the list pane).
Mouse over this tab, and a few more options will be revealed. Mouse over this tab, and a few more options will be revealed.
@ -61,20 +62,29 @@ To add a new plate, click the "New Plate" button:
You will be reminded that this is a one-way export (see JSON export/import below), You will be reminded that this is a one-way export (see JSON export/import below),
and then prompted by your browser to select a location for your file. and then prompted by your browser to select a location for your file.
Currently, it is not possible to import from nor export to a format produced by other similar software. #### Export as JSON (Saving Your Work)
Currently, it is not possible to export to a format produced by other similar software.
However, you might reasonably want to save a copy of your work However, you might reasonably want to save a copy of your work
either as a backup or to share. either as a backup or to share.
Mouse over the "File" tab, then "Export" as above, then alternatively select "Export as JSON". Mouse over the "File" tab, then "Export" as above, then alternatively select "Export as JSON".
Your browser will then prompt you to pick a suitable location to save your work as a file. Your browser will then prompt you to pick a suitable location to save your work as a file.
(See note 1 below) (See note 1 below)
#### Import from JSON (Recovering Your Work)
If we want to import one such file, mouse over the "File" tab as before If we want to import one such file, mouse over the "File" tab as before
and select "Import". and select "Import", and finally click "Import from JSON".
This opens a modal where you are prompted to upload (see note 2) This opens a modal where you are prompted to upload (see note 2)
your file; it will then be processed and loaded. your file; it will then be processed and loaded.
Keep in mind that this will overwrite any work you currently have open, Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above). so you may wish to export first (see above).
#### Import Transfer from CSV (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
_Note 1_: JSON files are plaintext! _Note 1_: JSON files are plaintext!
By default there is little whitespace (this makes comprehending them a challenge) By default there is little whitespace (this makes comprehending them a challenge)
but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice) but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice)

7830
assets/js/html2canvas.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,24 @@
function copy_screenshot(el) {
html2canvas(el).then((canvas) => {
console.log("Copying image to clipboard");
let data = canvas.toDataURL();
const textArea = document.createElement("textarea");
textArea.value = data;
document.body.prepend(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
});
}
function copy_screenshot_dest() {
let plate = document.getElementsByClassName("dest_plate")[0];
copy_screenshot(plate);
}
function copy_screenshot_src() {
let plate = document.getElementsByClassName("source_plate")[0];
copy_screenshot(plate);
}

View File

@ -12,6 +12,8 @@ div.upper_menu {
visibility: inherit; visibility: inherit;
display: flex;
div.dropdown { div.dropdown {
margin-right: 2px; margin-right: 2px;

View File

@ -4,6 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link data-trunk rel="scss" href="assets/scss/index.scss"> <link data-trunk rel="scss" href="assets/scss/index.scss">
<link data-trunk rel="copy-dir" href="assets/fonts"> <link data-trunk rel="copy-dir" href="assets/fonts">
<script data-trunk src="assets/js/screenshot_utility.js"></script>
<script data-trunk src="assets/js/html2canvas.js"></script>
<title>Plate Tool</title> <title>Plate Tool</title>
</head> </head>
</html> </html>

View File

@ -49,6 +49,15 @@ pub fn MainWindow() -> Html {
}); });
} }
let toggle_in_transfer_hashes_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
};
let new_plate_dialog_is_open = use_state_eq(|| false); let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = { let new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone(); let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
@ -476,6 +485,15 @@ pub fn MainWindow() -> Html {
</div> </div>
</div> </div>
</div> </div>
<div class="dropdown">
<button>{"Options"}</button>
<div class="dropdown-sub">
<button>{"Styles"}</button>
<div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
</div>
</div>
</div>
</div> </div>
<div class="main_container"> <div class="main_container">
<Tree open_new_plate_callback={open_new_plate_dialog_callback}/> <Tree open_new_plate_callback={open_new_plate_dialog_callback}/>

View File

@ -99,8 +99,8 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
<option value="12">{"12"}</option> <option value="12">{"12"}</option>
<option value="24">{"24"}</option> <option value="24">{"24"}</option>
<option value="48">{"48"}</option> <option value="48">{"48"}</option>
<option value="96">{"96"}</option> <option value="96" selected={true}>{"96"}</option>
<option value="384" selected={true}>{"384"}</option> <option value="384">{"384"}</option>
<option value="1536">{"1536"}</option> <option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option> <option value="3456">{"3456"}</option>
</select> </select>

View File

@ -41,6 +41,14 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
} }
let destination_wells = ct_state.transfer.transfer_region.get_destination_wells(); let destination_wells = ct_state.transfer.transfer_region.get_destination_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = { let mouse_callback = {
let m_start_handle = m_start_handle.clone(); let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone(); let m_end_handle = m_end_handle.clone();
@ -99,6 +107,11 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let mouseleave_callback = Callback::clone(&mouseup_callback); let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_dest()");
});
let column_header = { let column_header = {
let headers = (1..=props.destination_plate.plate.size().1) let headers = (1..=props.destination_plate.plate.size().1)
.map(|j| { .map(|j| {
@ -115,10 +128,10 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
<DestPlateCell i={i} j={j} <DestPlateCell i={i} j={j}
selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={destination_wells.contains(&(i,j))} in_transfer={destination_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j)) color={transfer_map.get(&(i,j))
.and_then(|t| t.last()) .and_then(|t| t.last())
.map(|t| PALETTE.get_uuid(t.get_uuid())) .map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} }
cell_height={props.cell_height} cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
@ -135,7 +148,8 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
.collect::<Html>(); .collect::<Html>();
html! { html! {
<div class={classes!{"dest_plate", <div ondblclick={screenshot_callback}
class={classes!{"dest_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}> "W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table <table
onmouseup={move |e| { onmouseup={move |e| {

View File

@ -61,6 +61,14 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let source_wells = ct_state.transfer.transfer_region.get_source_wells(); let source_wells = ct_state.transfer.transfer_region.get_source_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = { let mouse_callback = {
let m_start_handle = m_start_handle.clone(); let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone(); let m_end_handle = m_end_handle.clone();
@ -100,6 +108,10 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let mouseleave_callback = Callback::clone(&mouseup_callback); let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let column_header = { let column_header = {
let headers = (1..=props.source_plate.plate.size().1) let headers = (1..=props.source_plate.plate.size().1)
.map(|j| { .map(|j| {
@ -119,10 +131,10 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
<SourcePlateCell i={i} j={j} <SourcePlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={source_wells.contains(&(i,j))} in_transfer={source_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j)) color={transfer_map.get(&(i,j))
.and_then(|t| t.last()) .and_then(|t| t.last())
.map(|t| PALETTE.get_uuid(t.get_uuid())) .map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} }
cell_height={props.cell_height} cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
@ -140,7 +152,8 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
.collect::<Html>(); .collect::<Html>();
html! { html! {
<div class={classes!{"source_plate", <div ondblclick={screenshot_callback}
class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}> "W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table <table
onmouseup={move |e| { onmouseup={move |e| {

View File

@ -35,10 +35,23 @@ impl ColorPalette {
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64) self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)
} }
pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] { // pub fn get_uuid(&self, t: uuid::Uuid) -> [f64; 3] {
// self.get(t.as_u128() as f64 / (u128::MAX) as f64) // // self.get(t.as_u128() as f64 / (u128::MAX) as f64)
let mut r = SmallRng::seed_from_u64(t.as_u128() as u64); // let mut r = SmallRng::seed_from_u64(t.as_u128() as u64);
self.get(r.gen_range(0.0..1.0f64)) // self.get(r.gen_range(0.0..1.0f64))
// }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &Vec<uuid::Uuid>)
-> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
return self.get(Self::space_evenly(index))
}
fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize;
let d: usize = (2usize.pow(e as u32)) as usize;
let n: usize = (2*x + 1) % d;
return (n as f64) / (d as f64);
} }
} }

View File

@ -13,6 +13,17 @@ pub struct CurrentTransfer {
pub transfer: Transfer, pub transfer: Transfer,
} }
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct Preferences {
pub in_transfer_hashes: bool,
}
impl Default for Preferences {
fn default() -> Self {
Self { in_transfer_hashes: true }
}
}
#[derive(Default, PartialEq, Clone, Serialize, Deserialize)] #[derive(Default, PartialEq, Clone, Serialize, Deserialize)]
#[non_exhaustive] #[non_exhaustive]
pub struct MainState { pub struct MainState {
@ -22,6 +33,9 @@ pub struct MainState {
pub selected_source_plate: Uuid, pub selected_source_plate: Uuid,
pub selected_dest_plate: Uuid, pub selected_dest_plate: Uuid,
pub selected_transfer: Uuid, pub selected_transfer: Uuid,
#[serde(default)]
pub preferences: Preferences,
} }
impl Store for MainState { impl Store for MainState {

View File

@ -5,6 +5,8 @@ use uuid::Uuid;
#[derive(PartialEq, Clone, Serialize, Deserialize)] #[derive(PartialEq, Clone, Serialize, Deserialize)]
pub struct PlateInstance { pub struct PlateInstance {
pub plate: Plate, pub plate: Plate,
#[serde(rename = "id_v7")]
#[serde(default = "Uuid::now_v7")]
id: Uuid, id: Uuid,
pub name: String, pub name: String,
} }
@ -16,7 +18,7 @@ impl PlateInstance {
plate_type: sort, plate_type: sort,
plate_format: format, plate_format: format,
}, },
id: Uuid::new_v4(), id: Uuid::now_v7(),
name, name,
} }
} }
@ -34,7 +36,7 @@ impl From<Plate> for PlateInstance {
fn from(value: Plate) -> Self { fn from(value: Plate) -> Self {
PlateInstance { PlateInstance {
plate: value, plate: value,
id: Uuid::new_v4(), id: Uuid::now_v7(),
name: "New Plate".to_string(), name: "New Plate".to_string(),
} }
} }

View File

@ -10,7 +10,9 @@ pub struct Transfer {
pub source_id: Uuid, pub source_id: Uuid,
pub dest_id: Uuid, pub dest_id: Uuid,
pub name: String, pub name: String,
id: Uuid, #[serde(rename = "id_v7")]
#[serde(default = "Uuid::now_v7")]
pub id: Uuid,
pub transfer_region: TransferRegion, pub transfer_region: TransferRegion,
#[serde(default = "default_volume")] #[serde(default = "default_volume")]
pub volume: f32, pub volume: f32,
@ -44,7 +46,7 @@ impl Transfer {
source_id: source.get_uuid(), source_id: source.get_uuid(),
dest_id: dest.get_uuid(), dest_id: dest.get_uuid(),
name, name,
id: Uuid::new_v4(), id: Uuid::now_v7(),
transfer_region: tr, transfer_region: tr,
volume: 2.5, volume: 2.5,
} }