Figure Friday 2025 - week 24

join the Figure Friday session on June 20, at noon Eastern Time, to showcase your creation and receive feedback from the community.

What’s the average penalty or fine for open parking and camera violations? What months or days of the week see the highest amount of violations?

Answer these questions and a few others by using Plotly and Dash on the open parking and camera violations dataset.

The dataset is filtered for violations in 2023 only. You can find the complete dataset on the NYC Open Data portal.

Things to consider:

  • what can you improve in the app or sample figure below (bar chart)?
  • would you like to tell a different data story using a different graph?
  • can you create a different Dash app?

Sample figure:

Code for sample figure:
from dash import Dash, dcc
import dash_ag_grid as dag
import plotly.express as px
import pandas as pd

# Download CSV sheet from Google Drive: https://drive.google.com/file/d/1ZdH5C3kWaiRR3fOotvdW8l351YrKQJgG/view?usp=sharing
df = pd.read_csv("Open_Parking_and_Camera_Violations.csv")

df['Issue Date'] = pd.to_datetime(df['Issue Date'])
df['Day of Week'] = df['Issue Date'].dt.day_name()

# Filter the DataFrame to include only the top 7 most common agencies.
top_7_agencies = df['Issuing Agency'].value_counts().nlargest(7).index.tolist()
df_filtered = df[df['Issuing Agency'].isin(top_7_agencies)].copy()

# Group by 'Day of Week' and 'Issuing Agency' and count the number of violations in each group.
violations_summary = df_filtered.groupby(['Day of Week', 'Issuing Agency']).size().reset_index(name='Count')

# Prep for plotting
days_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
violations_summary['Day of Week'] = pd.Categorical(
    violations_summary['Day of Week'],
    categories=days_order,
    ordered=True
)
violations_summary = violations_summary.sort_values('Day of Week')

fig = px.bar(
    violations_summary,
    x='Day of Week',
    y='Count',
    color='Issuing Agency',
    barmode='stack',
    title='Count of Violations by Issuing Agency and Day of Week',
    labels={
        'Count': 'Number of Violations',
        'Day of Week': 'Day of the Week',
        'Issuing Agency': 'Issuing Agency'
    },
    template='plotly_white'
)

fig.update_layout(
    xaxis_title="Day of the Week",
    yaxis_title="Number of Violations",
    legend_title="Issuing Agency"
)


grid = dag.AgGrid(
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, 'filter': True, 'sortable': True} for i in df.columns],
    dashGridOptions={"pagination": True},
    # columnSize="sizeToFit"
)

app = Dash()
app.layout = [
    grid,
    dcc.Graph(figure=fig)
]


if __name__ == "__main__":
    app.run(debug=False)

Participation Instructions:

  • Create - use the weekly data set to build your own Plotly visualization or Dash app. Or, enhance the sample figure provided in this post, using Plotly or Dash.
  • Submit - post your creation to LinkedIn or Twitter with the hashtags #FigureFriday and #plotly by midnight Thursday, your time zone. Please also submit your visualization as a new post in this thread.
  • Celebrate - join the Figure Friday sessions to showcase your creation and receive feedback from the community.

:point_right: If you prefer to collaborate with others on Discord, join the Plotly Discord channel.

Data Source:

Thank you to the NYC Open Data portal for the data.

5 Likes

Here, I have create a pie chart to show top 5 state with highest Payment. And the rest are grouped as “Others”

  • I made it a donut style using “hole=0.3”
  • Notice the legend, “Others” is the last one (instead of order all the labels in alphabetical order.)

from dash import Dash, dcc   # https://dash.plotly.com/
import dash_ag_grid as dag
import plotly.express as px  # https://plotly.com/python/
import pandas as pd

# Download of complete CSV sheet from Google Drive: https://drive.google.com/file/d/1ZdH5C3kWaiRR3fOotvdW8l351YrKQJgG/view?usp=sharing
# This data app only uses 200 rows of the data becuase py.cafe limits data file size
df = pd.read_csv("Open_Parking_and_Camera_Violations.csv")


