Compare commits

...

23 Commits

Author SHA1 Message Date
Emilia Allison e8511c01b0
fix: Update README ToC
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-11 13:56:14 -04:00
Emilia Allison 83d74cfc6f
fix: Compiler lints
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-11 11:55:46 -04:00
Emilia Allison 9aad876a31
fix: More broken tests 2024-08-11 11:51:19 -04:00
Emilia Allison b25d1eb0c6
clippy 2024-08-11 11:47:53 -04:00
Emilia Allison c6d3b86237
fix: Tests had broken references 2024-08-11 11:47:34 -04:00
Emilia Allison 51093f5efd
fix: Consider well re-usage when calculating volume
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-10 04:52:23 -04:00
Emilia Allison a1d4cc74c5
Bump versions and update README
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-10 04:31:24 -04:00
Emilia Allison e88fdb0cdd
feature: Volume management for CSV import
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
Also more likely to be correct elsewhere
2024-08-10 04:15:30 -04:00
Emilia Allison 7771ce1786
refactor: Well struct
most devious refactoring
2024-08-10 02:39:37 -04:00
Emilia Allison ca142ef594
Revert "refactor: Well struct"
This reverts commit 7e321a78c4.
2024-08-10 01:40:25 -04:00
Emilia Allison deac725fdb
Revert "refactor: Well struct in web"
This reverts commit 63790c2145.
2024-08-10 01:40:14 -04:00
Emilia Allison 7031ae33dc
fix: SCSS selector 2024-08-10 01:01:34 -04:00
Emilia Allison 63790c2145
refactor: Well struct in web 2024-08-10 00:58:55 -04:00
Emilia Allison 7e321a78c4
refactor: Well struct
Will require bump to lib version!!
2024-08-10 00:52:50 -04:00
Emilia Allison 06baf0a053
feature: Heatmap enabled indicator 2024-08-09 22:57:50 -04:00
Emilia Allison 52b28aabb1
fix: Button styling updates 2024-08-09 22:35:00 -04:00
Emilia Allison 9e1abdc8bb
fix: Expand transfer name width 2024-08-09 21:07:59 -04:00
Emilia Allison e829a49424
fix: Volume not saved on new transfer creation 2024-08-09 21:01:50 -04:00
Emilia Allison 2ccb84041b
fix: CSS for plate add buttons
Some improvement, still not great
2024-08-09 20:58:27 -04:00
Emilia Allison caf10f10c1
feature: Separate add plate button per type 2024-08-09 20:33:09 -04:00
Emilia Allison 98d2d92b49
Update documentation 2024-08-09 19:39:41 -04:00
Emilia Allison ce6e88a347
Add some doc comments to lib
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-04 14:48:45 -04:00
Emilia Allison 94386a0485
fix: Change CSV serialization headers
Makes for easier imports into Cellario CP orders
shoutout to my employer ;)
2024-08-04 14:01:26 -04:00
35 changed files with 1000 additions and 390 deletions

289
Cargo.lock generated
View File

