treizh's picture
Fixed error when attempting to draw colors
baa9a32 verified
import io
import math
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from matplotlib.patches import Circle
import matplotlib as mpl
from pypdf import PdfWriter
import panel as pn
# MARK: Functions
class PANES:
TEMPLATE = "Template"
RAW_DATA = "Experiment raw data"
READY_DATA = "Experiment data"
PLATE_VIEWER = "Plate viewer"
PAGE_VIEWER = "Page viewer"
VERTICAL_PADDING = 4
CFO_SHOW_COORDINATES = "Show coordinates"
CFO_SHOW_BOLD_FONT = "Bold font"
def expand_position(row, col, discs_per_leaf):
min_row = (row - 1) * discs_per_leaf + 1
max_row = min_row + discs_per_leaf
return (min_row, max_row), col
def is_valid_forbibden_position(fp):
return fp is not None and fp[0] > 0 and fp[1] > 0
def is_forbibden_position(fp, r, c):
return is_valid_forbibden_position(fp) and r == fp[0] and c == fp[1]
def get_next_position(
tray, row, col, row_count: int, col_count: int, forbidden_position: tuple | None
):
if col == 0 and row == 0:
col = 1
row = 1
if is_forbibden_position(forbidden_position, row, col):
return get_next_position(
tray,
col,
row,
row_count=row_count,
col_count=col_count,
forbidden_position=forbidden_position,
)
return tray, row, col
if col < col_count:
col += 1
else:
col = 1
if row < row_count:
row += 1
else:
row = 1
tray += 1
if is_forbibden_position(forbidden_position, row, col):
return get_next_position(
tray,
col,
row,
row_count=row_count,
col_count=col_count,
forbidden_position=forbidden_position,
)
return tray, row, col
def draw_tray(
forbidden_position: tuple | None,
row_count: int,
col_count: int,
discs_per_leaf: int,
tray_size: tuple,
row: int = 0,
col: int = 0,
text_data: np.ndarray | None = None,
color_data: np.ndarray | None = None,
fig: Figure | None = None,
fontsize=14,
plaque_index: str | None = None,
print_coordinates: bool = False,
print_index: bool = False,
coord_bf: bool = False,
coord_font_size: int = 10,
leaf_size: int | None = None,
):
t = np.zeros((row_count * discs_per_leaf, col_count))
if is_valid_forbibden_position(forbidden_position):
t[
forbidden_position[0] - 1 : forbidden_position[0] - 1 + discs_per_leaf,
forbidden_position[1] - 1,
] = 1
if col > 0 and row > 0:
(min_row, max_row), col = expand_position(row, col, discs_per_leaf)
t[min_row - 1 : max_row - 1, col - 1] = 2
had_fig = fig is not None
if fig is None:
fig = Figure(figsize=tuple(i / 2.54 for i in tray_size))
axii = fig.subplots(nrows=t.shape[0], ncols=t.shape[1])
left, width = 0, 1
bottom, height = 0, 1
right = left + width
top = bottom + height
if color_data is not None and color_data.size > 0:
cmap = mpl.colormaps["cool"]
colors = cmap(np.linspace(0, 1, np.nanmax(color_data).astype(int) + 1))
def is_valid(a, i, j):
return a is not None and 0 <= i < a.shape[0] and 0 <= j < a.shape[1]
leaf_index = 0
for i, line in enumerate(axii):
for j, ax in enumerate(line):
if t[i, j] != 1:
leaf_index += 1
# Draw background
if i == 0 and j == 0 and plaque_index is not None and t[i, j] == 1:
ax.set_facecolor("xkcd:light grey")
else:
if t[i, j] == 1:
ax.set_facecolor("xkcd:black")
elif color_data is None:
match t[i, j]:
case 0:
ax.set_facecolor("xkcd:white")
case 2:
ax.set_facecolor("xkcd:salmon")
elif is_valid(color_data, i, j) and np.isnan(color_data[i, j]) != True:
ax.set_facecolor(colors[int(color_data[i, j].astype(int))])
# Draw leafs
if leaf_size is not None and leaf_size > 0 and t[i, j] != 1:
ax.add_patch(
Circle(
xy=(0.5, 0.5),
radius=leaf_size / 2,
edgecolor="green",
facecolor="lime",
linewidth=1,
)
)
# Write text
if print_coordinates is True:
ax.text(
left + 0.02,
top - 0.02,
f"{chr(65 + i)}{j+1}",
dict(size=coord_font_size),
horizontalalignment="left",
verticalalignment="top",
fontweight="bold" if coord_bf is True else "normal",
)
ax.text(
right - 0.03,
bottom + 0.05,
f"{chr(65 + i)}{j+1}",
dict(size=coord_font_size),
horizontalalignment="right",
verticalalignment="bottom",
rotation=180,
transform_rotates_text=True,
fontweight="bold" if coord_bf is True else "normal",
)
if print_index is True:
ax.text(
0.5,
0.5,
str(leaf_index),
dict(size=fontsize),
horizontalalignment="center",
verticalalignment="center",
transform=ax.transAxes,
)
if i == 0 and j == 0 and plaque_index is not None and t[i, j] == 1:
ax.set_facecolor("xkcd:light grey")
ax.text(
0.5,
0.5,
plaque_index,
dict(size=fontsize),
horizontalalignment="center",
verticalalignment="center",
)
if (
text_data is not None
and is_valid(text_data, i, j)
and isinstance(text_data[i, j], str) is True
):
ax.text(
0.5,
0.5,
text_data[i, j],
dict(size=fontsize),
horizontalalignment="center",
verticalalignment="center",
transform=ax.transAxes,
)
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_xticks([])
ax.set_yticks([])
if had_fig is False:
fig.tight_layout(pad=0.2)
return fig
def draw_page(
data: pd.DataFrame,
figs_per_col: int,
figs_per_row: int,
forbibden_position: tuple,
row_count: int,
col_count: int,
discs_per_leaf: int,
orientation: str,
font_size: int,
page_index: int,
):
main_fig = Figure(
figsize=(21, 29.7) if orientation == "portrait" else (29.7, 21),
layout="constrained",
)
subfigs = main_fig.subfigures(figs_per_row, figs_per_col, wspace=0.07)
start_tray = (figs_per_col * figs_per_row * (page_index - 1)) + 1
for i in range(figs_per_col * figs_per_row):
if i >= data.plaque.max():
break
current_fig = subfigs.flatten()[i]
draw_tray(
forbidden_position=(
None
if forbibden_position is None
else (forbibden_position[0], forbibden_position[1])
),
row_count=row_count,
col_count=col_count,
discs_per_leaf=discs_per_leaf,
tray_size=(row_count * discs_per_leaf, col_count),
text_data=data[data.plaque == i + start_tray]
.pivot(index="ligne", columns="colonne", values="Num_Plvt")
.values,
color_data=data[data.plaque == i + start_tray]
.pivot(index="ligne", columns="colonne", values="group")
.values,
fontsize=font_size,
fig=current_fig,
plaque_index=f"P{i+start_tray}",
)
if is_valid_forbibden_position(forbibden_position) is False:
current_fig.suptitle(f"P{i+start_tray}", fontsize=font_size)
if "experience" in data:
main_fig.suptitle(
f"{data.iloc[0].experience}, page {page_index}", fontsize=font_size * 1.5
)
return main_fig
def prepare_experiment(
raw_data: pd.DataFrame,
row_count: int,
col_count: int,
discs_per_leaf: int,
forbidden_position: tuple,
control_blocks: int = 3,
):
ctr_data = raw_data[raw_data.TYPE == "THD"].reset_index(drop=True)
gen_data = raw_data[raw_data.TYPE == "G"].reset_index(drop=True)
match control_blocks:
case 0:
all_data = gen_data
case 1:
all_data = pd.concat(
[
ctr_data.sample(n=len(ctr_data), random_state=0).assign(
group=lambda x: 1
),
gen_data,
]
)
case _:
len_chunks = math.ceil(len(gen_data) / (control_blocks - 1))
list_chunks = [
gen_data[i : i + len_chunks]
for i in range(0, len(gen_data), len_chunks)
]
all_data = ctr_data.sample(n=len(ctr_data), random_state=0).assign(
group=lambda x: 1
)
for i, chunk in enumerate(list_chunks):
all_data = pd.concat(
[
all_data,
chunk,
ctr_data.sample(n=len(ctr_data), random_state=i).assign(
group=lambda x: i + 2
),
]
)
tray, row, col = 1, 0, 0
out = []
for _, r in all_data.reset_index(drop=True).iterrows():
tray, row, col = get_next_position(
tray=tray,
row=row,
col=col,
row_count=row_count,
col_count=col_count,
forbidden_position=forbidden_position,
)
(min_row, max_row), col = expand_position(
row, col, discs_per_leaf=discs_per_leaf
)
for i in range(min_row, max_row):
out.append(
dict(r)
| {
"plaque": tray,
"ligne": i,
"colonne": col,
"alpha_line": chr(i + 64),
}
)
return pd.DataFrame(out)
# MARK: Template
ii_row_count = pn.widgets.IntInput(
name="Row count", start=1, end=100, value=3, sizing_mode="stretch_width"
)
ii_col_count = pn.widgets.IntInput(
name="Column count", start=1, end=100, value=9, sizing_mode="stretch_width"
)
ii_discs_per_leaf = pn.widgets.IntInput(
name="Discs per leaf", start=1, end=100, value=3, sizing_mode="stretch_width"
)
ii_forbibden_row = pn.widgets.IntInput(
name="Row", start=0, end=100, value=1, sizing_mode="stretch_width"
)
ii_forbibden_col = pn.widgets.IntInput(
name="Column", start=0, end=100, value=1, sizing_mode="stretch_width"
)
ii_template_width = pn.widgets.IntInput(
name="Template width (in mm)",
start=10,
end=1000,
value=215,
sizing_mode="stretch_width",
)
ii_template_height = pn.widgets.IntInput(
name="Template height (in mm)",
start=10,
end=1000,
value=215,
sizing_mode="stretch_width",
)
cbg_coordinates = pn.widgets.CheckBoxGroup(
name="Coordiantes options",
options=[CFO_SHOW_COORDINATES, CFO_SHOW_BOLD_FONT],
value=[CFO_SHOW_COORDINATES],
inline=False,
)
ii_coordinate_font_size = pn.widgets.IntInput(
name="Coordinates font size",
start=1,
end=100,
value=11,
sizing_mode="stretch_width",
)
ii_leaf_size = pn.widgets.FloatInput(
name="Leaf size",
start=0,
end=100,
value=0,
step=0.1,
sizing_mode="stretch_width",
)
dwn_template = pn.widgets.FileDownload(
file="",
filename="",
label="Download template",
name="",
# sizing_mode="stretch_width",
icon="file-download",
button_type="primary",
align="end",
)
cd_template = pn.layout.Card(
pn.Column(
pn.Row(ii_row_count, ii_col_count),
ii_discs_per_leaf,
pn.layout.Divider(),
pn.pane.Str("Forbidden position", margin=(0, 10, -10, 2), align="center"),
pn.Row(ii_forbibden_row, ii_forbibden_col),
pn.layout.Divider(),
pn.Row(ii_template_width, ii_template_height),
pn.layout.Divider(),
pn.Row(cbg_coordinates, ii_coordinate_font_size),
ii_leaf_size,
pn.layout.Divider(),
dwn_template,
),
title="Template",
collapsed=True,
margin=(VERTICAL_PADDING, 0, VERTICAL_PADDING, 0),
)
# MARK: Load experiment
csv_separator = pn.widgets.Select(
name="CSV separator", options=[";", ","], sizing_mode="stretch_width"
)
ii_control_blocks = pn.widgets.IntInput(
name="Control blocks", start=0, end=100, value=3, sizing_mode="stretch_width"
)
fi_raw_exp = pn.widgets.FileInput(accept=".csv,.json", sizing_mode="stretch_width")
cd_load_exp = pn.layout.Card(
pn.Column(pn.Row(csv_separator, ii_control_blocks), fi_raw_exp),
title="Load experiment",
collapsed=False,
margin=(VERTICAL_PADDING, 0, VERTICAL_PADDING, 0),
)
# MARK: View plates
ii_plate_number = pn.widgets.IntInput(
name="Plate number",
start=1,
end=1,
value=1,
sizing_mode="stretch_width",
)
ii_font_size = pn.widgets.IntInput(
name="Font size",
start=1,
end=100,
value=14,
sizing_mode="stretch_width",
)
fd_genrate_exp_plates = pn.widgets.FileDownload(
name="", button_type="primary", filename="exp_plates.pdf", align="end"
)
cd_view_plates = pn.layout.Card(
pn.Column(pn.Row(ii_font_size, ii_plate_number), fd_genrate_exp_plates),
title="View plates",
collapsed=False,
margin=(VERTICAL_PADDING, 0, VERTICAL_PADDING, 0),
)
# MARK: Page viewer
ii_figs_per_col = pn.widgets.IntInput(
name="Plates per column", start=1, end=100, value=3, sizing_mode="stretch_width"
)
ii_figs_per_row = pn.widgets.IntInput(
name="Plates per row", start=1, end=100, value=3, sizing_mode="stretch_width"
)
sel_orientation = pn.widgets.Select(
name="Page orientation",
options=["Landscape", "Portrait"],
sizing_mode="stretch_width",
)
ii_page_font_size = pn.widgets.IntInput(
name="Page font size",
start=1,
end=100,
value=14,
sizing_mode="stretch_width",
)
isl_page_index = pn.widgets.IntInput(
name="Page index", start=1, end=100, value=1, sizing_mode="stretch_width"
)
bt_generate_page = pn.widgets.Button(
name="View page", button_type="primary", align="center"
)
fd_genrate_exp_helper = pn.widgets.FileDownload(
name="",
button_type="primary",
filename="exp_helper.pdf",
align="center",
sizing_mode="stretch_width",
)
cd_view_pages = pn.layout.Card(
pn.Column(
pn.Row(ii_figs_per_row, ii_figs_per_col),
isl_page_index,
pn.Row(sel_orientation, ii_page_font_size),
pn.Row(bt_generate_page, fd_genrate_exp_helper),
),
title="View pages",
collapsed=False,
margin=(VERTICAL_PADDING, 0, VERTICAL_PADDING, 0),
)
# MARK: Main form
rbg_active = pn.widgets.RadioButtonGroup(
name="",
options=[
PANES.TEMPLATE,
PANES.RAW_DATA,
PANES.READY_DATA,
PANES.PLATE_VIEWER,
PANES.PAGE_VIEWER,
],
value=PANES.TEMPLATE,
button_type="primary",
button_style="outline",
sizing_mode="stretch_width",
)
plt_template = pn.pane.Matplotlib(align="center", sizing_mode="stretch_height")
tb_raw_experiment = pn.widgets.Tabulator(
value=pd.DataFrame(), pagination="local", page_size=20, sizing_mode="stretch_width"
)
tb_experiment = pn.widgets.Tabulator(
value=pd.DataFrame(), pagination="local", page_size=20, sizing_mode="stretch_width"
)
plt_plate = pn.pane.Matplotlib(align="center", sizing_mode="stretch_height")
plt_page = pn.pane.Matplotlib(align="center", sizing_mode="stretch_height")
ph_main = pn.pane.Placeholder(object=plt_template)
# MARK: Callbacks
def update_experiment():
tb_experiment.value = prepare_experiment(
raw_data=tb_raw_experiment.value,
row_count=ii_row_count.value,
col_count=ii_col_count.value,
discs_per_leaf=ii_discs_per_leaf.value,
forbidden_position=(ii_forbibden_row.value, ii_forbibden_col.value),
control_blocks=ii_control_blocks.value,
)
ii_plate_number.end = int(tb_experiment.value.plaque.max())
ii_plate_number.value = 1
plt_plate.object = update_tray(data=tb_experiment.value)
def update_tray(data: pd.DataFrame | None = None, plate_number: int | None = None):
if data is not None:
p_number = (
data.plaque == ii_plate_number.value
if plate_number is None
else plate_number
)
text_data = (
data[data.plaque == p_number]
.pivot(index="ligne", columns="colonne", values="Num_Plvt")
.values
)
color_data = (
data[data.plaque == p_number]
.pivot(index="ligne", columns="colonne", values="group")
.values
)
else:
text_data = None
color_data = None
return draw_tray(
forbidden_position=(ii_forbibden_row.value, ii_forbibden_col.value),
row_count=ii_row_count.value,
col_count=ii_col_count.value,
discs_per_leaf=ii_discs_per_leaf.value,
tray_size=(ii_template_width.value / 10, ii_template_height.value / 10),
text_data=text_data,
color_data=color_data,
fontsize=ii_font_size.value,
coord_font_size=ii_coordinate_font_size.value,
print_coordinates=CFO_SHOW_COORDINATES in cbg_coordinates.value,
coord_bf=CFO_SHOW_BOLD_FONT in cbg_coordinates.value,
leaf_size=ii_leaf_size.value,
)
def on_exp_plates_requested(data):
merger = PdfWriter()
for i in sorted(data.plaque.unique()):
fig = update_tray(data, plate_number=int(i))
s_buf = io.BytesIO()
fig.savefig(s_buf, bbox_inches="tight", pad_inches=0, format="pdf")
s_buf.seek(0)
merger.append(s_buf)
out_buf = io.BytesIO()
merger.write(out_buf)
out_buf.seek(0)
return out_buf
fd_genrate_exp_plates.callback = pn.bind(on_exp_plates_requested, tb_experiment)
def on_exp_helper_requested(data):
merger = PdfWriter()
figs_per_col = ii_figs_per_col.value
figs_per_row = ii_figs_per_row.value
page_count = data.plaque.max() // (figs_per_row * figs_per_col) + 1
for i in range(page_count):
fig = draw_page(
data=data,
figs_per_col=figs_per_col,
figs_per_row=figs_per_row,
forbibden_position=(ii_forbibden_row.value, ii_forbibden_col.value),
row_count=ii_row_count.value,
col_count=ii_col_count.value,
discs_per_leaf=ii_discs_per_leaf.value,
orientation=sel_orientation.value.lower(),
font_size=ii_page_font_size.value,
page_index=i + 1,
)
s_buf = io.BytesIO()
fig.savefig(s_buf, bbox_inches="tight", pad_inches=0, format="pdf")
s_buf.seek(0)
merger.append(s_buf)
out_buf = io.BytesIO()
merger.write(out_buf)
out_buf.seek(0)
return out_buf
fd_genrate_exp_helper.callback = pn.bind(on_exp_helper_requested, tb_experiment)
@pn.depends(ii_plate_number.param.value, watch=True)
def on_plate_number_changed(plate_number):
plt_plate.object = update_tray(data=tb_experiment.value)
@pn.depends(ii_font_size.param.value, watch=True)
def on_plate_number_changed(font_size):
plt_plate.object = update_tray(data=tb_experiment.value)
@pn.depends(
*[
p.param.value
for p in [
ii_row_count,
ii_col_count,
ii_discs_per_leaf,
ii_forbibden_row,
ii_forbibden_col,
ii_template_width,
ii_template_height,
cbg_coordinates,
ii_coordinate_font_size,
ii_leaf_size,
ii_control_blocks,
]
],
watch=True,
)
def on_template_changed(
row_count,
col_count,
discs_per_leaf,
forbibden_row,
forbibden_column,
template_width,
template_height,
show_coordinates,
coordinate_font_size,
leaf_size,
control_blocks,
):
fig = update_tray()
s_buf = io.BytesIO()
fig.savefig(s_buf, bbox_inches="tight", pad_inches=0, format="pdf")
s_buf.seek(0)
dwn_template.filename = (
f"[R{row_count}][C{col_count}][DPL{discs_per_leaf}]_template.pdf"
)
dwn_template.file = s_buf
plt_template.object = fig
if len(tb_raw_experiment.value) > 0:
update_experiment()
@pn.depends(fi_raw_exp.param.value, watch=True)
def on_file_loaded(value):
if not value or not isinstance(value, bytes):
return pd.DataFrame()
string_io = io.StringIO(value.decode("utf8"))
ret = pd.read_csv(string_io, sep=csv_separator.value)
# ret = pd.concat([ret.sample(n=len(ret)) for _ in range(60)]).reset_index(drop=True)
tb_raw_experiment.value = ret
update_experiment()
fd_genrate_exp_plates.file = "exp_plates.pdf"
fd_genrate_exp_helper.file = "exp_helper.pdf"
rbg_active.value = PANES.READY_DATA
@pn.depends(rbg_active.param.value, watch=True)
def on_active_pane_changed(rbg_value):
match rbg_value:
case PANES.TEMPLATE:
ph_main.object = plt_template
cd_template.collapsed = False
case PANES.RAW_DATA:
ph_main.object = tb_raw_experiment
case PANES.READY_DATA:
ph_main.object = tb_experiment
case PANES.PLATE_VIEWER:
ph_main.object = plt_plate
cd_view_plates.collapsed = False
case PANES.PAGE_VIEWER:
ph_main.object = plt_page
cd_view_pages.collapsed = False
def on_page_requested(event):
if not event:
return
rbg_active.value = PANES.PAGE_VIEWER
plt_page.object = draw_page(
data=tb_experiment.value,
figs_per_col=ii_figs_per_col.value,
figs_per_row=ii_figs_per_row.value,
forbibden_position=(ii_forbibden_row.value, ii_forbibden_col.value),
row_count=ii_row_count.value,
col_count=ii_col_count.value,
discs_per_leaf=ii_discs_per_leaf.value,
orientation=sel_orientation.value.lower(),
font_size=ii_page_font_size.value,
page_index=isl_page_index.value,
)
pn.bind(on_page_requested, bt_generate_page, watch=True)
on_template_changed(
*[
p.value
for p in [
ii_row_count,
ii_col_count,
ii_discs_per_leaf,
ii_forbibden_row,
ii_forbibden_col,
ii_template_width,
ii_template_height,
cbg_coordinates,
ii_coordinate_font_size,
ii_leaf_size,
ii_control_blocks,
]
]
)
# MARK: Layout
pn.extension("tabulator", "ipywidgets")
template = pn.template.BootstrapTemplate(title="Vitis Experiment Builder")
template.sidebar.append(
pn.Column(
cd_template,
cd_load_exp,
cd_view_plates,
cd_view_pages,
)
)
template.main.append(pn.Column(rbg_active, ph_main))
template.servable()