grid = dag.AgGrid(
    rowData=df.to_dict("records"),
    columnDefs=[{"field": i, 'filter': True, 'sortable': True} for i in df.columns],
    dashGridOptions={"pagination": True},
    # columnSize="sizeToFit"
)

# Make sure "Payment Amount" is numeric
df["Payment Amount"] = pd.to_numeric(df["Payment Amount"], errors="coerce")

# Group by state and sum "Amount Due"
state_totals = df.groupby("State")["Payment Amount"].sum().reset_index()

# Split top N and others
top_n = 5
top_states = state_totals.sort_values("Payment Amount", ascending=False).reset_index(drop=True)
top = top_states.iloc[:top_n]
others = top_states.iloc[top_n:]

# Add "Others" row
others_total = others["Payment Amount"].sum()

others_row = pd.DataFrame([{"State": "Others", "Payment Amount": others_total}])
top = pd.concat([top, others_row], ignore_index=True)

# Extract list of states, with "Others" at the end
states_order = [s for s in top["State"] if s != "Others"] + ["Others"]

# Create pie chart
fig = px.pie(
    top,
    names="State",
    values="Payment Amount",
    title="Total Payment Amount by State (Top 5 + Others)",
    hole=0.3,  # optional: makes it a donut chart
    category_orders={"State": states_order}  # <-- force legend order here
)
fig.update_layout(
    title="Total Payment Amount by State",
    legend=dict(
        orientation="h",         # horizontal layout
        yanchor="bottom",
        y=-0.1,
        xanchor="center",
        x=0.5,
        font=dict(size=12)
    ),
    height=850,  # increase figure height
    width=600    # optional: wider chart
)

app = Dash()
app.layout = [
    grid, dcc.Graph(figure=fig)]
6 Likes

Was a fun experience, created a bar chart using the data we got from Adam. Learnt a few about plotly using the documentation.

import pandas as pd
import plotly.express as px


# Load your dataset
open_drive = pd.read_csv("./Open_Parking_and_Camera_Violations.csv")  # Replace with your actual file path

# Optional: Clean the data (remove missing or invalid values)
open_drive = open_drive.dropna(subset=["Precinct", "Fine Amount"])

# Group by 'Precinct' and sum the 'Fine Amount'
grouped_data = open_drive.groupby("Precinct", as_index=False)["Fine Amount"].sum()

# Create a bar chart
fig = px.bar(
    grouped_data,
    x="Precinct",
    y="Fine Amount",
    color="Fine Amount",
    color_continuous_scale="Viridis",  # Cool color scale
    title="Total Fine Amount by Precinct",
    labels={"Precinct": "Precinct", "Fine Amount": "Total Fine Amount ($)"}
)

fig.update_layout(
    xaxis_title="Precinct",
    yaxis_title="Total Fine Amount ($)",
    template="plotly_dark"  # Optional: dark theme
)


fig.show()

type or paste code here

4 Likes

Hey @Alisa_Anny ,

First of all, congratulations on your first contribution here — that’s a great milestone! :tada: I also wanted to say well done on already applying some best practices, like highlighting the largest states, grouping the rest into “Others,” and making sure that category appears last. Really thoughtful work!

One small note: personally, I might have gone with a bar chart instead of a donut chart. That’s just a matter of preference though — it’s generally easier for people to compare lengths than areas. That said, your use of a donut chart isn’t wrong at all, especially since you included the labels to guide the viewer.

All in all, great job — excited to see more from you! :rocket:

4 Likes

Hey @T_Matt,

Congrats on your first contribution! :tada: Would you mind adding a screenshot of your chart as well? :raising_hands:

2 Likes

Thank you so much for your feedback :blush:. That’s very helpful. Can’t wait to learn more. :woman_technologist:

5 Likes

Hi Everyone, this week Figure Friday 24,

This time I designed the ":slot_machine: NYC Traffic Fine Lottery, What is this?

It’s a fun web app that lets you “spin a wheel” to see what NYC traffic fine you’d get if you were ticketed. But it’s not just a game - it uses real data from thousands of actual NYC fines to educate you about the costs.

