# -*- coding: utf-8 -*-
"""
Created on Thu Feb 20 15:00:14 2025
@author: win11
"""
import dash as dash
from dash import dcc, html, Input, Output, callback
import plotly.express as px
import plotly.graph_objects as go
#from data.dataprep import data_prep
import pandas as pd
import numpy as np
import dash_bootstrap_components as dbc
dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.min.css"
#df_money_order, df_money_orderdetail = data_prep()
df_money_orderdetail = pd.read_csv('orderdetails_money.csv')
#df_orderdetail_grouped = df_money_orderdetail.groupby(by = [df_money_orderdetail['orderDate'].dt.to_period('M').astype('str'), 'categoryName'])['nettoPrice'].sum().reset_index()
def format_prefix_ticktext_fig_money(catname):
if (catname == 'Total'):
return f"<span style='font-weight:bold'> {catname}</span> "
else: return f"<span> {catname}</span> "
def create_fig_money(data):
#VISUAL PRINTS REVENUE PER CATEGORY THIS MONTH
#INPUT IS DATA = SUMMARIZED CATEGORY, REVENUE, DELTA PM AND DELTA PM%
#all tickvalues y axis in an array to use to format
tv = data['categoryName'].to_numpy()
#format all ticks Y axis
tt = [format_prefix_ticktext_fig_money(member) for member in tv]
#ycoordinates for horizontal fake gridlines
#number of in between lines len(data) - 2: first line is black
# in this case we have 8 rows + total rows = 9 rows (len(data)).
# 9 rows is 8 in between rows, 1 row is black (the one above total), leaves 7
# starting at y=1.5. Ok but since np.arrange skips the last value, we do
#len(data) -1 :-)
#define the range
y_coords = np.arange(1.5, len(data) - 1, 1)
fig = go.Figure()
fig.add_trace(go.Bar(
y=data['categoryName'],
x=data['barLength'],
text=data['nettoPrice'],
orientation='h',
#the bars
marker=dict(
color='rgba(0, 0, 0, .9)',
#cornerradius=15,
line=dict(color='rgba(0, 0, 0, 1.0)',
width=1)
)
))
#labels at the end of bar, lambda function to create a bold label for the total
fig.update_traces(texttemplate=data['categoryName'].apply(lambda x: '<b>%{text:.2s}</b>' if x == 'Total' else '%{text:.2s}'), textposition='outside')
fig.update_xaxes(showticklabels=False)
#the generated pre formatted y ticks are used below, result: category Total is bold.
fig.update_yaxes( dict(
tickmode='array',
tickvals=tv,
ticktext=tt
),showgrid=False)
fig.update_layout(
margin=dict(l=5, r=5, t=25, b=5),
plot_bgcolor='white',
title = {
'text': 'AC',
'y':1, # new
'x':0.5,
'xanchor': 'center',
'yanchor': 'top' # new
},
)
#opacity + line_color make the lines appear nice on the white template as they are meant to be
fig.add_hline(y=0.5, line_dash="solid", line_width=1, opacity=1, line_color="Black")
for y in y_coords:
fig.add_hline(y=y, line_dash="solid", line_width=1, opacity=1, line_color='#ded6ca')
#vertical black line after horizontal lines to put it on top of semigridlines
fig.add_vline(x=0, line_dash="solid", line_width=1)
return dcc.Graph(figure = fig)
def create_fig_pm(data):
#get min and max to set a range for the barchart
#and prevent outside values to be invisible, not perfect (yet?)
min_x = data['delta_pm_money'].min()
if (min_x < 0) :
min_x = 2 * min_x
else:
min_x = 0.8 * min_x
max_x = data['delta_pm_money'].max()
if (max_x < 0) :
max_x = 0.8 * max_x
else:
max_x = 1.5 * max_x
#define the range of y-coordinates for the horizontal fake gridlines
y_coords = np.arange(1.5, len(data) - 1, 1)
fig = go.Figure()
fig.add_trace(go.Bar(
y=data['categoryName'],
x=data['delta_pm_money'],
text=data['delta_pm_money'],
orientation='h',
marker=dict(
color=data['delta_pm_money'].apply(lambda x: 'red' if x<0 else 'green'),
line=dict(color=data['delta_pm_money'].apply(lambda x: 'red' if x<0 else 'green'),
width=1)
)
))
#labels at the end of bar
fig.update_traces(texttemplate=data['categoryName'].apply(lambda x: '<b>%{text:.2s}</b>' if x == 'Total' else '%{text:.2s}'), textposition='outside')
#remove yaxis lables
fig.update_xaxes(showticklabels=False, range = [min_x, max_x])
fig.update_yaxes(showticklabels=False)
fig.update_layout(
title = {
'text': 'Δ' + ' PM',
'y':1, # new
'x':0.5,
'xanchor': 'center',
'yanchor': 'top' # new
},
margin=dict(l=5, r=5, t=25, b=5),
plot_bgcolor='white'
)
for y in y_coords:
fig.add_hline(y=y, line_dash="solid", line_width=1, opacity=1, line_color='#ded6ca')
fig.add_hline(y=0.5, line_dash="solid", line_width=1, opacity=1, line_color='Black')
#fig.update(layout_xaxis_range = [-100,100])
fig.add_vline(x=0, line_dash="solid", line_width=1)
return dcc.Graph(figure = fig)
def create_pm_perc(data):
#data = df.copy(deep=True)
#df.groupby('Customer')['Date'].shift()
#print(data[['categoryName','delta_pm_perc']].head(30))
#data = data.loc[data['orderDate'] == '2015-04'].sort_values('nettoPrice', ascending=True)
list_items = []
for index, row in data.iterrows():
color='red'
fontweight = 'normal'
value = row['delta_pm_perc']
if row['delta_pm_perc'] > 0:
value = '+' + str(row['delta_pm_perc'])
color='green'
if row['categoryName'] == 'Total':
fontweight='bold'
list_items.append(dbc.ListGroupItem(value, style={'color': color,'fontWeight':fontweight , 'fontSize':'13px','lineHeight':'30px'}))
#no idea why I need it but somehow the order is reverse in the histograms
list_items.reverse()
list_group = dbc.ListGroup(
list_items,
flush=True, style={'textAlign':'right'}
)
return list_group
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.SANDSTONE,dbc_css])
app.layout = dbc.Container([
dbc.Row([
dbc.Col([
html.H2('IBCS Experiment no.1'),
], className='col-md-6 col-sm-12'),
dbc.Col([
dbc.Select(
id="select_month",
options= df_money_orderdetail['orderDate'].unique(),
value= '2015-04'
)
], className='col-md-6 col-sm-12'),
], style={'marginBottom': '2rem'}),
dbc.Row([
dbc.Col([
html.Div(id='month_money'),
], className='col-md-7 col-sm-12'),
dbc.Col([
html.Div(id='MoM_change')
], className='col-md-3 col-sm-12'),
dbc.Col([
html.Div('\u0394' + ' PM%', style={'textAlign':'right','paddingTop':'0px'}),
html.Div(id='MoM_change_perc' )
], className='col-md-1 col-sm-12')
], className='col-md-12')
],style={'marginTop': '3rem'}, fluid=False)
@app.callback( Output('month_money', 'children'),
Output('MoM_change', 'children'),
Output('MoM_change_perc', 'children'),
Input(component_id='select_month', component_property='value')
)
def update_all(value):
#the incoming dataframe has all total, delta pm and delta pm% for each month,
#filter it on the selected value (=year-month), drop=True to remove the extra
#created useless index column
data = df_money_orderdetail.loc[(df_money_orderdetail['orderDate'].astype('str') == value)]\
.sort_values(['nettoPrice'], ascending=[False]).reset_index(drop=True)
#barLength is for all but the total which is added later the same value as NettoPrice
#in IBSC total has no bar, the barLength will be 0
data['barLength'] = data['nettoPrice']
#define a dict with all the values belonging to a total record
total_dict = {'categoryName': 'Total', 'orderDate': value, \
'nettoPrice': data['nettoPrice'].sum(), \
'delta_pm_money': data['delta_pm_money'].sum(), \
'delta_pm_perc': round(100 * (data['delta_pm_money'].sum()/(data['nettoPrice'].sum()+data['delta_pm_money'].sum())),1),\
'barLength': 0
}
#add the total record at the end of the dataframe where it also has to appear on the screen
data.loc[len(data)] = total_dict
#reverse dataframe, the horizontal bars are drawn from the bottom of the
#visual and ibcs wants it exactly the other way around, must be done after the latest
#indexing and addition of totals
data = data.iloc[::-1]
#update all visuals with the updated data
return create_fig_money(data), create_fig_pm(data), create_pm_perc(data)
app.run_server(debug=True)