import panel as pn import pandas as pd import param from bokeh.models.formatters import PrintfTickFormatter from calculations import CannabinoidCalculations from config import slider_design, slider_style, slider_stylesheet, get_formatter class CannabinoidEstimatorGUI(CannabinoidCalculations): # DataFrame params for tables money_data_unit_df = param.DataFrame( pd.DataFrame(), precedence=-1, # precedence to hide from param pane if shown ) money_data_time_df = param.DataFrame(pd.DataFrame(), precedence=-1) profit_data_df = param.DataFrame(pd.DataFrame(), precedence=-1) processing_data_df = param.DataFrame(pd.DataFrame(), precedence=-1) def __init__(self, **params): super().__init__(**params) self._create_sliders() self._create_tables_and_indicators() self._update_calculations() # Initial calculation and table update def _create_sliders(self): self.batch_frequency_radio = pn.widgets.RadioButtonGroup.from_param( self.param.batch_frequency, name=self.param.batch_frequency.label, options=["Shift", "Day", "Week"], button_type="primary", ) def _create_tables_and_indicators(self): # Table for $/kg Biomass and $/kg Output self.money_unit_table = pn.widgets.Tabulator( self.money_data_unit_df, # Initial empty or pre-filled df formatters={ "$/kg Biomass": get_formatter("$%.02f"), "$/kg Output": get_formatter("$%.02f"), }, disabled=True, layout="fit_data", sizing_mode="fixed", align="center", show_index=False, text_align={ " ": "right", "$/kg Biomass": "center", "$/kg Output": "center", }, ) # Table for Per Shift, Per Day, Per Week self.money_time_table = pn.widgets.Tabulator( self.money_data_time_df, # Initial empty or pre-filled df formatters={ "Per Shift": get_formatter("$%.02f"), "Per Day": get_formatter("$%.02f"), "Per Week": get_formatter("$%.02f"), }, disabled=True, layout="fit_data", sizing_mode="fixed", align="center", show_index=False, text_align={ " ": "right", "Per Shift": "center", "Per Day": "center", "Per Week": "center", }, ) self.profit_table = pn.widgets.Tabulator( self.profit_data_df, # Initial empty or pre-filled df disabled=True, layout="fit_data_table", sizing_mode="fixed", align="center", show_index=False, text_align={"Metric": "right", "Value": "center"}, ) self.processing_table = pn.widgets.Tabulator( self.processing_data_df, # Initial empty or pre-filled df formatters={}, disabled=True, layout="fit_data_table", sizing_mode="fixed", align="center", show_index=False, text_align={"Metric (Per Shift)": "right", "Value": "center"}, ) self.profit_weekly = pn.indicators.Number( name="Weekly Profit", value=0, format="$0 k", default_color="green", align="center", ) self.profit_pct = pn.indicators.Number( name="Operating Profit", value=0, format="0.00%", default_color="green", align="center", ) @param.depends("labour_hours_per_shift", watch=True) def _update_processing_hours_slider_constraints(self): new_max_processing_hours = self.labour_hours_per_shift # Ensure min bound is not greater than new max bound current_min_processing_hours = min( self.param.processing_hours_per_shift.bounds[0], new_max_processing_hours ) self.param.processing_hours_per_shift.bounds = ( current_min_processing_hours, new_max_processing_hours, ) # Check if processing_hours_per_shift_slider exists before trying to update it if hasattr(self, "processing_hours_per_shift_slider"): self.processing_hours_per_shift_slider.end = new_max_processing_hours if self.processing_hours_per_shift > new_max_processing_hours: self.processing_hours_per_shift = new_max_processing_hours # Also update start if it's now greater than end if self.processing_hours_per_shift_slider.start > new_max_processing_hours: self.processing_hours_per_shift_slider.start = ( current_min_processing_hours # or new_max_processing_hours ) def _post_calculation_update(self): """Overrides the base class method to update GUI elements.""" super()._post_calculation_update() # Call base class method if it has any logic self._update_tables_data() def _update_tables_data(self): metric_names = [ "Biomass cost", "Processing cost", "Gross Revenue", "Net Revenue", ] money_data_unit_dict = { " ": metric_names, "$/kg Biomass": [ self.bio_cost, self.internal_cogs_per_kg_bio, self.gross_rev_per_kg_bio, self.net_rev_per_kg_bio, ], "$/kg Output": [ self.biomass_cost_per_kg_output, self.internal_cogs_per_kg_output, self.wholesale_cbx_price, self.net_rev_per_kg_output, ], } self.money_data_unit_df = pd.DataFrame(money_data_unit_dict) if hasattr(self, "money_unit_table"): self.money_unit_table.value = self.money_data_unit_df money_data_time_dict = { " ": metric_names, "Per Shift": [ self.biomass_cost_per_shift, self.internal_cogs_per_shift, self.gross_rev_per_shift, self.net_rev_per_shift, ], "Per Day": [ self.biomass_cost_per_day, self.internal_cogs_per_day, self.gross_rev_per_day, self.net_rev_per_day, ], "Per Week": [ self.biomass_cost_per_week, self.internal_cogs_per_week, self.gross_rev_per_week, self.net_rev_per_week, ], } self.money_data_time_df = pd.DataFrame(money_data_time_dict) if hasattr(self, "money_time_table"): self.money_time_table.value = self.money_data_time_df profit_data_dict = { "Metric": ["Operating Profit", "Resin Spread"], "Value": [ f"{self.operating_profit_pct * 100.0:.2f}%", f"{self.resin_spread_pct * 100.0:.2f}%", ], } self.profit_data_df = pd.DataFrame(profit_data_dict) if hasattr(self, "profit_table"): self.profit_table.value = self.profit_data_df processing_values_formatted_shift = [ f"{self.kg_processed_per_shift:,.0f}", f"{self.saleable_kg_per_shift:,.0f}", f"${self.labour_cost_per_shift:,.2f}", f"${self.variable_cost_per_shift:,.2f}", f"${self.overhead_cost_per_shift:,.2f}", ] processing_values_formatted_day = [ f"{self.kg_processed_per_shift * self.shifts_per_day:,.0f}", f"{self.saleable_kg_per_day:,.0f}", f"${self.labour_cost_per_shift * self.shifts_per_day:,.2f}", f"${self.variable_cost_per_shift * self.shifts_per_day:,.2f}", f"${self.overhead_cost_per_shift * self.shifts_per_day:,.2f}", ] processing_values_formatted_week = [ f"{self.kg_processed_per_shift * self.shifts_per_week:,.0f}", f"{self.saleable_kg_per_week:,.0f}", f"${self.labour_cost_per_shift * self.shifts_per_week:,.2f}", f"${self.variable_cost_per_shift * self.shifts_per_week:,.2f}", f"${self.overhead_cost_per_shift * self.shifts_per_week:,.2f}", ] processing_data_dict = { "Metric Per": [ "Kilograms Extracted", "Kg CBx Produced", "Labour Cost", "Variable Cost", "Overhead", ], "Shift": processing_values_formatted_shift, "Day": processing_values_formatted_day, "Week": processing_values_formatted_week, } self.processing_data_df = pd.DataFrame(processing_data_dict) if hasattr(self, "processing_table"): self.processing_table.value = self.processing_data_df if hasattr(self, "profit_weekly"): self.profit_weekly.value = self.net_rev_per_week # Ensure format updates if value changes significantly (e.g. from 0 to large number) self.profit_weekly.format = ( f"${self.net_rev_per_week / 1000:.0f} k" if self.net_rev_per_week != 0 else "$0 k" ) if hasattr(self, "profit_pct"): self.profit_pct.value = self.operating_profit_pct self.profit_pct.format = f"{self.operating_profit_pct * 100.0:.2f}%" def view(self): input_col_max_width = 400 extractionCol = pn.Column( "### Extraction", self.param.kg_processed_per_hour, self.param.finished_product_yield_pct, sizing_mode="stretch_width", max_width=input_col_max_width, ) biomassCol = pn.Column( pn.pane.Markdown("### Biomass parameters", margin=0), self.param.bio_cbx_pct, self.param.bio_cost, sizing_mode="stretch_width", max_width=input_col_max_width, ) consumableCol = pn.Column( pn.pane.Markdown("### Consumable rates", margin=0), self.param.kwh_rate, self.param.water_cost_per_1000l, self.param.consumables_per_kg_bio_rate, sizing_mode="stretch_width", max_width=input_col_max_width, ) wholesaleCol = pn.Column( pn.pane.Markdown("### Wholesale details", margin=0), self.param.wholesale_cbx_price, self.param.wholesale_cbx_pct, sizing_mode="stretch_width", max_width=input_col_max_width, ) variableCol = pn.Column( pn.pane.Markdown("### Variable processing costs", margin=0), self.param.kwh_per_kg_bio, self.param.water_liters_consumed_per_kg_bio, self.param.consumables_per_kg_output, sizing_mode="stretch_width", max_width=input_col_max_width, ) complianceBatchCol = pn.Column( pn.pane.Markdown("### Compliance", margin=0), self.param.batch_test_cost, pn.pane.Markdown("New Batch Every:", margin=0), self.batch_frequency_radio, sizing_mode="stretch_width", max_width=input_col_max_width, ) leechCol = pn.Column( pn.pane.Markdown("### Weekly Rent & Fixed Overheads", margin=0), self.param.weekly_rent, self.param.non_production_electricity_cost_weekly, self.param.property_insurance_weekly, self.param.general_liability_insurance_weekly, self.param.product_recall_insurance_weekly, sizing_mode="stretch_width", max_width=input_col_max_width, ) workerCol = pn.Column( pn.pane.Markdown("### Worker Details", margin=0), self.param.workers_per_shift, self.param.worker_base_pay_rate, self.param.managers_per_shift, self.param.manager_base_pay_rate, self.param.direct_cost_pct, sizing_mode="stretch_width", max_width=input_col_max_width, ) shiftCol = pn.Column( pn.pane.Markdown("### Shift details", margin=0), self.param.labour_hours_per_shift, self.param.processing_hours_per_shift, self.param.shifts_per_day, self.param.shifts_per_week, sizing_mode="stretch_width", max_width=input_col_max_width, ) input_grid = pn.FlexBox( extractionCol, biomassCol, consumableCol, wholesaleCol, variableCol, complianceBatchCol, workerCol, shiftCol, leechCol, align_content="flex-start", align_items="flex-start", # valid options include: '[stretch, flex-start, flex-end, center, baseline, first baseline, last baseline, start, end, self-start, self-end]' flex_wrap="wrap", ) # Added flex_wrap money_unit_table_display = pn.Column( pn.pane.Markdown( "### Financial Summary (Per Unit)", styles={"text-align": "center"} ), self.money_unit_table, sizing_mode="stretch_width", max_width=input_col_max_width + 50, ) money_time_table_display = pn.Column( pn.pane.Markdown( "### Financial Summary (Aggregated)", styles={"text-align": "center"} ), self.money_time_table, sizing_mode="stretch_width", max_width=500, ) profit_table_display = pn.Column( pn.pane.Markdown("### Profitability", styles={"text-align": "center"}), self.profit_table, sizing_mode="stretch_width", max_width=input_col_max_width, ) processing_table_display = pn.Column( pn.pane.Markdown("### Processing Summary", styles={"text-align": "center"}), self.processing_table, sizing_mode="stretch_width", max_width=input_col_max_width, ) table_grid = pn.FlexBox( self.profit_weekly, self.profit_pct, processing_table_display, profit_table_display, money_unit_table_display, money_time_table_display, align_content="normal", flex_wrap="wrap", ) main_layout = pn.Column( pn.Accordion(("Knobs & Dials",input_grid)), pn.layout.Divider(margin=(10, 0)), table_grid, styles={"margin": "0px 10px"}, ) return main_layout