How does it work?

  1. You spin the wheel - Like at a casino, but with fines :sweat_smile:
  2. You get a real fine - From $50 to $650
  3. You see all the details - What violation you committed, when it usually happens, etc.
  4. You learn fun facts - Like how a $650 fine equals 43 hours of minimum wage work

What can you do?

  • Compare your fine with everyday things (how many Starbucks coffees is it worth?)
  • Calculate annual cost if you keep getting tickets
  • See real statistics about when and where each type of violation happens
  • Read fun facts about NYC fines

Why did you do this?

Because traffic fines are super boring until you have to pay one. This app turns real data (sometimes shocking) into something entertaining and educational.

It’s like a “fine simulator” that helps you understand:

  • How expensive fines can get
  • How much money you could spend per year
  • How “lucky” or “unlucky” you’d be

The experience

Imagine a casino, but instead of betting money, you “bet” to see what fine you get. Except here you always “lose” because all fines cost money :stuck_out_tongue_winking_eye:

Bottom line: It’s a fun way to learn about something that would normally be super boring, using real NYC data to educate you about financial responsibility and traffic rules.

Perfect for killing time while learning something useful! :thinking:

the app link:

This app is far from being perfect so any bug/issue, I´m sorry , any comments/suggestions more than welcome

6 Likes

:rocket: .header-section::before

Your app made me ask for this, because someone once prevented me from
doing something typically dutch, actually we’re educated when we are around
4 yo how to do it safe, how to cross a street. Didn’t know that in the US there
were other rules 30 years ago:

Summary for SF pedestrians:

  • Crossing outside walk signals or mid-block? Allowed if it’s safe.
  • Police can only cite you if your crossing creates an immediate hazard.
  • Unsafe crossing can still get you a $200+ fine.
3 Likes

Love the creativity, @Avacsiglo21

You know what might be cool interactivity?! If every time after the user spins the wheel, the respective pie slice gets pulled from the center.

This is so fun to play :smiley:

You should add your Github/LInkedin/Forum profile in the footnote section.

2 Likes

Good point Adam, I actually thought about putting the wheel on the spinner, but I opted for the simplest option since it’s all about the information, not distracting more than necessary. Thanks a Lot for your useful feedback

2 Likes

This is a very interesting topic, my proposal is to visualize the monetary components associated with traffic fines, distinguishing between the different elements. Additionally, to identify temporal patterns in the occurrence of traffic violations throughout the day, in order to analyze at what times certain specific types of violations are more frequent.


Application code

4 Likes

Hi everyone, here my contribution to Figure Friday 2025 week 24:




Find the code here

4 Likes

Hi @Avacsiglo21, always great to see things from you!

I have to say, this is one of the most entertaining dashboards I’ve seen! :joy: You’ve taken a typically dry topic and turned it into something genuinely fun and engaging. The casino-style “Spin the Wheel” mechanic is such a clever and creative way to bring real data to life.


What’s Working Well

Engagement & Gamification

  • The “Spin the Wheel” feature is highly interactive and immediately draws users in.
  • Including fun facts, comparisons, and everyday analogies (like movie tickets or Starbucks coffees) makes the data feel much more accessible. Very smart!

Clear Structure & Visual Hierarchy

  • The dashboard is well-structured, with clearly defined sections.
  • Key information—such as fine amount, rank, and risk level—is easy to find and visually stands out.

Visual Variety

  • The use of a pie chart for the wheel, along with icons and color-coded risk indicators, keeps the design visually dynamic while helping users quickly grasp the content.

Opportunities for Improvement

Visual Clutter

  • Suggestion: Since the pie chart is already quite busy, consider showing only the dollar amounts in the segments and keeping percentage details in the hover tooltip (which you already have) only. This small change could help reduce clutter and make the chart easier to read at a glance.

Layout & Responsiveness

  • Suggestion: If possible, aim to fit the dashboard within a single screen to avoid scrolling. You might also try rearranging some sections—perhaps placing the “Fine Comparison” directly below the “NYC Traffic Fine wheel,” and moving the “NY Traffic Fine Facts” above or below the “Quick Stats”. This way, all the information related to the fine is grouped together on the left, and the extra fun facts are on the right. This would further enhance the user experience by keeping related content together :+1:

Visual Consistency

  • Suggestion: This is a tiny detail (and admittedly a bit of an OCD moment :grinning_face_with_smiling_eyes:), but in the “Quick Stats” and “Fine Comparison” sections, I’d suggest keeping the text on the same line as the emojis, followed by a line break before the next item. This would help maintain visual consistency and give the layout a cleaner, more polished look.

Overall: This is an outstanding example of how to turn “boring” data into something fun, engaging, and memorable. Great work! :clap:

3 Likes

Hi @U-Danny, really great work! I always enjoy seeing your dashboards—your signature navigation arrows make it so easy to move between views and really support the storytelling flow! :clap:


What’s Working Well

Strong Visual Encodings

  • The heatmap is a great way to show hourly patterns across violation types. The color gradient quickly highlights peaks and helps spot trends at a glance.

Consistent Design & Layout

  • The use of a cohesive color scheme and structured layout give the dashboard a clean, polished feel.
  • Clear section headers and well-labeled axes make it easy to understand what each chart is showing.

Guided Storytelling

  • The navigation arrows clearly communicate that this is a multi-part story. It’s a nice way to guide users through the analysis in a logical order.

Opportunities for Improvement

Heatmap

(Some of this may already be implemented—it’s just hard to tell from the screenshot!)

  • Hover Details: If not already included, adding tooltips with Z-scores or counts would give more depth without cluttering the chart.
  • Z-Score Explanation: A quick note or info icon explaining what a Z-score is could be helpful for users unfamiliar with the term.
  • Ordering: If the violation types aren’t already ordered by total frequency, this could be a helpful tweak. Sorting them from most to least common would allow users to focus on the highest-impact categories first.

Parallel Coordinates

  • Interpretability: Parallel coordinates charts can become visually dense when many lines overlap. Adding interactivity—such as filters, line highlighting, or hover details—would help users explore specific patterns more effectively.
  • Color Clarity: It’s a bit unclear what the current line colors represent. A legend or more distinct color-coding could help differentiate groups :+1:t2:

Overall: This is a well-designed dashboard with a good mix of interactivity and storytelling. Excited to see more from you :clap:

2 Likes

Hi @Xavi.LL,

really nice work pulling together all these visualizations! There’s a lot of valuable information here, and the mix of chart types helps tell a well-rounded story about parking and camera violations. :blush:


What’s Working Well

Variety of Visualizations

  • Great use of both bar charts and maps to highlight trends across time, categories, and geography.
  • The combination of stacked bars and choropleth maps allows users to explore the data from multiple angles.

Clear Grouping and Labeling

  • Each chart is clearly titled, making it easy to understand what’s being shown.
  • Legends are included and color-coded, which helps with interpreting the different categories.

Geographic Context

  • The state-level maps work well to highlight regional differences in fines and penalties. Pairing them with bar charts makes it easy to compare states side by side.

Opportunities for Improvement

Stacked Bar Charts

Color & Readability

  • You might consider using a more colorblind-friendly palette, as some colors (like black and dark blue) are a bit hard to distinguish next to each other—especially for users with vision impairments.
  • Since stacked bars can get visually busy with many categories, it could be worth grouping less frequent ones into an “Other” category to improve clarity and focus on the main patterns e.g. focus on the biggest 3-5 categories only.

Ordering & Sorting

  • It might also be helpful to sort categories by total fine amount—both in the legend and within the bars—so the most important segments are easier to identify at a glance.

Geographic Maps & Bar

Interactivity

  • This setup would be perfect for a parameterized view! Right now, you’ve created five versions of the same map and bar chart, each showing a different metric. If you’re using Dash, you could simplify this by using a dropdown to switch between variables. That way, users can explore different metrics without you having to duplicate these charts and they would then all follow the same color sequence :slight_smile:

Overall: Very nice job! With a few adjustments around color accessibility, category grouping, and interactivity, it could become even more intuitive and user-friendly. Excited to see what you build next! :clap:

3 Likes

Really nice parallel coordinates chart. It looks like they are many more final Payment Amounts than the initial Fine Amount. Why is that? Is that because the initial Fine Amount just had a few categories?

1 Like

