import panel as pn import pandas as pd import param from bokeh.models.formatters import PrintfTickFormatter # from custom_themes import AIDefaultTheme, AIDarkTheme # Initialize Panel extension for Tabulator and set a global sizing mode pn.extension( "tabulator", sizing_mode="stretch_width", template="fast", # theme = AIDarkTheme, ) # --- Styling Placeholders (as per user instruction) --- slider_design = {} slider_style = {} slider_stylesheet = [] # --- Helper for NumberFormatters --- def get_formatter(format_str): if format_str == "%i": return pn.widgets.tables.NumberFormatter(format="0") elif format_str == "%.1f": return pn.widgets.tables.NumberFormatter(format="0.0") elif format_str == "%.2f": return pn.widgets.tables.NumberFormatter(format="0.00") elif format_str == "%.4f": return pn.widgets.tables.NumberFormatter(format="0.0000") elif format_str == "$%.02f": return pn.widgets.tables.NumberFormatter(format="$0,0.00") return format_str class CannabinoidEstimator(param.Parameterized): # --- Input Parameters --- kg_processed_per_hour = param.Number( default=150.0, bounds=(0, 2000), step=1.0, label="Biomass processed per hour (kg)", ) finished_product_yield_pct = param.Number( default=60.0, bounds=(0.01, 100), step=0.01, label="Product yield: CBx Weight Output / Weight Input (%)", ) kwh_rate = param.Number( default=0.25, bounds=(0.01, 5), step=0.01, label="Power rate ($ per kWh)" ) water_cost_per_1000l = param.Number( default=2.50, bounds=(0.01, 10), step=0.01, label="Water rate ($ per 1000L / m3)", ) consumables_per_kg_bio_rate = param.Number( default=0.0032, bounds=(0, 10), step=0.0001, label="Other Consumables rate ($ per kg biomass)", ) kwh_per_kg_bio = param.Number( default=0.25, bounds=(0.05, 15), step=0.01, label="Power consumption (kWh per kg biomass)", ) water_liters_consumed_per_kg_bio = param.Number( default=3.0, bounds=(0.1, 100), step=0.1, label="Water consumption (liters per kg biomass)", ) consumables_per_kg_output = param.Number( default=10.0, bounds=(0, 100), step=0.01, label="Consumables per kg finished product ($)", ) bio_cbx_pct = param.Number( default=10.0, bounds=(0, 30), step=0.1, label="Cannabinoid (CBx) in biomass (%)" ) bio_cost = param.Number( default=3.0, bounds=(0, 200), step=0.25, label="Biomass purchase cost ($ per kg)", ) wholesale_cbx_price = param.Number( default=220.0, bounds=(25, 6000), step=5.0, label="Gross revenue ($ per kg output)", ) wholesale_cbx_pct = param.Number( default=99.9, bounds=(0, 100), step=0.01, label="CBx in finished product (%)" ) batch_test_cost = param.Number( default=1300.0, bounds=(100, 5000), step=25.0, label="Per-batch testing/compliance costs ($)", ) fixed_overhead_per_week = param.Number( default=2000.0, bounds=(0, 10000), step=1.0, label="Weekly fixed costs ($)" ) workers_per_shift = param.Number( default=9.0, bounds=(1, 20), step=1.0, label="Workers per shift" ) worker_hourly_rate = param.Number( default=5.0, bounds=(0.25, 50), step=0.25, label="Worker loaded pay rate ($/hr)" ) managers_per_shift = param.Number( default=1.0, bounds=(1, 10), step=1.0, label="Supervisors per shift" ) manager_hourly_rate = param.Number( default=10.0, bounds=(5.0, 50), step=0.25, label="Supervisor loaded pay rate ($/hr)", ) processing_hours_per_shift = param.Number( default=7.0, bounds=(0.25, 8.0), step=0.25, label="Processing hours per shift" ) labour_hours_per_shift = param.Number( default=8.0, bounds=(6.0, 12), step=0.25, label="Labor hours per shift" ) shifts_per_day = param.Number( default=3.0, bounds=(1, 10), step=1.0, label="Shifts per day" ) shifts_per_week = param.Number( default=21.0, bounds=(1, 28), step=1.0, label="Shifts per week" ) kg_processed_per_shift = 0.0 labour_cost_per_shift = 0.0 variable_cost_per_shift = 0.0 overhead_cost_per_shift = 0.0 saleable_kg_per_kg_bio = 0.0 saleable_kg_per_shift = 0.0 saleable_kg_per_day = 0.0 saleable_kg_per_week = 0.0 biomass_kg_per_saleable_kg = 0.0 internal_cogs_per_kg_bio = 0.0 internal_cogs_per_shift = 0.0 internal_cogs_per_day = 0.0 internal_cogs_per_week = 0.0 internal_cogs_per_kg_output = 0.0 biomass_cost_per_shift = 0.0 biomass_cost_per_day = 0.0 biomass_cost_per_week = 0.0 biomass_cost_per_kg_output = 0.0 gross_rev_per_kg_bio = 0.0 gross_rev_per_shift = 0.0 gross_rev_per_day = 0.0 gross_rev_per_week = 0.0 net_rev_per_kg_bio = 0.0 net_rev_per_shift = 0.0 net_rev_per_day = 0.0 net_rev_per_week = 0.0 net_rev_per_kg_output = 0.0 operating_profit_pct = 0.0 resin_spread_pct = 0.0 money_data_df = param.DataFrame(pd.DataFrame()) profit_data_df = param.DataFrame(pd.DataFrame()) processing_data_df = param.DataFrame(pd.DataFrame()) def __init__(self, **params): super().__init__(**params) self._create_sliders() self.money_table = pn.widgets.Tabulator( self.money_data_df, formatters=self._get_money_formatters(), disabled=True, layout="fit_data", sizing_mode="fixed", align="center", show_index=False, # Hide index column text_align={ " ": "right", "$/kg Biomass": "center", "$/kg Output": "center", "Per Shift": "center", "Per Day": "center", "Per Week": "center", }, ) self.profit_table = pn.widgets.Tabulator( self.profit_data_df, disabled=True, layout="fit_data_table", sizing_mode="fixed", align="center", show_index=False, # Hide index column text_align={ "Metric": "right", "Value": "center", }, ) self.processing_table = pn.widgets.Tabulator( self.processing_data_df, formatters={}, disabled=True, layout="fit_data_table", sizing_mode="fixed", align="center", show_index=False, # Hide index column text_align={ "Metric (Per Shift)": "right", "Value": "center", }, ) self._update_calculations() def _create_sliders(self): self.kg_processed_per_hour_slider = pn.widgets.EditableFloatSlider.from_param( self.param.kg_processed_per_hour, name=self.param.kg_processed_per_hour.label, fixed_start=self.param.kg_processed_per_hour.bounds[0], fixed_end=self.param.kg_processed_per_hour.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, # format="0", format=PrintfTickFormatter(format="%i kg"), ) self.finished_product_yield_pct_slider = ( pn.widgets.EditableFloatSlider.from_param( self.param.finished_product_yield_pct, name=self.param.finished_product_yield_pct.label, fixed_start=self.param.finished_product_yield_pct.bounds[0], fixed_end=self.param.finished_product_yield_pct.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) ) self.kwh_rate_slider = pn.widgets.EditableFloatSlider.from_param( self.param.kwh_rate, name=self.param.kwh_rate.label, fixed_start=self.param.kwh_rate.bounds[0], fixed_end=self.param.kwh_rate.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", # format=PrintfTickFormatter(format='%.2f per kWh'), ) self.water_cost_per_1000l_slider = pn.widgets.EditableFloatSlider.from_param( self.param.water_cost_per_1000l, name=self.param.water_cost_per_1000l.label, fixed_start=self.param.water_cost_per_1000l.bounds[0], fixed_end=self.param.water_cost_per_1000l.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.consumables_per_kg_bio_rate_slider = ( pn.widgets.EditableFloatSlider.from_param( self.param.consumables_per_kg_bio_rate, name=self.param.consumables_per_kg_bio_rate.label, fixed_start=self.param.consumables_per_kg_bio_rate.bounds[0], fixed_end=self.param.consumables_per_kg_bio_rate.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.0000", ) ) self.kwh_per_kg_bio_slider = pn.widgets.EditableFloatSlider.from_param( self.param.kwh_per_kg_bio, name=self.param.kwh_per_kg_bio.label, fixed_start=self.param.kwh_per_kg_bio.bounds[0], fixed_end=self.param.kwh_per_kg_bio.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.water_liters_consumed_per_kg_bio_slider = ( pn.widgets.EditableFloatSlider.from_param( self.param.water_liters_consumed_per_kg_bio, name=self.param.water_liters_consumed_per_kg_bio.label, fixed_start=self.param.water_liters_consumed_per_kg_bio.bounds[0], fixed_end=self.param.water_liters_consumed_per_kg_bio.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.0", ) ) self.consumables_per_kg_output_slider = ( pn.widgets.EditableFloatSlider.from_param( self.param.consumables_per_kg_output, name=self.param.consumables_per_kg_output.label, fixed_start=self.param.consumables_per_kg_output.bounds[0], fixed_end=self.param.consumables_per_kg_output.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) ) self.bio_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param( self.param.bio_cbx_pct, name=self.param.bio_cbx_pct.label, fixed_start=self.param.bio_cbx_pct.bounds[0], fixed_end=self.param.bio_cbx_pct.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.0", ) self.bio_cost_slider = pn.widgets.EditableFloatSlider.from_param( self.param.bio_cost, name=self.param.bio_cost.label, fixed_start=self.param.bio_cost.bounds[0], fixed_end=self.param.bio_cost.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.wholesale_cbx_price_slider = pn.widgets.EditableFloatSlider.from_param( self.param.wholesale_cbx_price, name=self.param.wholesale_cbx_price.label, fixed_start=self.param.wholesale_cbx_price.bounds[0], fixed_end=self.param.wholesale_cbx_price.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) self.wholesale_cbx_pct_slider = pn.widgets.EditableFloatSlider.from_param( self.param.wholesale_cbx_pct, name=self.param.wholesale_cbx_pct.label, fixed_start=self.param.wholesale_cbx_pct.bounds[0], fixed_end=self.param.wholesale_cbx_pct.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.batch_test_cost_slider = pn.widgets.EditableFloatSlider.from_param( self.param.batch_test_cost, name=self.param.batch_test_cost.label, fixed_start=self.param.batch_test_cost.bounds[0], fixed_end=self.param.batch_test_cost.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) self.fixed_overhead_per_week_slider = pn.widgets.EditableFloatSlider.from_param( self.param.fixed_overhead_per_week, name=self.param.fixed_overhead_per_week.label, fixed_start=self.param.fixed_overhead_per_week.bounds[0], fixed_end=self.param.fixed_overhead_per_week.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) self.workers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param( self.param.workers_per_shift, name=self.param.workers_per_shift.label, fixed_start=self.param.workers_per_shift.bounds[0], fixed_end=self.param.workers_per_shift.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) self.worker_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param( self.param.worker_hourly_rate, name=self.param.worker_hourly_rate.label, fixed_start=self.param.worker_hourly_rate.bounds[0], fixed_end=self.param.worker_hourly_rate.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.managers_per_shift_slider = pn.widgets.EditableFloatSlider.from_param( self.param.managers_per_shift, name=self.param.managers_per_shift.label, fixed_start=self.param.managers_per_shift.bounds[0], fixed_end=self.param.managers_per_shift.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) self.manager_hourly_rate_slider = pn.widgets.EditableFloatSlider.from_param( self.param.manager_hourly_rate, name=self.param.manager_hourly_rate.label, fixed_start=self.param.worker_hourly_rate.default, # Keeping original logic as per file fixed_end=self.param.manager_hourly_rate.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.labour_hours_per_shift_slider = pn.widgets.EditableFloatSlider.from_param( self.param.labour_hours_per_shift, name=self.param.labour_hours_per_shift.label, fixed_start=self.param.labour_hours_per_shift.bounds[ 0 ], # Changed in previous request fixed_end=self.param.labour_hours_per_shift.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) self.processing_hours_per_shift_slider = ( pn.widgets.EditableFloatSlider.from_param( self.param.processing_hours_per_shift, name=self.param.processing_hours_per_shift.label, fixed_start=self.param.processing_hours_per_shift.bounds[0], fixed_end=self.labour_hours_per_shift, # Changed in previous request design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0.00", ) ) self.shifts_per_day_slider = pn.widgets.EditableFloatSlider.from_param( self.param.shifts_per_day, name=self.param.shifts_per_day.label, fixed_start=self.param.shifts_per_day.bounds[0], fixed_end=self.param.shifts_per_day.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) self.shifts_per_week_slider = pn.widgets.EditableFloatSlider.from_param( self.param.shifts_per_week, name=self.param.shifts_per_week.label, fixed_start=self.param.shifts_per_week.bounds[0], fixed_end=self.param.shifts_per_week.bounds[1], design=slider_design, styles=slider_style, stylesheets=slider_stylesheet, format="0", ) @param.depends( "kg_processed_per_hour", "finished_product_yield_pct", "kwh_rate", "water_cost_per_1000l", "consumables_per_kg_bio_rate", "kwh_per_kg_bio", "water_liters_consumed_per_kg_bio", "consumables_per_kg_output", "bio_cbx_pct", "bio_cost", "wholesale_cbx_price", "wholesale_cbx_pct", "batch_test_cost", "fixed_overhead_per_week", "workers_per_shift", "worker_hourly_rate", "managers_per_shift", "manager_hourly_rate", "labour_hours_per_shift", "processing_hours_per_shift", "shifts_per_day", "shifts_per_week", watch=True, ) def _update_calculations(self, *events): self.kg_processed_per_shift = ( self.processing_hours_per_shift * self.kg_processed_per_hour ) if self.shifts_per_week == 0: self.shifts_per_week = 1 self._calc_saleable_kg() self._calc_biomass_cost() self._calc_cogs() self._calc_gross_revenue() self._calc_net_revenue() self.operating_profit_pct = ( (self.net_rev_per_kg_bio / self.gross_rev_per_kg_bio) if self.gross_rev_per_kg_bio else 0.0 ) self.resin_spread_pct = ( ((self.gross_rev_per_kg_bio - self.bio_cost) / self.bio_cost) if self.bio_cost else 0.0 ) self._update_tables_data() @param.depends("labour_hours_per_shift", watch=True) def _update_processing_hours_slider_constraints(self): new_max_processing_hours = self.labour_hours_per_shift # Get the current lower bound of the processing_hours_per_shift parameter current_min_processing_hours = self.param.processing_hours_per_shift.bounds[0] # Update the bounds of the underlying param.Number object for processing_hours_per_shift # This allows the parameter to accept values up to the new maximum self.param.processing_hours_per_shift.bounds = ( current_min_processing_hours, new_max_processing_hours, ) # Ensure the slider widget has been created before trying to access it if hasattr(self, "processing_hours_per_shift_slider"): # Update the 'end' property of the slider widget self.processing_hours_per_shift_slider.end = new_max_processing_hours # If the current value of processing_hours_per_shift is now greater than # the new maximum, adjust it to be the new maximum. if self.processing_hours_per_shift > new_max_processing_hours: self.processing_hours_per_shift = new_max_processing_hours def _calc_cogs(self): worker_cost = self.workers_per_shift * self.worker_hourly_rate manager_cost = self.managers_per_shift * self.manager_hourly_rate self.labour_cost_per_shift = ( worker_cost + manager_cost ) * self.labour_hours_per_shift power_cost_per_kg = self.kwh_rate * self.kwh_per_kg_bio water_cost_per_kg = ( self.water_cost_per_1000l / 1000.0 ) * self.water_liters_consumed_per_kg_bio total_variable_consumable_cost_per_kg = ( self.consumables_per_kg_bio_rate + power_cost_per_kg + water_cost_per_kg ) self.variable_cost_per_shift = ( total_variable_consumable_cost_per_kg * self.kg_processed_per_shift ) self.overhead_cost_per_shift = ( self.fixed_overhead_per_week / self.shifts_per_week if self.shifts_per_week > 0 else 0.0 ) shift_cogs_before_output_specific = ( self.labour_cost_per_shift + self.variable_cost_per_shift + self.overhead_cost_per_shift ) shift_output_specific_cogs = ( self.consumables_per_kg_output * self.saleable_kg_per_shift ) self.internal_cogs_per_shift = ( shift_cogs_before_output_specific + shift_output_specific_cogs ) self.internal_cogs_per_kg_bio = ( self.internal_cogs_per_shift / self.kg_processed_per_shift if self.kg_processed_per_shift > 0 else 0.0 ) self.internal_cogs_per_day = self.internal_cogs_per_shift * self.shifts_per_day self.internal_cogs_per_week = ( self.internal_cogs_per_shift * self.shifts_per_week ) self.internal_cogs_per_kg_output = ( (self.internal_cogs_per_kg_bio * self.biomass_kg_per_saleable_kg) if self.biomass_kg_per_saleable_kg != 0 else 0.0 ) def _calc_gross_revenue(self): self.gross_rev_per_kg_bio = ( self.saleable_kg_per_kg_bio * self.wholesale_cbx_price ) self.gross_rev_per_shift = ( self.gross_rev_per_kg_bio * self.kg_processed_per_shift ) self.gross_rev_per_day = self.gross_rev_per_shift * self.shifts_per_day self.gross_rev_per_week = self.gross_rev_per_shift * self.shifts_per_week def _calc_net_revenue(self): self.net_rev_per_kg_bio = ( self.gross_rev_per_kg_bio - self.internal_cogs_per_kg_bio - self.bio_cost ) self.net_rev_per_shift = self.net_rev_per_kg_bio * self.kg_processed_per_shift self.net_rev_per_day = self.net_rev_per_shift * self.shifts_per_day self.net_rev_per_week = self.net_rev_per_shift * self.shifts_per_week self.net_rev_per_kg_output = ( (self.biomass_kg_per_saleable_kg * self.net_rev_per_kg_bio) if self.biomass_kg_per_saleable_kg != 0 else 0.0 ) def _calc_biomass_cost(self): self.biomass_cost_per_shift = self.kg_processed_per_shift * self.bio_cost self.biomass_cost_per_day = self.biomass_cost_per_shift * self.shifts_per_day self.biomass_cost_per_week = self.biomass_cost_per_shift * self.shifts_per_week def _calc_saleable_kg(self): if self.wholesale_cbx_pct == 0: self.saleable_kg_per_kg_bio = 0.0 else: self.saleable_kg_per_kg_bio = ( (self.bio_cbx_pct / 100.0) * (self.finished_product_yield_pct / 100.0) / (self.wholesale_cbx_pct / 100.0) ) self.saleable_kg_per_shift = ( self.saleable_kg_per_kg_bio * self.kg_processed_per_shift ) self.saleable_kg_per_day = self.saleable_kg_per_shift * self.shifts_per_day self.saleable_kg_per_week = self.saleable_kg_per_shift * self.shifts_per_week self.biomass_kg_per_saleable_kg = ( 1 / self.saleable_kg_per_kg_bio if self.saleable_kg_per_kg_bio > 0 else 0.0 ) self.biomass_cost_per_kg_output = ( self.biomass_kg_per_saleable_kg * self.bio_cost ) def _update_tables_data(self): money_data_dict = { " ": ["Biomass cost", "Processing cost", "Gross Revenue", "Net Revenue"], "$/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, ], "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_df = pd.DataFrame(money_data_dict) if hasattr(self, "money_table"): self.money_table.value = self.money_data_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 = [ f"{self.kg_processed_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_data_dict = { "Metric (Per Shift)": [ "Kilograms Extracted", "Labour Cost", "Variable Cost", "Overhead", ], "Value": processing_values_formatted, } self.processing_data_df = pd.DataFrame(processing_data_dict) if hasattr(self, "processing_table"): self.processing_table.value = self.processing_data_df def _get_money_formatters(self): return { "$/kg Biomass": get_formatter("$%.02f"), "$/kg Output": get_formatter("$%.02f"), "Per Shift": get_formatter("$%.02f"), "Per Day": get_formatter("$%.02f"), "Per Week": get_formatter("$%.02f"), } def view(self): input_col_max_width = 400 col1 = pn.Column( "### Extraction", self.kg_processed_per_hour_slider, self.finished_product_yield_pct_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col2 = pn.Column( pn.pane.Markdown("### Biomass parameters"), self.bio_cbx_pct_slider, self.bio_cost_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col3 = pn.Column( pn.pane.Markdown("### Consumable rates"), self.kwh_rate_slider, self.water_cost_per_1000l_slider, self.consumables_per_kg_bio_rate_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col4 = pn.Column( pn.pane.Markdown("### Wholesale details"), self.wholesale_cbx_price_slider, self.wholesale_cbx_pct_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col5 = pn.Column( pn.pane.Markdown("### Variable costs"), self.kwh_per_kg_bio_slider, self.water_liters_consumed_per_kg_bio_slider, self.consumables_per_kg_output_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col6 = pn.Column( pn.pane.Markdown("### Compliance"), self.batch_test_cost_slider, pn.pane.Markdown("### Overhead"), self.fixed_overhead_per_week_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col8 = pn.Column( pn.pane.Markdown("### Worker Details"), self.workers_per_shift_slider, self.worker_hourly_rate_slider, self.managers_per_shift_slider, self.manager_hourly_rate_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) col9 = pn.Column( pn.pane.Markdown("### Shift details"), self.labour_hours_per_shift_slider, self.processing_hours_per_shift_slider, self.shifts_per_day_slider, self.shifts_per_week_slider, sizing_mode="stretch_width", max_width=input_col_max_width, ) input_grid = pn.FlexBox( col1, col2, col3, col4, col5, col8, col9, col6, align_content="normal" ) money_table_display = pn.Column( pn.pane.Markdown("### Financial Summary", styles={"text-align": "center"}), self.money_table, sizing_mode="stretch_width", max_width=700, ) 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, ) profit_weekly = pn.indicators.Number( name="Weekly Profit", value=self.net_rev_per_week, format=f"${self.net_rev_per_week / 1000:.0f} k", default_color="green", align="center", ) profit_pct = pn.indicators.Number( name="Operating Profit", value=self.operating_profit_pct, format=f"{self.operating_profit_pct * 100.0:.2f}%", default_color="green", align="center", ) table_grid = pn.FlexBox( profit_weekly, profit_pct, processing_table_display, profit_table_display, money_table_display, align_content="normal", ) main_layout = pn.Column( input_grid, pn.layout.Divider(margin=(10, 0)), table_grid, styles={"margin": "0px 10px"}, ) return main_layout estimator_app = CannabinoidEstimator() # To run in a Panel server: # pn.config.raw_css = custom_themes.get_base_css(custom_themes.DARK_THEME_VARS) estimator_app.view().servable(title="CBx Revenue Estimator") if __name__ == "__main__": pn.serve( estimator_app.view(), title="CBx Revenue Estimator (Panel)", show=True, port=5007, )