@ -26,6 +26,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anymap"
version = "1.0.0-beta.2"
@ -76,6 +91,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bincode"
version = "1.3.3"
@ -118,6 +139,19 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"serde",
"windows-targets",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@ -128,6 +162,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "csv"
version = "1.3.0"
@ -184,6 +224,22 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fnv"
version = "1.0.7"
@ -467,12 +523,24 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hermit-abi"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.11"
@ -484,6 +552,29 @@ dependencies = [
"itoa",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -496,7 +587,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd6201e7c30ccb24773cac7efa6fec1e06189d414b7439ce756a481c8bfbf53"
dependencies = [
"indexmap",
"indexmap 1.9.3",
]
[[package]]
@ -506,7 +597,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"serde",
]
[[package]]
@ -557,6 +660,21 @@ dependencies = [
"adler",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
@ -633,7 +751,7 @@ dependencies = [
[[package]]
name = "plate-tool-lib"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"csv",
"getrandom",
@ -643,12 +761,14 @@ dependencies = [
"regex",
"serde",
"serde_json",
"serde_with",
"serde_yaml",
"uuid",
]
[[package]]
name = "plate-tool-web"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"csv",
"getrandom",
@ -669,6 +789,12 @@ dependencies = [
"yewdux",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -881,6 +1007,49 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.3.0",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.3.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -938,6 +1107,37 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tokio"
version = "1.36.0"
@ -996,6 +1196,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "uuid"
version = "1.7.0"
@ -1145,6 +1351,79 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "yew"
version = "0.20.0"
@ -1155,7 +1434,7 @@ dependencies = [
"futures",
"gloo",
"implicit-clone",
"indexmap",
"indexmap 1.9.3",
"js-sys",
"prokio",
"rustversion",

274
README.md
View File

@ -1,147 +1,193 @@
# plate-tool
A web-based tool for creating assays for your favorite (acoustic) liquid handler.
A web-based tool for creating and visualizing picklists for your favorite (possibly acoustic) liquid handler.
## Table of Contents
- [Usage](#Usage)
- [Installation](#Installation)
- [Usage](#usage)
- [Plates](#plates)
- [Adding plates](#adding-plates)
- [Modifying plates](#modifying-plates)
- [Transfers](#transfers)
- [Adding a transfer](#adding-a-transfer)
- [Modifying transfers](#modifying-transfers)
- [Importing and Exporting](#importing-and-Exporting)
- [Export as CSV](#export-as-csv)
- [Export as JSON](#export-as-json)
- [Import from JSON](#import-from-json)
- [Import Transfer from CSV](#import-transfer-from-csv)
- [Other Neat Features](#other-neat-features)
- [Installation](#installation)
## Usage
### Adding plates
### Plates
#### Adding plates
When you open plate tool for the first time,
you'll be greeted by a message informing you that no plates are selected.
To add a new plate, click the "New Plate" button:
Once you've added at least one source plate and one destination plate,
click one of each to select them.
The right-most pane will now display these plates.
### Modifying and deleting plates
Suppose you erroneously created a plate, or misspelled its name.
Double click on that plate in the list (top-left pane) and a new modal will open.
Here you can rename a plate or delete it.
You may also change the format of a plate
(note that this will not delete any data if you accidentally switch to a smaller format).
### Adding a transfer
Now that you have two plates selected,
it's time to add a transfer.
We can see all of the properties of our transfer in the bottom-left pane.
You should first name your transfer (this name is only used for your reference, and is not passed to the liquid handler).
You can enter your source and destination regions in their respective fields;
the accepted format should be familiar—capital letters for the row and arabic numerals for the column.
However, it is much easier to click-and-drag the desired region.
If we click and hold on a well (see right pane), that specifies our start well.
Then, we can drag and subsequently release on our desired end well.
Our selected wells will be highlighted in light blue for our source plate and light red for our destination plate.
You might also notice that some wells are hatched:
this indicates wells that will be used in the transfer.
Not all selected wells will necessarily be hatched,
depending on the transfer type and interleave settings.
When all of the settings are to your liking, click the "Save" button.
Note that it now appears in the "Transfers" section of the list pane.
To add a new plate, click the "Add" button for the corresponding plate type:
Once you've added at least one source plate and one destination plate,
click one of each to select them.
The right-most pane will now display these plates.
### Modifying and deleting transfers
If you already saved a transfer and would like to change it,
click on its entry in the list.
Now change the properties of the transfer as you did during initial creation.
When finished, click the "Save" button to commit these changes.
If you no longer need a transfer, select it as above and then click the "Delete" button.
### Importing and Exporting
#### Modifying plates
Suppose you erroneously created a plate, or misspelled its name.
Double click on that plate in the list (top-left pane) and a new modal will open.
Here you can rename a plate or delete it.
You may also change the format of a plate
(note that this will not delete any data if you accidentally switch to a smaller format).
#### Export as CSV
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).
Mouse over this tab, and a few more options will be revealed.
We want to export: mouse over export and select "Export as CSV".
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.
### Transfers
#### Adding a transfer
Now that you have two plates selected,
it's time to add a transfer.
We can see all of the properties of our transfer in the bottom-left pane.
You should first name your transfer (this name is only used for your reference, and is not passed to the liquid handler).
You can enter your source and destination regions in their respective fields;
the accepted format should be familiar—capital letters for the row and arabic numerals for the column.
#### 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
either as a backup or to share.
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.
(See note 1 below)
However, it is much easier to click-and-drag the desired region.
If we click and hold on a well (see right pane), that specifies our start well.
Then, we can drag and subsequently release on our desired end well.
#### Import from JSON (Recovering Your Work)
If we want to import one such file, mouse over the "File" tab as before
and select "Import", and finally click "Import from JSON".
This opens a modal where you are prompted to upload (see note 2)
your file; it will then be processed and loaded.
Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above).
Our selected wells will be highlighted in light blue for our source plate and light red for our destination plate.
You might also notice that some wells are hatched:
this indicates wells that will be used in the transfer.
Not all selected wells will necessarily be hatched,
depending on the transfer type and interleave settings.
_Note 1_: JSON files are plaintext!
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)
it immediately becomes more readable.
It is encouraged (although by no means necessary) to take a look at your export;
you will see that the representation here very closely mirrors the representation presented
in Plate Tool.
_Note 2_: Use of the word "upload" might imply that your data is leaving your computer.
It does not.
You are welcome to verify (use your browser's developer tools, it should have a network tab)
that this application does not "phone home".
Your data is stored locally (unless you choose to export it and distribute it yourself).
When all of the settings are to your liking, click the "Save" button.
Note that it now appears in the "Transfers" section of the list pane.
#### 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.
#### Modifying transfers
If you already saved a transfer and would like to change it,
click on its entry in the list.
Now change the properties of the transfer as you did during initial creation.
When finished, click the "Save" button to commit these changes.
You may either manually map plates in a picklist to plates you've already created, or click "Auto" to:
1. Generate all plates in the picklist
2. Generate transfers for all source:destination pairs.
_Note_: If you try to use this feature and no plates are available to select,
there was likely an issue parsing your picklist.
Your browser's console may have guidance as to why parsing failed;
plate-tool was probably expecting a different name for a column than was in your file.
If you no longer need a transfer, select it as above and then click the "Delete" button.
### 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.
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.
We want to export: mouse over export and select "Export as CSV".
You will be prompted by your browser to select a location for your file.
As of version `0.4.0`, it is possible to pick a CSV export format:
Mouse over options, then export, then click "Change CSV export type".
In the dialog that opens, select your desired export type.
Currently, plate-tool supports:
- Normal
- This format can be imported by Cellario's cherrypick hook.
- Echo Client
- This format is useful if you want to run a picklist directly from the
Echo Client software.
This will export just the transfers between the currently selected plates;
I assume you'd be using this feature in a non-automation context
and know to load your plates into your Echo yourself.
#### 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
either as a backup or to share.
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.
(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
and select "Import", and finally click "Import from JSON".
This opens a modal where you are prompted to upload (see note 2)
your file; it will then be processed and loaded.
Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above).
_Note 1_: JSON files are plaintext!
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)
it immediately becomes more readable.
It is encouraged (although by no means necessary) to take a look at your export;
you will see that the representation here very closely mirrors the representation presented
in Plate Tool.
_Note 2_: Use of the word "upload" might imply that your data is leaving your computer.
It does not.
You are welcome to verify (use your browser's developer tools, it should have a network tab)
that this application does not "phone home".
Your data is stored locally (unless you choose to export it and distribute it yourself).
#### 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.
You may either manually map plates in a picklist to plates you've already created, or click "Auto" to:
1. Generate all plates in the picklist
2. Generate transfers for all source:destination pairs.
_Note_: If you try to use this feature and no plates are available to select,
there was likely an issue parsing your picklist.
Your browser's console may have guidance as to why parsing failed;
plate-tool was probably expecting a different name for a column than was in your file.
_Note_: If you find a picklist that Cellario *can* import that plate-tool cannot,
please email me!
Odds are your picklist contains a weird edge case I've not considered,
and I would like to fix that!
### Other Neat Features
#### Taking Pictures of Plates
If you double click on a plate
(try to avoid clicking a well since that will change your selection)
plate-tool will do some magic to take a screenshot of your plate
and deposit it in your clipboard for you.
You can then paste this into PowerPoint, GIMP, or whereever else
you want a pretty picture of a plate.
#### Taking Pictures of Plates
If you double click on a plate
(try to avoid clicking a well since that will change your selection)
plate-tool will do some magic to take a screenshot of your plate
and deposit it in your clipboard for you.
You can then paste this into PowerPoint, GIMP, or whereever else
you want a pretty picture of a plate.
I hope this is helpful for arts and crafts.
_NOTE:_ I won't guarantee this feature will work in all contexts;
it relies on your browser thinking that you have plate-tool open
in a "secure context" (localhost or https).
_NOTE:_ I won't guarantee this feature will work in all contexts;
it relies on your browser thinking that you have plate-tool open
in a "secure context" (localhost or https).
#### Turn off the in-transfer hashes
Have you noticed that when you select a transfer, the wells to
be used in that transfer have little diagonal lines over them?
Hopefully you have, because this is supposed to happen!
However, if you want to take a Pretty Plate Picture (see above),
you might want these indicators turned off.
#### Turn off the in-transfer hashes
Have you noticed that when you select a transfer, the wells to
be used in that transfer have little diagonal lines over them?
Hopefully you have, because this is supposed to happen!
However, if you want to take a Pretty Plate Picture (see above),
you might want these indicators turned off.
To disable the indicators, mouse over "Options" (top-left of screen), then "Styles",
then click "Toggle transfer hashes".
To turn them back on, do the exact same thing.
To disable the indicators, mouse over "Options" (top-left of screen), then "Styles",
then click "Toggle transfer hashes".
To turn them back on, do the exact same thing.
#### Transfer Volume Heatmap (Experimental)
This can be used to verify that all of the wells in a plate will have the same volume
transferred at a glance.
Wells will be colored based on the sum of all transfers using that plate.
To toggle this feature, mouse over "Options", then "Styles", then click "Toggle volume heatmap".
_NOTE_: The scale for the colors is spaced linearly; if you have a well that is being used
significantly more than some others, it may be difficult to see the difference between other wells
with more similar volumes.
If you have a use case that would benefit from a logarithmic scale here, please let me know.
## Installation
Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience.
However, you're absolutely welcome to host your own instance (even locally).
Here's how:
(_Note:_ ~~If you run Windows you're probably best off doing the following in WSL2~~ You're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths.)
(_Note:_ If you run Windows you're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths. I haven't personally validated this. )
1. Make sure you have a working Rust toolchain
1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/),

View File

@ -1,6 +1,6 @@
[package]
name = "plate-tool-lib"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,3 +15,5 @@ csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
lazy_static = "1.4"
serde_with = { version = "3.9.0", features = ["json"] }
serde_yaml = "0.9.34"

View File

@ -2,9 +2,10 @@ use serde::Serialize;
use super::TransferRecord;
// Format preferred by the Echo Client when using the Pick-List function.
// Note that this does not export plate info!
// Exports of this type should combine all transfers between the two actively selected plates.
/// Format preferred by the Echo Client when using the Pick-List function.
///
/// Note that this does not export plate info!
/// Exports of this type should combine all transfers between the two actively selected plates.
#[derive(Serialize, Debug)]
pub struct EchoClientTransferRecord {
#[serde(rename = "SrcWell")]

View File

@ -1,10 +1,17 @@
//! Auto module for importing picklists without creating plates first
//!
//! Before the auto module, it was necessary for a user to create the plates
//! used in an imported transfer before import.
use super::{string_well_to_pt, TransferRecord, read_csv};
use crate::plate::{PlateFormat, PlateType};
use crate::plate_instances::PlateInstance;
use crate::transfer::Transfer;
use crate::transfer_volume::{TransferVolume, VolumeMaps};
use crate::Well;
use crate::transfer_region::{CustomRegion, Region, TransferRegion};
use std::collections::HashSet;
use std::collections::{HashSet, HashMap};
pub struct AutoOutput {
pub sources: Vec<PlateInstance>,
@ -12,6 +19,7 @@ pub struct AutoOutput {
pub transfers: Vec<Transfer>,
}
/// Two lists of plates that are guaranteed to be unique (no duplicate plates)
struct UniquePlates {
sources: Vec<PlateInstance>,
destinations: Vec<PlateInstance>,
@ -24,6 +32,12 @@ const W384_ROW_MAX: u8 = 16;
const W1536_COL_MAX: u8 = 48;
const W1536_ROW_MAX: u8 = 32;
/// Main function for the auto module
/// Takes a list of pre-deserialized transfer records and generates output
/// This output is 3 lists of:
/// 1. The source plates used in the picklist
/// 2. The destination plates used in the picklist
/// 3. All of the transfers between those plates
pub fn auto(records: &[TransferRecord]) -> AutoOutput {
let unique_plates = find_unique_plates(records);
let transfers = get_transfer_for_all_pairs(records, &unique_plates);
@ -31,11 +45,18 @@ pub fn auto(records: &[TransferRecord]) -> AutoOutput {
AutoOutput { sources: unique_plates.sources, destinations: unique_plates.destinations, transfers }
}
/// Helper function that reads a CSV and immediately dumps the resulting transfer records
/// into the auto function defined in this module.
pub fn read_csv_auto(data: &str) -> AutoOutput {
let transfer_records = read_csv(data);
auto(&transfer_records)
}
/// Looks through a list of transfer records and generates two lists
/// of all of the unique plates found in those transfers.
/// Worth noting that the Transfer struct requires two associated PlateInstances;
/// these PlateInstance structs are generated here---they are not present in a TransferRecord.
/// See notes on the UniquePlates struct
fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
let mut source_names: HashSet<&str> = HashSet::new();
let mut destination_names: HashSet<&str> = HashSet::new();
@ -79,10 +100,16 @@ fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
}
}
/// Tries to guess the format of a plate given the southeastern-est well used.
/// A picklist does not necessarily contain plate format info; this method guesses
/// based on the well furthest from A1 that exists in a transfer.
/// It's possible for a 1536 well plate to be seen as a 384 well if all of the wells
/// used could have fit in the top-left quadrant of a 384 well plate
/// (i.e. all lower than P24 in this case)
fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat {
let mut guess = PlateFormat::W96; // Never guess smaller than 96
for record in plate_filtered_records {
if let Some((row, col)) = string_well_to_pt(match which {
if let Some(Well { row, col }) = string_well_to_pt(match which {
PlateType::Source => &record.source_well,
PlateType::Destination => &record.destination_well,
}) {
@ -116,6 +143,17 @@ fn get_transfer_for_all_pairs(
transfers
}
/// Given a source and destination plate, get all of the wells transferred
/// from source to destination and turn this into a Transfer.
///
/// Note that even if you have what looks like two separate transfers,
/// this will join all transfers for a given source-dest pair.
/// For example:
/// Consider expanding all of src:A1 to the column dst:A
/// and src:B1 -> row dst:1
/// If you were making this transfer in plate tool, it would be easiest
/// to do this as two separate transfers.
/// On import, this would be seen as one transfer.
fn get_transfer_for_pair(
records: &[TransferRecord],
source: &PlateInstance,
@ -129,12 +167,16 @@ fn get_transfer_for_pair(
.filter(|x| x.source_plate == source_name && x.destination_plate == destination_name)
.peekable();
if filtered_records.peek().is_none() {
return None;
}
filtered_records.peek()?;
let mut source_wells: HashSet<Well> = HashSet::new();
let mut destination_wells: HashSet<Well> = HashSet::new();
// Volume Hash Maps
let mut source_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut destination_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut paired_volume_map: HashMap<(Well, Well), f32> = HashMap::new();
let mut source_wells: HashSet<(u8, u8)> = HashSet::new();
let mut destination_wells: HashSet<(u8, u8)> = HashSet::new();
for record in filtered_records {
let source_point_opt = string_well_to_pt(&record.source_well);
let destination_point_opt = string_well_to_pt(&record.destination_well);
@ -145,10 +187,23 @@ fn get_transfer_for_pair(
source_wells.insert(source_point);
destination_wells.insert(destination_point);
source_only_volume_map.entry(source_point).and_modify(|v| *v += record.volume)
.or_insert(record.volume);
destination_only_volume_map.entry(destination_point).and_modify(|v| *v += record.volume)
.or_insert(record.volume);
paired_volume_map.entry((source_point, destination_point)).and_modify(|v| *v += record.volume)
.or_insert_with(|| record.volume); // No idea why this one needs to be different
}
}
let source_wells_vec: Vec<(u8, u8)> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<(u8, u8)> = destination_wells.into_iter().collect();
let source_wells_vec: Vec<Well> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<Well> = destination_wells.into_iter().collect();
let transfer_volume: TransferVolume = TransferVolume::WellMap(VolumeMaps {
source_only: source_only_volume_map,
destination_only: destination_only_volume_map,
paired: paired_volume_map,
});
let custom_region: Region =
Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec));
@ -164,10 +219,13 @@ fn get_transfer_for_pair(
let transfer_name = format!("{} to {}", source.name, destination.name);
Some(Transfer::new(
let mut transfer = Transfer::new(
source.clone(),
destination.clone(),
transfer_region,
transfer_name,
))
);
transfer.volume = transfer_volume;
Some(transfer)
}

View File

@ -1,7 +1,11 @@
use crate::transfer::Transfer;
use crate::transfer_volume::TransferVolume;
use crate::util::*;
use crate::{transfer::Transfer, Well};
use super::{alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers, transfer_record::TransferRecordDeserializeIntermediate, TransferRecord};
use super::{
alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers,
transfer_record::TransferRecordDeserializeIntermediate, TransferRecord,
};
use lazy_static::lazy_static;
use regex::Regex;
use std::error::Error;
@ -15,17 +19,32 @@ pub fn transfer_to_records(
let map = tr.transfer_region.calculate_map();
let mut records: Vec<TransferRecord> = vec![];
if let TransferVolume::WellMap(wm) = &tr.volume {
log::debug!("{:?}\n{}", wm.paired, wm.paired.len());
}
for s_well in source_wells {
let dest_wells = map(s_well);
if let Some(dest_wells) = dest_wells {
for d_well in dest_wells {
// Get volume here:
let volume: f32 = match &tr.volume {
TransferVolume::Single(x) => *x, // If all have the same value, use it everywhere
TransferVolume::WellMap(wm) => {
*wm.paired.get(&(s_well, d_well)).unwrap_or(&2.5f32)
}
};
records.push(TransferRecord {
source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1),
source_well: format!("{}{}", num_to_letters(s_well.row).unwrap(), s_well.col),
destination_plate: dest_barcode.to_string(),
destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1),
volume: tr.volume,
destination_well: format!(
"{}{}",
num_to_letters(d_well.row).unwrap(),
d_well.col
),
volume,
concentration: None,
})
}
@ -52,16 +71,19 @@ pub fn records_to_echo_client_csv(trs: Vec<TransferRecord>) -> Result<String, Bo
Ok(data)
}
pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> {
/// Converts "spreadsheet format" well identification to coordinates
pub fn string_well_to_pt(input: &str) -> Option<Well> {
lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
// Can this be removed?
static ref REGEX_ALT: Regex = Regex::new(r"(\d+)").unwrap();
}
if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
return Some((row, col))
return Some(Well { row, col });
} else {
return None
return None;
}
}
None

View File

@ -1,6 +1,15 @@
//! This contains the `mangle_headers` function and its helpers
//!
//! Mangling headers is necessary to normalize CSV column names before
//! deserialization can be done by Serde.
//! Field detection occurs in `detect_field`.
//!
//! As of lib 0.4.0, unrecognized headers are ignored and left untouched by mangling.
//! However, they are not passed through to any subsequent exports from plate-tool.
pub fn mangle_headers(data: &str) -> String {
let (header, rows) = data.split_at(data.find('\n').unwrap());
let fields = header.trim().split(",");
let fields = header.trim().split(',');
let mut modified_headers: Vec<String> = Vec::new();
for field in fields {
if let Some(f) = detect_field(field) {

View File

@ -4,7 +4,6 @@ mod auto;
mod mangle_headers;
mod alternative_formats;
pub use transfer_record::volume_default;
pub use transfer_record::TransferRecord;
pub use conversion::*;

View File

@ -1,14 +1,18 @@
use serde::{Deserialize, Serialize};
use crate::{plate::PlateFormat, transfer::Transfer, util::num_to_letters};
use crate::{plate::PlateFormat, util::num_to_letters};
/// Represents a single line of a CSV picklist.
/// In practice, this is generated from the deserialize intermediate.
/// A list of `TransferRecord`s can be used to create a `transfer::Transfer` struct;
/// see the `auto` module for this.
#[derive(Serialize, Debug, Clone)]
pub struct TransferRecord {
#[serde(rename = "Source Plate")]
#[serde(rename = "Source Barcode")]
pub source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Plate")]
#[serde(rename = "Dest Barcode")]
pub destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
@ -18,6 +22,12 @@ pub struct TransferRecord {
pub concentration: Option<f32>,
}
/// The deserialization intermediate is generated from a mangled CSV s.t.
/// the column headers are standardized to those given to the derive macro.
///
/// `TransferRecord` can *always* be generated from the intermediate.
/// Currently there's no reason why the inverse couldn't be true,
/// but there's no reason to convert back into the deserialize-only intermediate.
#[derive(Deserialize, Debug, Clone)]
pub struct TransferRecordDeserializeIntermediate {
#[serde(rename = "sourceplate")]
@ -64,7 +74,7 @@ impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
}
}
let volume = value.volume.unwrap_or(volume_default());
let volume = value.volume.unwrap_or(2.5f32);
TransferRecord {
source_plate: value.source_plate,
@ -78,6 +88,7 @@ impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
}
impl TransferRecordDeserializeIntermediate {
/// Used to cull malformed picklist entries that lack required information
pub fn is_empty(&self) -> bool {
self.source_plate.is_empty()
|| self.destination_plate.is_empty()
@ -94,7 +105,3 @@ fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option<Stri
Some(format!("{}{}", row_str, column))
}
pub fn volume_default() -> f32 {
Transfer::default().volume
}

View File

@ -3,4 +3,8 @@ pub mod plate;
pub mod plate_instances;
pub mod transfer;
pub mod transfer_region;
pub mod transfer_volume;
pub mod util;
mod well;
pub use well::Well;

View File

@ -1,5 +1,8 @@
use crate::transfer_volume;
use super::plate_instances::*;
use super::transfer_region::*;
use super::transfer_volume::*;
use serde::Deserialize;
use serde::Serialize;
use uuid::Uuid;
@ -14,8 +17,7 @@ pub struct Transfer {
#[serde(default = "Uuid::now_v7")]
pub id: Uuid,
pub transfer_region: TransferRegion,
#[serde(default = "default_volume")]
pub volume: f32,
pub volume: TransferVolume,
}
impl Default for Transfer {
@ -26,15 +28,11 @@ impl Default for Transfer {
name: "New Transfer".to_string(),
id: Default::default(),
transfer_region: Default::default(),
volume: default_volume(),
volume: Default::default(),
}
}
}
fn default_volume() -> f32 {
2.5f32
}
impl Transfer {
pub fn new(
source: PlateInstance,
@ -48,7 +46,7 @@ impl Transfer {
name,
id: Uuid::now_v7(),
transfer_region: tr,
volume: 2.5,
volume: transfer_volume::TransferVolume::Single(2.5),
}
}

View File

@ -1,31 +1,32 @@
use serde::{Deserialize, Serialize};
use super::plate::Plate;
use crate::Well;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion {
src: Vec<(u8, u8)>,
dest: Vec<(u8, u8)>,
src: Vec<Well>,
dest: Vec<Well>,
}
impl CustomRegion {
pub fn new(src: Vec<(u8, u8)>, dest: Vec<(u8, u8)>) -> Self {
pub fn new(src: Vec<Well>, dest: Vec<Well>) -> Self {
CustomRegion { src, dest }
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region {
Rect((u8, u8), (u8, u8)),
Point((u8, u8)),
Rect(Well, Well),
Point(Well),
Custom(CustomRegion),
}
impl Default for Region {
fn default() -> Self {
Region::Point((1, 1))
Region::Point(Well { row: 1, col: 1 })
}
}
impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
impl TryFrom<Region> for (Well, Well) {
type Error = &'static str;
fn try_from(region: Region) -> Result<Self, Self::Error> {
if let Region::Rect(c1, c2) = region {
@ -37,12 +38,10 @@ impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
}
}
type Corner = (u8, u8);
type Rectangle = (Corner, Corner);
impl Region {
pub fn new_custom(transfers: &Vec<Rectangle>) -> Self {
let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
pub fn new_custom(transfers: &Vec<(Well, Well)>) -> Self {
let mut src_pts: Vec<Well> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<Well> = Vec::with_capacity(transfers.len());
for transfer in transfers {
src_pts.push(transfer.0);
@ -80,10 +79,10 @@ impl Default for TransferRegion {
}
impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
pub fn get_source_wells(&self) -> Vec<Well> {
match &self.source_region {
Region::Rect(c1, c2) => {
let mut wells = Vec::<(u8, u8)>::new();
let mut wells = Vec::<Well>::new();
let (ul, br) = standardize_rectangle(c1, c2);
let (interleave_i, interleave_j) = self.interleave_source;
// NOTE: This will panic if either is 0!
@ -93,12 +92,12 @@ impl TransferRegion {
let (interleave_i, interleave_j) =
(i8::max(interleave_i, 1), i8::max(interleave_j, 1));
for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) {
for i in (ul.row..=br.row).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.col..=br.col).step_by(i8::abs(interleave_j) as usize) {
// NOTE: It looks like we're ignoring negative interleaves,
// because it wouldn't make a difference here---the same
// wells will still be involved in the transfer.
wells.push((i, j))
wells.push(Well { row: i, col: j })
}
}
wells
@ -108,14 +107,14 @@ impl TransferRegion {
}
}
pub fn get_destination_wells(&self) -> Vec<(u8, u8)> {
pub fn get_destination_wells(&self) -> Vec<Well> {
match &self.source_region {
Region::Custom(c) => c.dest.clone(),
_ => {
let map = self.calculate_map();
let source_wells = self.get_source_wells();
let mut wells = Vec::<(u8, u8)>::new();
let mut wells = Vec::<Well>::new();
for well in source_wells {
if let Some(mut dest_wells) = map(well) {
@ -129,14 +128,14 @@ impl TransferRegion {
}
#[allow(clippy::type_complexity)] // Resolving gives inherent associated type error
pub fn calculate_map(&self) -> Box<dyn Fn((u8, u8)) -> Option<Vec<(u8, u8)>> + '_> {
pub fn calculate_map(&self) -> Box<dyn Fn(Well) -> Option<Vec<Well>> + '_> {
// By validating first, we have a stronger guarantee that
// this function will not panic. :)
// log::debug!("Validating: {:?}", self.validate());
if let Err(msg) = self.validate() {
eprintln!("{}", msg);
eprintln!("This transfer will be empty.");
return Box::new(|(_, _)| None);
return Box::new(|_| None);
}
// log::debug!("What is ild? {:?}", self);
@ -144,10 +143,10 @@ impl TransferRegion {
let il_dest = self.interleave_dest;
let il_source = self.interleave_source;
let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
Region::Point((x, y)) => ((x, y), (x, y)),
let source_corners: (Well, Well) = match self.source_region {
Region::Point(w) => (w, w),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)),
Region::Custom(_) => (Well { row: 0, col: 0 }, Well { row: 0, col: 0 }),
};
let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1);
// This map is not necessarily injective or surjective,
@ -157,43 +156,44 @@ impl TransferRegion {
// Non-replicate transfers:
match &self.dest_region {
Region::Point((x, y)) => {
Box::new(move |(i, j)| {
if source_wells.contains(&(i, j)) {
Region::Point(Well { row: x, col: y }) => {
Box::new(move |Well { row: i, col: j }| {
if source_wells.contains(&Well { row: i, col: j }) {
// Validity here already checked by self.validate()
Some(vec![(
x + i
.checked_sub(source_ul.0)
Some(vec![Well {
row: x + i
.checked_sub(source_ul.row)
.expect("Point cannot have been less than UL")
.checked_div(il_source.0.unsigned_abs())
.expect("Source interleave cannot be 0")
.mul(il_dest.0.unsigned_abs()),
y + j
.checked_sub(source_ul.1)
col: y + j
.checked_sub(source_ul.col)
.expect("Point cannot have been less than UL")
.checked_div(il_source.1.unsigned_abs())
.expect("Source interleave cannot be 0")
.mul(il_dest.1.unsigned_abs()),
)])
}])
} else {
None
}
})
}
Region::Rect(c1, c2) => {
Box::new(move |(i, j)| {
if source_wells.contains(&(i, j)) {
Box::new(move |w| {
let Well { row: i, col: j } = w;
if source_wells.contains(&w) {
let possible_destination_wells = create_dense_rectangle(c1, c2);
let (d_ul, d_br) = standardize_rectangle(c1, c2);
let (s_ul, s_br) =
standardize_rectangle(&source_corners.0, &source_corners.1);
let s_dims = (
s_br.0.checked_sub(s_ul.0).unwrap() + 1,
s_br.1.checked_sub(s_ul.1).unwrap() + 1,
s_br.row.checked_sub(s_ul.row).unwrap() + 1,
s_br.col.checked_sub(s_ul.col).unwrap() + 1,
);
let d_dims = (
d_br.0.checked_sub(d_ul.0).unwrap() + 1,
d_br.1.checked_sub(d_ul.1).unwrap() + 1,
d_br.row.checked_sub(d_ul.row).unwrap() + 1,
d_br.col.checked_sub(d_ul.col).unwrap() + 1,
);
let number_used_src_wells = (
// Number of used source wells
@ -222,34 +222,34 @@ impl TransferRegion {
.unwrap() as u8,
);
let i = i
.saturating_sub(s_ul.0)
.saturating_sub(s_ul.row)
.saturating_div(il_source.0.unsigned_abs());
let j = j
.saturating_sub(s_ul.1)
.saturating_sub(s_ul.col)
.saturating_div(il_source.1.unsigned_abs());
Some(
possible_destination_wells
.into_iter()
.filter(|(x, _)| {
x.checked_sub(d_ul.0).unwrap()
.filter(|Well { row: x , ..}| {
x.checked_sub(d_ul.row).unwrap()
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x
== (il_dest.0.unsigned_abs() *i)
% (number_used_src_wells.0 * il_dest.0.unsigned_abs())
})
.filter(|(_, y)| {
y.checked_sub(d_ul.1).unwrap()
.filter(|Well { col: y, .. }| {
y.checked_sub(d_ul.col).unwrap()
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u
== (il_dest.1.unsigned_abs() *j)
% (number_used_src_wells.1 * il_dest.1.unsigned_abs())
})
.filter(|(x, y)| {
.filter(|Well { row: x, col: y }| {
// How many times have we replicated? < How many are we allowed
// to replicate?
x.checked_sub(d_ul.0).unwrap().div_euclid(
x.checked_sub(d_ul.row).unwrap().div_euclid(
number_used_src_wells.0 * il_dest.0.unsigned_abs(),
) < count.0
&& y.checked_sub(d_ul.1).unwrap().div_euclid(
&& y.checked_sub(d_ul.col).unwrap().div_euclid(
number_used_src_wells.1 * il_dest.1.unsigned_abs(),
) < count.1
})
@ -260,14 +260,14 @@ impl TransferRegion {
}
})
}
Region::Custom(c) => Box::new(move |(i, j)| {
Region::Custom(c) => Box::new(move |Well { row: i, col: j }| {
let src = c.src.clone();
let dest = c.dest.clone();
let points: Vec<(u8, u8)> = src
let points: Vec<Well> = src
.iter()
.enumerate()
.filter(|(_index, (x, y))| *x == i && *y == j)
.filter(|(_index, Well { row: x, col: y })| *x == i && *y == j)
.map(|(index, _)| dest[index])
.collect();
if points.is_empty() {
@ -296,15 +296,15 @@ impl TransferRegion {
// later
Region::Rect(s1, s2) => {
// Check if all source wells exist:
if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 {
if s1.row == 0 || s1.col == 0 || s2.row == 0 || s2.col == 0 {
return Err("Source region is out-of-bounds! (Too small)");
}
// Sufficient to check if the corners are in-bounds
let source_max = self.source_plate.size();
if s1.0 > source_max.0 || s2.0 > source_max.0 {
if s1.row > source_max.0 || s2.row > source_max.0 {
return Err("Source region is out-of-bounds! (Too tall)");
}
if s1.1 > source_max.1 || s2.1 > source_max.1 {
if s1.col > source_max.1 || s2.col > source_max.1 {
// log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1);
return Err("Source region is out-of-bounds! (Too wide)");
}
@ -325,28 +325,34 @@ impl TransferRegion {
}
}
fn create_dense_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> Vec<(u8, u8)> {
fn create_dense_rectangle(c1: &Well, c2: &Well) -> Vec<Well> {
// Creates a vector of every point between two corners
let (c1, c2) = standardize_rectangle(c1, c2);
let mut points = Vec::<(u8, u8)>::new();
for i in c1.0..=c2.0 {
for j in c1.1..=c2.1 {
points.push((i, j));
let mut points = Vec::<Well>::new();
for i in c1.row..=c2.row {
for j in c1.col..=c2.col {
points.push(Well { row: i, col: j });
}
}
points
}
fn standardize_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> ((u8, u8), (u8, u8)) {
let upper_left_i = u8::min(c1.0, c2.0);
let upper_left_j = u8::min(c1.1, c2.1);
let bottom_right_i = u8::max(c1.0, c2.0);
let bottom_right_j = u8::max(c1.1, c2.1);
fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
let upper_left_i = u8::min(c1.row, c2.row);
let upper_left_j = u8::min(c1.col, c2.col);
let bottom_right_i = u8::max(c1.row, c2.row);
let bottom_right_j = u8::max(c1.col, c2.col);
(
(upper_left_i, upper_left_j),
(bottom_right_i, bottom_right_j),
Well {
row: upper_left_i,
col: upper_left_j,
},
Well {
row: bottom_right_i,
col: bottom_right_j,
},
)
}
@ -363,7 +369,7 @@ impl fmt::Display for TransferRegion {
let mut source_string = String::new();
for i in 1..=source_dims.0 {
for j in 1..=source_dims.1 {
if source_wells.contains(&(i, j)) {
if source_wells.contains(&Well { row: i, col: j }) {
source_string.push('x')
} else {
source_string.push('.')
@ -379,7 +385,7 @@ impl fmt::Display for TransferRegion {
let mut dest_string = String::new();
for i in 1..=dest_dims.0 {
for j in 1..=dest_dims.1 {
if dest_wells.contains(&(i, j)) {
if dest_wells.contains(&Well { row: i, col: j }) {
dest_string.push('x')
} else {
dest_string.push('.')
@ -393,134 +399,130 @@ impl fmt::Display for TransferRegion {
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use crate::data::plate::*;
use crate::data::transfer_region::*;
use crate::plate::*;
use crate::transfer_region::*;
#[test]
#[wasm_bindgen_test]
fn test_simple_transfer() {
let source = Plate::new(PlateType::Source, PlateFormat::W96);
let destination = Plate::new(PlateType::Destination, PlateFormat::W384);
let transfer1 = TransferRegion {
source_plate: source,
source_region: Region::Rect((1, 1), (3, 3)),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well { row: 1, col: 1 }),
dest_plate: destination,
dest_region: Region::Point((3, 3)),
dest_region: Region::Point(Well {row: 3, col: 3}),
interleave_source: (1, 1),
interleave_dest: (1, 1),
};
let transfer1_map = transfer1.calculate_map();
assert_eq!(
transfer1_map((1, 1)),
Some(vec! {(3,3)}),
transfer1_map(Well {row: 1, col: 1}),
Some(vec! {Well {row: 3, col: 3}}),
"Failed basic shift transfer 1"
);
assert_eq!(
transfer1_map((1, 2)),
Some(vec! {(3,4)}),
transfer1_map(Well{ row: 1, col: 2}),
Some(vec! {Well{ row: 3, col: 4}}),
"Failed basic shift transfer 2"
);
assert_eq!(
transfer1_map((2, 2)),
Some(vec! {(4,4)}),
transfer1_map(Well{ row: 2, col: 2}),
Some(vec! {Well{ row: 4, col: 4}}),
"Failed basic shift transfer 3"
);
let transfer2 = TransferRegion {
source_plate: source,
source_region: Region::Rect((1, 1), (3, 3)),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}),
dest_plate: destination,
dest_region: Region::Point((3, 3)),
dest_region: Region::Point(Well{ row: 3, col: 3}),
interleave_source: (2, 2),
interleave_dest: (1, 1),
};
let transfer2_map = transfer2.calculate_map();
assert_eq!(
transfer2_map((1, 1)),
Some(vec! {(3,3)}),
transfer2_map(Well{ row: 1, col: 1}),
Some(vec! {Well{ row: 3, col: 3}}),
"Failed source interleave, type simple 1"
);
assert_eq!(
transfer2_map((1, 2)),
transfer2_map(Well{ row: 1, col: 2}),
None,
"Failed source interleave, type simple 2"
);
assert_eq!(
transfer2_map((2, 2)),
transfer2_map(Well{ row: 2, col: 2}),
None,
"Failed source interleave, type simple 3"
);
assert_eq!(
transfer2_map((3, 3)),
Some(vec! {(4,4)}),
transfer2_map(Well{ row: 3, col: 3}),
Some(vec! {Well{ row: 4, col: 4}}),
"Failed source interleave, type simple 4"
);
let transfer3 = TransferRegion {
source_plate: source,
source_region: Region::Rect((1, 1), (3, 3)),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}),
dest_plate: destination,
dest_region: Region::Point((3, 3)),
dest_region: Region::Point(Well{ row: 3, col: 3}),
interleave_source: (1, 1),
interleave_dest: (2, 3),
};
let transfer3_map = transfer3.calculate_map();
assert_eq!(
transfer3_map((1, 1)),
Some(vec! {(3,3)}),
transfer3_map(Well{ row: 1, col: 1}),
Some(vec! {Well{ row: 3, col: 3}}),
"Failed destination interleave, type simple 1"
);
assert_eq!(
transfer3_map((2, 1)),
Some(vec! {(5,3)}),
transfer3_map(Well{ row: 2, col: 1}),
Some(vec! {Well{ row: 5, col: 3}}),
"Failed destination interleave, type simple 2"
);
assert_eq!(
transfer3_map((1, 2)),
Some(vec! {(3,6)}),
transfer3_map(Well{ row: 1, col: 2}),
Some(vec! {Well{ row: 3, col: 6}}),
"Failed destination interleave, type simple 3"
);
assert_eq!(
transfer3_map((2, 2)),
Some(vec! {(5,6)}),
transfer3_map(Well{ row: 2, col: 2}),
Some(vec! {Well{ row: 5, col: 6}}),
"Failed destination interleave, type simple 4"
);
}
#[test]
#[wasm_bindgen_test]
fn test_replicate_transfer() {
let source = Plate::new(PlateType::Source, PlateFormat::W96);
let destination = Plate::new(PlateType::Destination, PlateFormat::W384);
let transfer1 = TransferRegion {
source_plate: source,
source_region: Region::Rect((1, 1), (2, 2)),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 2}),
dest_plate: destination,
dest_region: Region::Rect((2, 2), (11, 11)),
dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{row:11, col:11}),
interleave_source: (1, 1),
interleave_dest: (3, 3),
};
let transfer1_map = transfer1.calculate_map();
assert_eq!(
transfer1_map((1, 1)),
Some(vec! {(2, 2), (2, 8), (8, 2), (8, 8)}),
transfer1_map(Well{ row: 1, col: 1}),
Some(vec! {Well{ row: 2, col: 2}, Well{ row: 2, col: 8}, Well{ row: 8, col: 2}, Well{ row: 8, col: 8}}),
"Failed type replicate 1"
);
assert_eq!(
transfer1_map((2, 1)),
Some(vec! {(5, 2), (5, 8), (11, 2), (11, 8)}),
transfer1_map(Well{ row: 2, col: 1}),
Some(vec! {Well{ row: 5, col: 2}, Well{ row: 5, col: 8}, Well{ row: 11, col: 2}, Well{ row: 11, col: 8}}),
"Failed type replicate 1"
);
let transfer2 = TransferRegion {
source_plate: Plate::new(PlateType::Source, PlateFormat::W384),
dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384),
source_region: Region::Rect((1, 1), (2, 3)),
dest_region: Region::Rect((2, 2), (11, 16)),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 3}),
dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{ row: 11, col: 16}),
interleave_source: (1, 1),
interleave_dest: (2, 2),
};
@ -528,71 +530,69 @@ mod tests {
let transfer2_dest = transfer2.get_destination_wells();
assert_eq!(
transfer2_source,
vec![(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)],
vec![Well{ row: 1, col: 1}, Well{ row: 1, col: 2}, Well{ row: 1, col: 3}, Well{ row: 2, col: 1}, Well{ row: 2, col: 2}, Well{ row: 2, col: 3}],
"Failed type replicate 2 source"
);
assert_eq!(
transfer2_dest,
vec![
(2, 2),
(2, 8),
(6, 2),
(6, 8),
(2, 4),
(2, 10),
(6, 4),
(6, 10),
(2, 6),
(2, 12),
(6, 6),
(6, 12),
(4, 2),
(4, 8),
(8, 2),
(8, 8),
(4, 4),
(4, 10),
(8, 4),
(8, 10),
(4, 6),
(4, 12),
(8, 6),
(8, 12)
Well{ row: 2, col: 2},
Well{ row: 2, col: 8},
Well{ row: 6, col: 2},
Well{ row: 6, col: 8},
Well{ row: 2, col: 4},
Well{ row: 2, col: 10},
Well{ row: 6, col: 4},
Well{ row: 6, col: 10},
Well{ row: 2, col: 6},
Well{ row: 2, col: 12},
Well{ row: 6, col: 6},
Well{ row: 6, col: 12},
Well{ row: 4, col: 2},
Well{ row: 4, col: 8},
Well{ row: 8, col: 2},
Well{ row: 8, col: 8},
Well{ row: 4, col: 4},
Well{ row: 4, col: 10},
Well{ row: 8, col: 4},
Well{ row: 8, col: 10},
Well{ row: 4, col: 6},
Well{ row: 4, col: 12},
Well{ row: 8, col: 6},
Well{ row: 8, col: 12}
],
"Failed type replicate 2 destination"
);
}
#[test]
#[wasm_bindgen_test]
fn test_pooling_transfer() {
use std::collections::HashSet;
let transfer1 = TransferRegion {
source_plate: Plate::new(PlateType::Source, PlateFormat::W384),
dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384),
source_region: Region::Rect((1, 4), (3, 7)),
dest_region: Region::Point((1, 9)),
source_region: Region::Rect(Well{ row: 1, col: 4}, Well{ row: 3, col: 7}),
dest_region: Region::Point(Well{ row: 1, col: 9}),
interleave_source: (1, 1),
interleave_dest: (0, 2),
};
//let transfer1_source = transfer1.get_source_wells();
let mut transfer1_dest = transfer1.get_destination_wells();
transfer1_dest.sort();
transfer1_dest.dedup(); // Makes our check easier, otherwise we have repeated wells
let transfer1_dest: HashSet<Well> = transfer1.get_destination_wells().into_iter().collect();
let transfer1_map = transfer1.calculate_map();
// Skipping source check---it's just 12 wells.
assert_eq!(
transfer1_dest,
vec![(1, 9), (1, 11), (1, 13), (1, 15)],
vec![Well{ row: 1, col: 9}, Well{ row: 1, col: 11}, Well{ row: 1, col: 13}, Well{ row: 1, col: 15}].into_iter().collect(),
"Failed type pool 1 dest"
);
assert_eq!(
transfer1_map((2, 6)),
Some(vec![(1, 13)]),
transfer1_map(Well{ row: 2, col: 6}),
Some(vec![Well{ row: 1, col: 13}]),
"Failed type pool 1 map 1"
);
assert_eq!(
transfer1_map((3, 7)),
Some(vec![(1, 15)]),
transfer1_map(Well{ row: 3, col: 7}),
Some(vec![Well{ row: 1, col: 15}]),
"Failed type pool 1 map 2"
);
}

View File

@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::Well;
type WellPair = (Well, Well);
#[non_exhaustive]
#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub enum TransferVolume {
Single(f32),
WellMap(VolumeMaps),
}
#[serde_with::serde_as]
#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub struct VolumeMaps {
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub source_only: HashMap<Well, f32>,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub destination_only: HashMap<Well, f32>,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub paired: HashMap<WellPair, f32>,
}
impl Default for TransferVolume {
fn default() -> Self {
TransferVolume::Single(2.5f32)
}
}

View File

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Debug, Hash)]
pub struct Well {
/// Top to bottom assuming landscape, i.e. lettered
pub row: u8,
/// Left to right assuming landscape, i.e. numbered
pub col: u8,
}

View File

@ -1,6 +1,6 @@
[package]
name = "plate-tool-web"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -0,0 +1,27 @@
@use "sass:color";
@use "./variables" as *;
$selection-border-width: 2px;
@mixin standard_button {
color: inherit;
display: inline;
margin-left: 3px;
border: 2px solid rgba(1,1,1,0.2);
background: transparent;
user-select: none;
list-style: none;
line-height: 1em;
&::before {
text-align: center;
vertical-align: middle;
}
&:hover {
background: color.change($color-light, $alpha: 0.08);
transition: background 0.1s;
border: $selection-border-width solid color.change($color-light, $alpha:0.3);
}
}

View File

@ -2,6 +2,8 @@
@use "../variables" as *;
div.plate_container {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-evenly;
@ -38,4 +40,22 @@ div.plate_container {
grid-row: 2;
}
}
div.plate_container--heatmap-notice {
position: absolute;
top: 0.5em;
left: 0.5em;
animation: 1s 1 attention_on_load;
}
}
@keyframes attention_on_load {
from{
color: $color-light;
transform: scale(1.05);
}
to {
color: inherit;
}
}

View File

@ -1,5 +1,6 @@
@use "sass:color";
@use "../variables" as *;
@use "../button" as *;
div.transfer_menu {
position: relative;
@ -40,6 +41,11 @@ div.transfer_menu {
}
}
div.transfer_menu input[type="button"] {
@include standard_button;
}
input {
text-align: center;
margin-left: 0.5em;
@ -50,10 +56,10 @@ input {
padding: 0;
&[type="text"] {
width: 4em;
width: 6em;
}
&[name="name"] {
width: 6em; // Override above
width: calc(100% - 6em); // Override above
}
&[type="number"] {
width: 2em;

View File

@ -1,5 +1,6 @@
@use "sass:color";
@use "../variables" as *;
@use "../button" as *;
$selection-border-width: 2px;
@ -53,3 +54,17 @@ div.tree li {
background: color.change($color-light, $alpha: 0.2);
}
}
div.tree--header {
margin-top: 1%;
display: flex;
align-items: center;
}
div.tree--header button {
@include standard_button;
&::before {
content: "Add";
}
}

View File

@ -12,6 +12,7 @@ use crate::components::states::MainState;
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::{Region, TransferRegion};
use plate_tool_lib::Well;
use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord};
@ -238,7 +239,7 @@ pub fn import_transfer_csv_submit_callback(
let from_dest = from_dest.value();
let to_dest = to_dest.value();
let records: Vec<((u8, u8), (u8, u8))> = records
let records: Vec<(Well, Well)> = records
.iter()
.filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest)
@ -295,11 +296,11 @@ fn auto_callback(
Closure::<dyn FnMut(_)>::new(move |_| {
let res = auto(&records);
main_dispatch.reduce_mut(|state| {
state.source_plates.extend(res.sources.into_iter());
state.source_plates.extend(res.sources);
state
.destination_plates
.extend(res.destinations.into_iter());
state.transfers.extend(res.transfers.into_iter());
.extend(res.destinations);
state.transfers.extend(res.transfers);
});
})
}

View File

@ -9,8 +9,6 @@ use crate::components::states::MainState;
use super::create_close_button;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,

View File

@ -1,35 +1,27 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::state_to_csv;
use crate::components::{new_plate_dialog::{NewPlateDialogType}, states::{CurrentTransfer, MainState}};
type NoParamsCallback = Box<dyn Fn(())>;
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(false);
new_plate_dialog_is_open.set(None);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback {
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>
) -> Box<dyn Fn(NewPlateDialogType)> {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(true);
Box::new(move |dt| {
new_plate_dialog_is_open.set(Some(dt));
})
}

View File

@ -1,10 +1,9 @@
#![allow(non_snake_case)]
use js_sys::Function;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
FileReader, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement,
HtmlDialogElement, HtmlFormElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
@ -14,8 +13,6 @@ use crate::components::states::{CsvExportType, MainState};
use super::create_close_button;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {

View File

@ -4,8 +4,6 @@ use wasm_bindgen::{prelude::*, JsCast};
use web_sys::HtmlElement;
use yew::prelude::*;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap();
let close_button = document

View File

@ -6,7 +6,7 @@ use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::transfer_menu::RegionDisplay;
use plate_tool_lib::{transfer::Transfer, transfer_region::Region};
use plate_tool_lib::{transfer::Transfer, transfer_region::Region, transfer_volume::TransferVolume};
use crate::components::states::{CurrentTransfer, MainState};
@ -142,7 +142,7 @@ pub fn on_volume_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Call
.expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.volume = num;
state.transfer.volume = TransferVolume::Single(num);
});
}
})
@ -184,12 +184,18 @@ pub fn save_transfer_button_callback_callback(
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{
let new_transfer = Transfer::new(
// Only mutable for volume assignment!
let mut new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region.clone(),
ct_state.transfer.name.clone(),
);
// This clone is cheap - we know this is just a single value
// so cloning f32 will be fast.
new_transfer.volume = ct_state.transfer.volume.clone();
main_dispatch.reduce_mut(|state| {
state.transfers.push(new_transfer);
state.selected_transfer = state

View File

@ -1,8 +1,8 @@
use plate_tool_lib::{plate::PlateType, plate_instances::PlateInstance};
use plate_tool_lib::{plate::PlateType};
use uuid::Uuid;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, HtmlOptionElement,
HtmlDialogElement, HtmlElement, HtmlOptionElement,
HtmlSelectElement,
};
@ -203,7 +203,7 @@ fn find_plate_type(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<Plate
.iter()
.any(|x| x.get_uuid() == id)
{
return Some(PlateType::Source);
Some(PlateType::Source)
} else if main_dispatch
.get()
.destination_plates

View File

@ -2,7 +2,7 @@
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::new_plate_dialog::NewPlateDialog;
use crate::components::new_plate_dialog::{NewPlateDialog, NewPlateDialogType};
use crate::components::plates::plate_container::PlateContainer;
use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::TransferMenu;
@ -53,7 +53,7 @@ pub fn MainWindow() -> Html {
main_window_callbacks::change_csv_export_type_callback(main_dispatch)
};
let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>> = use_state_eq(|| None);
let new_plate_dialog_callback =
main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone());
@ -124,8 +124,8 @@ pub fn MainWindow() -> Html {
<TransferMenu />
<PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/>
if {*new_plate_dialog_is_open} {
<NewPlateDialog close_callback={new_plate_dialog_callback}/>
if {new_plate_dialog_is_open.is_some()} {
<NewPlateDialog close_callback={new_plate_dialog_callback} dialog_type={new_plate_dialog_is_open.unwrap()}/>
}
</div>
</>

View File

@ -10,6 +10,16 @@ use crate::components::callbacks::new_plate_dialog_callbacks;
#[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps {
pub close_callback: Callback<()>,
pub dialog_type: NewPlateDialogType,
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum NewPlateDialogType {
SourceOnly,
DestinationOnly,
#[allow(dead_code)]
Both, // Retained old functionality
}
#[function_component]
@ -43,9 +53,15 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
);
}
let header_interior_text = match props.dialog_type {
NewPlateDialogType::Both => "",
NewPlateDialogType::SourceOnly => " source",
NewPlateDialogType::DestinationOnly => " destination",
};
html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{"Create a plate:"}</h2>
<h2>{format!("Create a{} plate:", header_interior_text)}</h2>
<form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format">
@ -58,13 +74,35 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
<option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option>
</select>
<input type="radio" name="new_plate_type" id="npt_src" value="src" />
<label for="npt_src">{"Source"}</label>
<input type="radio" name="new_plate_type" id="npt_dest" value="dest" />
<label for="npt_dest">{"Destination"}</label>
{ plate_type_selector(props.dialog_type) }
<input type="submit" name="new_plate_button" value="Create" />
</form>
<form class="modal_close" method="dialog"><button /></form>
</dialog>
}
}
fn plate_type_selector(t: NewPlateDialogType) -> Html {
match t {
NewPlateDialogType::Both => {
html! {
<>
<input type="radio" name="new_plate_type" id="npt_src" value="src" />
<label for="npt_src">{"Source"}</label>
<input type="radio" name="new_plate_type" id="npt_dest" value="dest" />
<label for="npt_dest">{"Destination"}</label>
</>
}
},
NewPlateDialogType::SourceOnly => {
html! {
<input type="hidden" name="new_plate_type" id="npt_src" value="src" />
}
},
NewPlateDialogType::DestinationOnly => {
html! {
<input type="hidden" name="new_plate_type" id="npt_dest" value="dest" />
}
}
}
}

View File

@ -1,6 +1,9 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use plate_tool_lib::transfer_volume::TransferVolume;
use yew::prelude::*;
use yewdux::prelude::*;
@ -8,6 +11,7 @@ use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::plate::PlateType;
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::Well;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
@ -32,24 +36,24 @@ pub fn Plate(props: &PlateProps) -> Html {
PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(),
};
let (pt1, pt2) = match region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Point(Well {row: x, col: y }) => ((x, y), (x, y)),
Region::Rect(c1, c2) => ((c1.row, c1.col ), (c2.row, c2.col)),
Region::Custom(_) => ((0, 0), (0, 0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let tooltip_map: HashMap<(u8, u8), Vec<&Transfer>>;
let volume_map: HashMap<(u8, u8), f32>;
let tooltip_map: HashMap<Well, Vec<&Transfer>>;
let volume_map: HashMap<Well, f32>;
let volume_max: f32;
{
let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_plate.get_uuid(),
PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(),
});
let mut tooltip_map_temp: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
let mut volume_map_temp: HashMap<(u8,u8), f32> = HashMap::new();
let mut tooltip_map_temp: HashMap<Well, Vec<&Transfer>> = HashMap::new();
let mut volume_map_temp: HashMap<Well, f32> = HashMap::new();
let mut volume_max_temp: f32 = f32::NEG_INFINITY;
for transfer in transfers {
let wells = match props.ptype {
@ -62,12 +66,27 @@ pub fn Plate(props: &PlateProps) -> Html {
} else {
tooltip_map_temp.insert(well, vec![transfer]);
}
let temp_volume: f32 = match &transfer.volume {
TransferVolume::Single(x) => *x,
TransferVolume::WellMap(wm) => {
*match props.ptype {
PlateType::Source => wm.source_only.get(&well),
PlateType::Destination => wm.destination_only.get(&well)
}.unwrap_or(&2.5f32)
},
_ => unreachable!(),
};
// Usage to be used as a volume multiplier; should be in [1, U32_MAX]
// First convert to f64 (u32 can not fit in f32 directly) and then cast
// to round into the closest f32.
let usage: f32 = Into::<f64>::into(count_plate_usage(transfer, &well).unwrap_or(1u32).max(1u32)) as f32;
if let Some(val) = volume_map_temp.get_mut(&well) {
*val += transfer.volume;
*val += temp_volume * usage;
} else {
volume_map_temp.insert(well, transfer.volume);
volume_map_temp.insert(well, temp_volume * usage);
}
volume_max_temp = f32::max(volume_max_temp, transfer.volume);
volume_max_temp = f32::max(volume_max_temp, *volume_map_temp.get(&well).expect("Just added"));
}
}
tooltip_map = tooltip_map_temp;
@ -135,22 +154,22 @@ pub fn Plate(props: &PlateProps) -> Html {
.map(|j| {
let color = {
if !main_state.preferences.volume_heatmap {
tooltip_map.get(&(i,j))
tooltip_map.get(&Well { row: i, col: j })
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} else {
volume_map.get(&(i,j))
volume_map.get(&Well { row: i, col: j })
.map(|t| PALETTE.get_linear(*t as f64, volume_max as f64))
}
};
let title = {
let mut out = String::new();
let used_by = tooltip_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
let used_by = tooltip_map.get(&Well { row: i, col: j }).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")));
if let Some(val) = used_by {
out += &val;
}
let volume_sum = volume_map.get(&(i,j))
let volume_sum = volume_map.get(&Well { row: i, col: j })
.map(|t| format!("Volume: {}", t));
if let Some(val) = volume_sum {
if !out.is_empty() { out += "\n" }
@ -162,7 +181,7 @@ pub fn Plate(props: &PlateProps) -> Html {
<PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
in_transfer={wells.contains(&Well { row: i, col: j }) && main_state.preferences.in_transfer_hashes}
color={ color }
cell_height={props.cell_height}
title={title}
@ -256,6 +275,15 @@ pub fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8
}
}
fn count_plate_usage(transfer: &Transfer, well: &Well) -> Option<u32> {
if let Region::Custom(_) = transfer.transfer_region.source_region {
return None;
}
let map = transfer.transfer_region.calculate_map();
map(*well).map(|list| list.len() as u32)
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;

View File

@ -41,7 +41,7 @@ pub fn mouseup_callback(
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
if let Ok(rd) = RegionDisplay::try_from((ul.1, ul.0, br.1, br.0)) {
ct_dispatch.reduce_mut(|state| {
match ptype {
PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd),

View File

@ -5,10 +5,12 @@ use yew::prelude::*;
use plate_tool_lib::plate::PlateType;
use plate_tool_lib::plate_instances::PlateInstance;
use yewdux::functional::use_store;
// use super::destination_plate::DestinationPlate;
// use super::source_plate::SourcePlate;
use super::plate::Plate;
use crate::components::states::MainState;
#[derive(Properties, PartialEq)]
pub struct PlateContainerProps {
@ -18,6 +20,8 @@ pub struct PlateContainerProps {
#[function_component]
pub fn PlateContainer(props: &PlateContainerProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let cell_height = {
let height = web_sys::window()
.unwrap()
@ -49,8 +53,15 @@ pub fn PlateContainer(props: &PlateContainerProps) -> Html {
.set_onresize(Some(onresize.as_ref().unchecked_ref()));
onresize.forget(); // Magic!
let heatmap_enabled = main_state.preferences.volume_heatmap;
html! {
<div class="plate_container">
if heatmap_enabled {
<div class="plate_container--heatmap-notice" >
<h3>{"Volume Heatmap Enabled"}</h3>
</div>
}
if let Some(spi) = props.source_dims.clone() {
if let Some(dpi) = props.destination_dims.clone() {
<div class="plate_container--source">

View File

@ -26,6 +26,7 @@ impl ColorPalette {
]
}
#[allow(dead_code)] // Preserve old implementation for reference
fn get_u8(&self, t: u8) -> [f64; 3] {
assert!(t > 0, "t must be greater than zero!");
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)

View File

@ -1,6 +1,7 @@
#![allow(non_snake_case)]
use lazy_static::lazy_static;
use plate_tool_lib::Well;
use regex::Regex;
use serde::{Deserialize, Serialize};
use yew::prelude::*;
@ -8,6 +9,7 @@ use yewdux::prelude::*;
use crate::components::callbacks::transfer_menu_callbacks;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::transfer_volume::TransferVolume;
use plate_tool_lib::util::{letters_to_num, num_to_letters};
use super::states::{CurrentTransfer, MainState};
@ -143,7 +145,10 @@ pub fn TransferMenu() -> Html {
<input type="number" name="volume" class="volume_input"
min="0" step="0.1"
onchange={on_volume_change}
value={ct_state.transfer.volume.to_string()}/>
value={match ct_state.transfer.volume {
TransferVolume::Single(x) => x.to_string(),
_ => unreachable!(),
}}/>
</div>
}
<div id="controls">
@ -233,10 +238,10 @@ impl TryFrom<&str> for RegionDisplay {
impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self {
match *value {
Region::Point((col, row)) => {
Region::Point(Well { row, col }) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
}
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.row, c1.col, c2.row, c2.col))
.ok()
.unwrap(),
Region::Custom(_) => RegionDisplay {
@ -252,11 +257,11 @@ impl From<&Region> for RegionDisplay {
impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point((value.col_start, value.row_start))
Region::Point(Well { row: value.row_start, col: value.col_start })
} else {
Region::Rect(
(value.col_start, value.row_start),
(value.col_end, value.row_end),
Well { row: value.row_start, col: value.col_start },
Well { row: value.row_end, col: value.col_end },
)
}
}

View File

@ -2,17 +2,18 @@
use plate_tool_lib::plate::PlateFormat;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{HtmlDialogElement, HtmlInputElement};
use web_sys::{HtmlDialogElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::callbacks::tree_callbacks;
use crate::components::callbacks::{tree_callbacks};
use crate::components::new_plate_dialog::NewPlateDialogType;
use crate::components::states::{CurrentTransfer, MainState};
#[derive(PartialEq, Properties)]
pub struct TreeProps {
pub open_new_plate_callback: Callback<()>,
pub open_new_plate_callback: Callback<NewPlateDialogType>,
}
#[function_component]
@ -98,13 +99,25 @@ pub fn Tree(props: &TreeProps) -> Html {
html! {
<div class="tree">
<div id="source-plates">
<div class="tree--header">
<h3>{"Source Plates:"}</h3>
<button onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(NewPlateDialogType::SourceOnly)}
} />
</div>
<ul>
{source_plates}
</ul>
</div>
<div id="destination-plates">
<div class="tree--header">
<h3>{"Destination Plates:"}</h3>
<button onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(NewPlateDialogType::DestinationOnly)}
} />
</div>
<ul>
{dest_plates}
</ul>
@ -120,14 +133,6 @@ pub fn Tree(props: &TreeProps) -> Html {
delete_button_callback={plate_info_delete_callback}/>
}
<div id="controls">
<button type="button"
onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(())}
}>
{"New Plate"}</button>
</div>
</div>
}
}
@ -159,19 +164,17 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
None => "Not Found".to_string(),
};
let plate_format = plate.map(|p| p.plate.plate_format);
let plate_formats = vec![
PlateFormat::W6,
let plate_formats = [PlateFormat::W6,
PlateFormat::W12,
PlateFormat::W24,
PlateFormat::W48,
PlateFormat::W96,
PlateFormat::W384,
PlateFormat::W1536,
PlateFormat::W3456,
];
PlateFormat::W3456];
let plate_format_options = plate_formats.iter().map(|v| {
let selected = Some(v) == plate_format.as_ref();
html!{
html! {
<option value={v.to_string()} selected={selected}>{ v.to_string() }</option>
}
});

View File

@ -25,9 +25,9 @@ pub fn plate_test() {
let transfer = transfer_region::TransferRegion {
source_plate: source,
source_region: transfer_region::Region::Rect((1, 1), (2, 2)),
source_region: transfer_region::Region::Rect(Well { row: 1, col: 1 }, Well { row: 2, col: 2 }),
dest_plate: destination,
dest_region: transfer_region::Region::Rect((2, 2), (11, 11)),
dest_region: transfer_region::Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 11 }),
interleave_source: (1, 1),
interleave_dest: (3, 3),
};