First of all, thank you very much for your always generous and gratifying words about my work. I’m immensely grateful that you invest your time in giving us such comprehensive advice.

  1. I completely agree with what you said about the pie chart; reorganizing it would make the numbers clearer. However, I wanted to keep the probabilities visible because they’re a key piece of information.
  2. Agreed! Believe me, I tried to keep it on a single screen to avoid scrolling. I thought about using radio items or accordions, but then I realized, looking at it as a whole, that information would be lost, or at least it would force the user to search for information.

Once again, thank you very much for all your valuable advice.

2 Likes

Hi @li.nguyen, thank you very much for your comments. I really appreciate the improvements you mentioned. I’m sure implementing them would make it much more intuitive and interactive. Thanks!

2 Likes

Parking and Camera Violations Interactive Dashboard

This Dash app loads and cleans a CSV file of parking and camera violations, extracting key fields like violation status, date, and hour. It provides dropdown filters for state and violation type, and displays four KPI cards: unique plates, total violations, total fine amount, and unpaid amount. The interface has three tabs: one for violation types and times (bar chart and heatmap), one for status and amounts (pie chart and scatter plot), and one for an interactive data table. All charts and KPIs update dynamically based on the selected filters. The app uses Dash Bootstrap Components for styling and Dash AG Grid for the data table.

I try to upload it later to pycafe.

Code:
import pandas as pd
from dash import Dash, html, Input, Output, dcc
import dash_bootstrap_components as dbc
import plotly.express as px
import dash_ag_grid as dag

# US state abbreviation mapping
us_state_abbrev = {
    'AL': 'Alabama', 'AK': 'Alaska', 'AZ': 'Arizona', 'AR': 'Arkansas',
    'CA': 'California', 'CO': 'Colorado', 'CT': 'Connecticut', 'DE': 'Delaware',
    'FL': 'Florida', 'GA': 'Georgia', 'HI': 'Hawaii', 'ID': 'Idaho',
    'IL': 'Illinois', 'IN': 'Indiana', 'IA': 'Iowa', 'KS': 'Kansas',
    'KY': 'Kentucky', 'LA': 'Louisiana', 'ME': 'Maine', 'MD': 'Maryland',
    'MA': 'Massachusetts', 'MI': 'Michigan', 'MN': 'Minnesota', 'MS': 'Mississippi',
    'MO': 'Missouri', 'MT': 'Montana', 'NE': 'Nebraska', 'NV': 'Nevada',
    'NH': 'New Hampshire', 'NJ': 'New Jersey', 'NM': 'New Mexico', 'NY': 'New York',
    'NC': 'North Carolina', 'ND': 'North Dakota', 'OH': 'Ohio', 'OK': 'Oklahoma',
    'OR': 'Oregon', 'PA': 'Pennsylvania', 'RI': 'Rhode Island', 'SC': 'South Carolina',
    'SD': 'South Dakota', 'TN': 'Tennessee', 'TX': 'Texas', 'UT': 'Utah',
    'VT': 'Vermont', 'VA': 'Virginia', 'WA': 'Washington', 'WV': 'West Virginia',
    'WI': 'Wisconsin', 'WY': 'Wyoming', 'DC': 'District of Columbia'
}

# Load and clean data
df = pd.read_csv("violations.csv")
df = df.drop_duplicates()

# Use "in progress" instead of "unknown"
df['Violation Status'] = df['Violation Status'].fillna("in progress")
df['Violation Status'] = df['Violation Status'].replace("unknown", "in progress")

df['Issue Date'] = pd.to_datetime(df['Issue Date'])
df['Day of Week'] = df['Issue Date'].dt.day_name()

def extract_hour(time_str):
    try:
        if pd.isna(time_str):
            return None
        t = time_str.strip().upper()
        if t[-1] in ['A', 'P']:
            hour = int(t.split(':')[0])
            if t[-1] == 'P' and hour != 12:
                hour += 12
            if t[-1] == 'A' and hour == 12:
                hour = 0
            return hour
        return int(t.split(':')[0])
    except Exception:
        return None

df['Violation Hour'] = df['Violation Time'].apply(extract_hour)

# Add full state name column
state_col = 'Plate State' if 'Plate State' in df.columns else 'State'
if state_col not in df.columns:
    raise Exception("No state column found! Please add a 'State' or 'Plate State' column.")
df['State Full'] = df[state_col].map(us_state_abbrev).fillna(df[state_col])

def dbc_select_options(col):
    opts = [{"label": "All", "value": "ALL"}]
    opts += [{"label": str(val), "value": str(val)} for val in sorted(df[col].dropna().unique())]
    return opts

def state_dropdown_options(col):
    opts = [{"label": "All", "value": "ALL"}]
    opts += [
        {
            "label": us_state_abbrev.get(val, val),
            "value": val
        }
        for val in sorted(df[col].dropna().unique())
    ]
    return opts

state_options = state_dropdown_options(state_col)
violation_options = dbc_select_options('Violation')

tab_style = {
    "padding": "12px 24px",
    "fontWeight": "bold",
    "background": "transparent",
    "color": "#fff",
    "border": "none",
    "borderBottom": "3px solid transparent",
    "fontSize": "1.1rem",
    "marginRight": "8px",
    "marginBottom": "0px",
    "textAlign": "center",
    "transition": "border-bottom 0.2s"
}
tab_selected_style = {
    **tab_style,
    "color": "#98ACED",
    "borderBottom": "3px solid #98ACED",
    "background": "transparent"
}

YELLOW_PALETTE = [
    "#fde725", "#ffd600", "#ffe066", "#fff59d", "#fffde7",
    "#ffe082", "#ffd54f", "#ffecb3", "#fff9c4", "#fffde7"
]

def make_aggrid(dataframe):
    # Choose your important columns here (adjust names as needed)
    important_cols = []
    for col in [
        "Plate", "Plate Number", "Plate State", "State", "Violation",
        "Violation Status", "Fine Amount", "Issue Date", "Amount Due",
        "Violation Time", "Day of Week", "Issuing Agency", "Street Name",
        "Vehicle Body Type", "Vehicle Make", "Vehicle Expiration Date"
    ]:
        if col in dataframe.columns:
            important_cols.append(col)
    # Pick the first 10 unique ones
    important_cols = list(dict.fromkeys(important_cols))[:10]
    if not important_cols:
        important_cols = dataframe.columns[:10]
    return dag.AgGrid(
        rowData=dataframe[important_cols].to_dict("records"),
        columnDefs=[
            {
                "field": col,
                "filter": True,
                "sortable": True,
                "hide": False,
                "checkboxSelection": True if i == 0 else False,
                "headerCheckboxSelection": True if i == 0 else False
            }
            for i, col in enumerate(important_cols)
        ],
        dashGridOptions={
            "pagination": True,
            "domLayout": "autoHeight",
            "sideBar": {
                "toolPanels": [
                    {
                        "id": "columns",
                        "labelDefault": "Columns",
                        "labelKey": "columns",
                        "iconKey": "columns",
                        "toolPanel": "agColumnsToolPanel",
                        "toolPanelParams": {
                            "suppressRowGroups": True,
                            "suppressValues": True,
                            "suppressPivots": True,
                            "suppressPivotMode": True
                        }
                    }
                ],
                "defaultToolPanel": "columns",
                "position": "left"
            },
            "rowSelection": "multiple"
        },
        className="ag-theme-alpine-dark",
        style={"height": "600px", "width": "100%"}
    )

def kpi_card(title, value, icon, color):
    return dbc.Card(
        dbc.CardBody([
            html.Div([
                html.I(
                    className=f"bi {icon}",
                    style={
                        "fontSize": "2rem",
                        "color": color,
                        "position": "absolute",
                        "top": "12px",
                        "left": "16px"
                    }
                ),
                html.Div(title, style={
                    "fontWeight": "bold",
                    "fontSize": "1.1rem",
                    "color": "#fff",
                    "marginTop": "8px",
                    "marginLeft": "0px",
                    "textAlign": "center"
                }),
                html.H3(value, className="card-title", style={
                    "color": "#fff",
                    "fontWeight": "bold",
                    "margin": 0,
                    "marginTop": "8px",
                    "textAlign": "center"
                }),
            ],
            style={
                "position": "relative",
                "height": "100%",
                "display": "flex",
                "flexDirection": "column",
                "alignItems": "center",
                "justifyContent": "center"
            })
        ]),
        style={
            "backgroundColor": "#23272b",
            "borderRadius": "18px",
            "border": "none",
            "boxShadow": "none",
            "minWidth": "220px",
            "margin": "0 8px",
            "textAlign": "center"
        },
        className="mx-2"
    )

app = Dash(__name__, external_stylesheets=[
    dbc.themes.CYBORG,
    "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
])

app.layout = dbc.Container([
    html.H2("Parking and Camera Violations Dashboard", className="mt-4 mb-4", style={"color": "white"}),
    dbc.Row([
        dbc.Col(width=3),
        dbc.Col([
            html.Label("Filter by State:", style={"color": "white"}),
            dbc.Select(
                id="state-select",
                options=state_options,
                value="ALL",
                style={
                    "backgroundColor": "#222831",
                    "color": "white",
                    "border": "1px solid #444",
                    "fontWeight": "bold",
                    "fontSize": "0.9rem"
                },
                className="mb-2"
            )
        ], width=3),
        dbc.Col([
            html.Label("Filter by Violation Type:", style={"color": "white"}),
            dbc.Select(
                id="violation-select",
                options=violation_options,
                value="ALL",
                style={
                    "backgroundColor": "#222831",
                    "color": "white",
                    "border": "1px solid #444",
                    "fontWeight": "bold",
                    "fontSize": "0.9rem"
                },
                className="mb-2"
            )
        ], width=3),
    ], justify="end"),
    html.Div(style={"height": "34px"}),
    dbc.Row([
        dbc.Col(
            dcc.Tabs(
                id="tabs",
                value="tab-1",
                children=[
                    dcc.Tab(
                        label="Violation Types & Times",
                        value="tab-1",
                        style=tab_style,
                        selected_style=tab_selected_style
                    ),
                    dcc.Tab(
                        label="Status & Amounts",
                        value="tab-2",
                        style=tab_style,
                        selected_style=tab_selected_style
                    ),
                    dcc.Tab(
                        label="Data Table",
                        value="tab-3",
                        style=tab_style,
                        selected_style=tab_selected_style
                    ),
                ],
                className="mb-0"
            ),
            width=12
        )
    ], className="mb-0"),
    html.Div(style={"height": "24px"}),
    dbc.Row([
        dbc.Col(
            dcc.Loading(
                id="loading-kpi",
                type="circle",
                color="#3FD927",
                children=html.Div(id="kpi-cards")
            ),
            width=12,
            className="mb-4"
        )
    ]),
    dbc.Row([
        dbc.Col(
            dcc.Loading(
                id="loading-tab",
                type="circle",
                color="#3FD927",
                children=html.Div(id="tab-content", className="mt-2")
            ),
            width=12
        ),
    ])
], fluid=True, style={"backgroundColor": "#181a1b", "minHeight": "100vh"})

def filter_all(df, state, violation):
    dff = df.copy()
    # Only show rows where Violation Status is not "in progress"
    dff = dff[dff['Violation Status'] != "in progress"]
    if state and state != "ALL":
        dff = dff[dff[state_col] == state]
    if violation and violation != "ALL":
        dff = dff[dff['Violation'] == violation]
    return dff

@app.callback(
    Output("kpi-cards", "children"),
    Input("state-select", "value"),
    Input("violation-select", "value"),
)
def update_kpis(state, violation):
    filtered = filter_all(df, state, violation)
    total_viol = len(filtered)
    total_fine = filtered['Fine Amount'].sum() if 'Fine Amount' in filtered.columns else 0
    unpaid = filtered['Amount Due'].sum() if 'Amount Due' in filtered.columns else 0

    # Unique Plates as FIRST KPI
    plate_col = 'Plate' if 'Plate' in filtered.columns else (
        'Plate Number' if 'Plate Number' in filtered.columns else None
    )
    if plate_col:
        unique_plates = filtered[plate_col].nunique()
    else:
        unique_plates = "N/A"

    return dbc.Row([
        dbc.Col(kpi_card("Unique Plates", f"{unique_plates:,}", "bi-car-front-fill", "#98ACED"), width=3),
        dbc.Col(kpi_card("Total Violations", f"{total_viol:,}", "bi-exclamation-triangle-fill", "#d6e80d"), width=3),
        dbc.Col(kpi_card("Total Fine Amount", f"${total_fine:,.0f}", "bi-cash-coin", "#3FD927"), width=3),
        dbc.Col(kpi_card("Unpaid Amount", f"${unpaid:,.0f}", "bi-credit-card-2-front-fill", "#246FDF"), width=3),
    ], className="justify-content-center")

@app.callback(
    Output("tab-content", "children"),
    Input("state-select", "value"),
    Input("violation-select", "value"),
    Input("tabs", "value")
)
def update_tab_charts(state, violation, tab):
    filtered = filter_all(df, state, violation)
    card_style = {
        "backgroundColor": "transparent",
        "borderRadius": "18px",
        "border": "none",
        "boxShadow": "none"
    }
    if tab == "tab-1":
        top_viol = filtered['Violation'].value_counts().nlargest(8)
        bar = px.bar(
            x=top_viol.index,
            y=top_viol.values,
            labels={'x': 'Violation Type', 'y': 'Count'},
            title='Violation Types by Count',
            template='plotly_dark',
            color_discrete_sequence=YELLOW_PALETTE
        )
        # Reverse days order for heatmap
        days_order = [
            'Sunday', 'Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday'
        ]
        heatmap_data = (
            filtered
            .dropna(subset=['Day of Week', 'Violation Hour'])
            .groupby(['Day of Week', 'Violation Hour'])
            .size()
            .reset_index(name='Count')
        )
        heatmap_data['Day of Week'] = pd.Categorical(
            heatmap_data['Day of Week'], categories=days_order, ordered=True
        )
        heatmap_data = heatmap_data.sort_values(['Day of Week', 'Violation Hour'])
        heatmap = px.density_heatmap(
            heatmap_data,
            x='Violation Hour',
            y='Day of Week',
            z='Count',
            color_continuous_scale='Viridis',
            title='Violations Heatmap by Day and Hour',
            nbinsx=24,
            template='plotly_dark'
        )
        heatmap.update_xaxes(dtick=1)
        return dbc.Row([
            dbc.Col(
                dbc.Card(
                    dcc.Graph(figure=bar),
                    body=True,
                    style=card_style
                ), width=6
            ),
            dbc.Col(
                dbc.Card(
                    dcc.Graph(figure=heatmap),
                    body=True,
                    style=card_style
                ), width=6
            ),
        ])
    elif tab == "tab-2":
        pie = px.pie(
            filtered,
            names='Violation Status',
            title='Violation Status Distribution',
            hole=0.8,
            template='plotly_dark'
        )
        if 'Fine Amount' in filtered.columns and 'Amount Due' in filtered.columns:
            scatter = px.scatter(
                filtered,
                x='Fine Amount',
                y='Amount Due',
                color='Violation Status',
                title='Fine Amount vs. Amount Due',
                template='plotly_dark'
            )
            scatter.update_traces(marker=dict(size=14))
        else:
            scatter = px.scatter(
                pd.DataFrame({'x': [], 'y': []}),
                x='x', y='y',
                title='Fine Amount vs. Amount Due',
                template='plotly_dark'
            )
        return dbc.Row([
            dbc.Col(
                dbc.Card(
                    dcc.Graph(figure=pie),
                    body=True,
                    style=card_style
                ), width=6
            ),
            dbc.Col(
                dbc.Card(
                    dcc.Graph(figure=scatter),
                    body=True,
                    style=card_style
                ), width=6
            ),
        ])
    elif tab == "tab-3":
        return make_aggrid(filtered)
    else:
        return html.Div("Select a tab.", style={"color": "white"})

if __name__ == "__main__":
    app.run()



4 Likes

Hi all, I would like to ask, where the status is blank, could I write that there is no data, or rather that it is in progress? /because I wrote unknown/ :thinking:

1 Like