refactor: update app code for domain-scoped schema migration
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled

- Update dbt model references to use new schema naming (stg_toronto, int_toronto, mart_toronto)
- Refactor figure factories to use consistent column naming from new schema
- Update callbacks to work with refactored data structures
- Add centralized design tokens module for consistent styling
- Streamline CLAUDE.md documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 17:00:30 -05:00
parent cda2a078d9
commit dfa5f92d8a
21 changed files with 618 additions and 407 deletions

View File

@@ -28,7 +28,7 @@ def create_metric_selector(
label=label,
data=options,
value=default_value or (options[0]["value"] if options else None),
style={"width": "200px"},
w=200,
)
@@ -64,7 +64,7 @@ def create_map_controls(
id=f"{id_prefix}-layer-toggle",
label="Show Boundaries",
checked=True,
style={"marginTop": "10px"},
mt="sm",
)
)

View File

@@ -38,7 +38,7 @@ def create_year_selector(
label=label,
data=options,
value=str(default_year),
style={"width": "120px"},
w=120,
)
@@ -83,7 +83,8 @@ def create_time_slider(
marks=marks,
step=1,
minRange=1,
style={"marginTop": "20px", "marginBottom": "10px"},
mt="md",
mb="sm",
),
],
p="md",
@@ -131,5 +132,5 @@ def create_month_selector(
label=label,
data=options,
value=str(default_month),
style={"width": "140px"},
w=140,
)

View File

@@ -0,0 +1,48 @@
"""Design system tokens and utilities."""
from .tokens import (
CHART_PALETTE,
COLOR_ACCENT,
COLOR_NEGATIVE,
COLOR_POSITIVE,
COLOR_WARNING,
GRID_COLOR,
GRID_COLOR_DARK,
PALETTE_COMPARISON,
PALETTE_GENDER,
PALETTE_TREND,
PAPER_BG,
PLOT_BG,
POLICY_COLORS,
TEXT_MUTED,
TEXT_PRIMARY,
TEXT_SECONDARY,
get_colorbar_defaults,
get_default_layout,
)
__all__ = [
# Text colors
"TEXT_PRIMARY",
"TEXT_SECONDARY",
"TEXT_MUTED",
# Chart backgrounds
"GRID_COLOR",
"GRID_COLOR_DARK",
"PAPER_BG",
"PLOT_BG",
# Semantic colors
"COLOR_POSITIVE",
"COLOR_NEGATIVE",
"COLOR_WARNING",
"COLOR_ACCENT",
# Palettes
"CHART_PALETTE",
"PALETTE_COMPARISON",
"PALETTE_GENDER",
"PALETTE_TREND",
"POLICY_COLORS",
# Utility functions
"get_default_layout",
"get_colorbar_defaults",
]

View File

@@ -0,0 +1,162 @@
"""Centralized design tokens for consistent styling across the application.
This module provides a single source of truth for colors, ensuring:
- Consistent styling across all Plotly figures and components
- Accessibility compliance (WCAG color contrast)
- Easy theme updates without hunting through multiple files
Usage:
from portfolio_app.design import TEXT_PRIMARY, CHART_PALETTE
fig.update_layout(font_color=TEXT_PRIMARY)
"""
from typing import Any
# =============================================================================
# TEXT COLORS (Dark Theme)
# =============================================================================
TEXT_PRIMARY = "#c9c9c9"
"""Primary text color for labels, titles, and body text."""
TEXT_SECONDARY = "#888888"
"""Secondary text color for subtitles, captions, and muted text."""
TEXT_MUTED = "#666666"
"""Muted text color for disabled states and placeholders."""
# =============================================================================
# CHART BACKGROUND & GRID
# =============================================================================
GRID_COLOR = "rgba(128, 128, 128, 0.2)"
"""Standard grid line color with transparency."""
GRID_COLOR_DARK = "rgba(128, 128, 128, 0.3)"
"""Darker grid for radar charts and polar plots."""
PAPER_BG = "rgba(0, 0, 0, 0)"
"""Transparent paper background for charts."""
PLOT_BG = "rgba(0, 0, 0, 0)"
"""Transparent plot background for charts."""
# =============================================================================
# SEMANTIC COLORS
# =============================================================================
COLOR_POSITIVE = "#40c057"
"""Positive/success indicator (Mantine green-6)."""
COLOR_NEGATIVE = "#fa5252"
"""Negative/error indicator (Mantine red-6)."""
COLOR_WARNING = "#fab005"
"""Warning indicator (Mantine yellow-6)."""
COLOR_ACCENT = "#228be6"
"""Primary accent color (Mantine blue-6)."""
# =============================================================================
# ACCESSIBLE CHART PALETTE
# =============================================================================
# Okabe-Ito palette - optimized for all color vision deficiencies
# Reference: https://jfly.uni-koeln.de/color/
CHART_PALETTE = [
"#0072B2", # Blue (primary data series)
"#E69F00", # Orange
"#56B4E9", # Sky blue
"#009E73", # Teal/green
"#F0E442", # Yellow
"#D55E00", # Vermillion
"#CC79A7", # Pink
"#000000", # Black (use sparingly)
]
"""
Accessible categorical palette (Okabe-Ito).
Distinguishable for deuteranopia, protanopia, and tritanopia.
Use indices 0-6 for most charts; index 7 (black) for emphasis only.
"""
# Semantic subsets for specific use cases
PALETTE_COMPARISON = [CHART_PALETTE[0], CHART_PALETTE[1]]
"""Two-color palette for A/B comparisons."""
PALETTE_GENDER = {
"male": "#56B4E9", # Sky blue
"female": "#CC79A7", # Pink
}
"""Gender-specific colors (accessible contrast)."""
PALETTE_TREND = {
"positive": COLOR_POSITIVE,
"negative": COLOR_NEGATIVE,
"neutral": TEXT_SECONDARY,
}
"""Trend indicator colors for sparklines and deltas."""
# =============================================================================
# POLICY/EVENT MARKERS (Time Series)
# =============================================================================
POLICY_COLORS = {
"policy_change": "#E69F00", # Orange - policy changes
"major_event": "#D55E00", # Vermillion - major events
"data_note": "#56B4E9", # Sky blue - data annotations
"forecast": "#009E73", # Teal - forecast periods
"highlight": "#F0E442", # Yellow - highlighted regions
}
"""Colors for policy markers and event annotations on time series."""
# =============================================================================
# CHART LAYOUT DEFAULTS
# =============================================================================
def get_default_layout() -> dict[str, Any]:
"""Return default Plotly layout settings with design tokens.
Returns:
dict: Layout configuration for fig.update_layout()
Example:
fig.update_layout(**get_default_layout())
"""
return {
"paper_bgcolor": PAPER_BG,
"plot_bgcolor": PLOT_BG,
"font": {"color": TEXT_PRIMARY},
"title": {"font": {"color": TEXT_PRIMARY}},
"legend": {"font": {"color": TEXT_PRIMARY}},
"xaxis": {
"gridcolor": GRID_COLOR,
"linecolor": GRID_COLOR,
"tickfont": {"color": TEXT_PRIMARY},
"title": {"font": {"color": TEXT_PRIMARY}},
},
"yaxis": {
"gridcolor": GRID_COLOR,
"linecolor": GRID_COLOR,
"tickfont": {"color": TEXT_PRIMARY},
"title": {"font": {"color": TEXT_PRIMARY}},
},
}
def get_colorbar_defaults() -> dict[str, Any]:
"""Return default colorbar settings with design tokens.
Returns:
dict: Colorbar configuration for choropleth/heatmap traces
"""
return {
"tickfont": {"color": TEXT_PRIMARY},
"title": {"font": {"color": TEXT_PRIMARY}},
}

View File

@@ -6,6 +6,17 @@ import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from portfolio_app.design import (
CHART_PALETTE,
COLOR_NEGATIVE,
COLOR_POSITIVE,
GRID_COLOR,
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
def create_ranking_bar(
data: list[dict[str, Any]],
@@ -14,8 +25,8 @@ def create_ranking_bar(
title: str | None = None,
top_n: int = 10,
bottom_n: int = 10,
color_top: str = "#4CAF50",
color_bottom: str = "#F44336",
color_top: str = COLOR_POSITIVE,
color_bottom: str = COLOR_NEGATIVE,
value_format: str = ",.0f",
) -> go.Figure:
"""Create horizontal bar chart showing top and bottom rankings.
@@ -87,10 +98,10 @@ def create_ranking_bar(
barmode="group",
showlegend=True,
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02},
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR, "title": None},
yaxis={"autorange": "reversed", "title": None},
margin={"l": 10, "r": 10, "t": 40, "b": 10},
)
@@ -126,10 +137,10 @@ def create_stacked_bar(
df = pd.DataFrame(data)
# Default color scheme
# Default color scheme using accessible palette
if color_map is None:
categories = df[category_column].unique()
colors = px.colors.qualitative.Set2[: len(categories)]
colors = CHART_PALETTE[: len(categories)]
color_map = dict(zip(categories, colors, strict=False))
fig = px.bar(
@@ -147,11 +158,11 @@ def create_stacked_bar(
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
yaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR, "title": None},
yaxis={"gridcolor": GRID_COLOR, "title": None},
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02},
margin={"l": 10, "r": 10, "t": 60, "b": 10},
)
@@ -164,7 +175,7 @@ def create_horizontal_bar(
name_column: str,
value_column: str,
title: str | None = None,
color: str = "#2196F3",
color: str = CHART_PALETTE[0],
value_format: str = ",.0f",
sort: bool = True,
) -> go.Figure:
@@ -204,10 +215,10 @@ def create_horizontal_bar(
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR, "title": None},
yaxis={"title": None},
margin={"l": 10, "r": 10, "t": 40, "b": 10},
)
@@ -225,13 +236,13 @@ def _create_empty_figure(title: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"visible": False},
yaxis={"visible": False},
)

View File

@@ -5,6 +5,13 @@ from typing import Any
import plotly.express as px
import plotly.graph_objects as go
from portfolio_app.design import (
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
def create_choropleth_figure(
geojson: dict[str, Any] | None,
@@ -55,9 +62,9 @@ def create_choropleth_figure(
margin={"l": 0, "r": 0, "t": 40, "b": 0},
title=title or "Toronto Housing Map",
height=500,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
)
fig.add_annotation(
text="No geometry data available. Complete QGIS digitization to enable map.",
@@ -66,7 +73,7 @@ def create_choropleth_figure(
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
return fig
@@ -98,17 +105,17 @@ def create_choropleth_figure(
margin={"l": 0, "r": 0, "t": 40, "b": 0},
title=title,
height=500,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
coloraxis_colorbar={
"title": {
"text": color_column.replace("_", " ").title(),
"font": {"color": "#c9c9c9"},
"font": {"color": TEXT_PRIMARY},
},
"thickness": 15,
"len": 0.7,
"tickfont": {"color": "#c9c9c9"},
"tickfont": {"color": TEXT_PRIMARY},
},
)

View File

@@ -5,6 +5,16 @@ from typing import Any
import pandas as pd
import plotly.graph_objects as go
from portfolio_app.design import (
CHART_PALETTE,
GRID_COLOR,
PALETTE_GENDER,
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
def create_age_pyramid(
data: list[dict[str, Any]],
@@ -52,7 +62,7 @@ def create_age_pyramid(
x=male_values_neg,
orientation="h",
name="Male",
marker_color="#2196F3",
marker_color=PALETTE_GENDER["male"],
hovertemplate="%{y}<br>Male: %{customdata:,}<extra></extra>",
customdata=male_values,
)
@@ -65,7 +75,7 @@ def create_age_pyramid(
x=female_values,
orientation="h",
name="Female",
marker_color="#E91E63",
marker_color=PALETTE_GENDER["female"],
hovertemplate="%{y}<br>Female: %{x:,}<extra></extra>",
)
)
@@ -77,12 +87,12 @@ def create_age_pyramid(
title=title,
barmode="overlay",
bargap=0.1,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={
"title": "Population",
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
"range": [-max_val * 1.1, max_val * 1.1],
"tickvals": [-max_val, -max_val / 2, 0, max_val / 2, max_val],
"ticktext": [
@@ -93,7 +103,7 @@ def create_age_pyramid(
f"{max_val:,.0f}",
],
},
yaxis={"title": None, "gridcolor": "rgba(128,128,128,0.2)"},
yaxis={"title": None, "gridcolor": GRID_COLOR},
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02},
margin={"l": 10, "r": 10, "t": 60, "b": 10},
)
@@ -127,17 +137,9 @@ def create_donut_chart(
df = pd.DataFrame(data)
# Use accessible palette by default
if colors is None:
colors = [
"#2196F3",
"#4CAF50",
"#FF9800",
"#E91E63",
"#9C27B0",
"#00BCD4",
"#FFC107",
"#795548",
]
colors = CHART_PALETTE
fig = go.Figure(
go.Pie(
@@ -153,8 +155,8 @@ def create_donut_chart(
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
font_color=TEXT_PRIMARY,
showlegend=False,
margin={"l": 10, "r": 10, "t": 60, "b": 10},
)
@@ -167,7 +169,7 @@ def create_income_distribution(
bracket_column: str,
count_column: str,
title: str | None = None,
color: str = "#4CAF50",
color: str = CHART_PALETTE[3], # Teal
) -> go.Figure:
"""Create histogram-style bar chart for income distribution.
@@ -199,17 +201,17 @@ def create_income_distribution(
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={
"title": "Income Bracket",
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
"tickangle": -45,
},
yaxis={
"title": "Households",
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
},
margin={"l": 10, "r": 10, "t": 60, "b": 80},
)
@@ -227,13 +229,13 @@ def _create_empty_figure(title: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"visible": False},
yaxis={"visible": False},
)

View File

@@ -4,6 +4,14 @@ from typing import Any
import plotly.graph_objects as go
from portfolio_app.design import (
CHART_PALETTE,
GRID_COLOR_DARK,
PAPER_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
def create_radar_figure(
data: list[dict[str, Any]],
@@ -32,16 +40,9 @@ def create_radar_figure(
if not data or not metrics:
return _create_empty_figure(title or "Radar Chart")
# Default colors
# Use accessible palette by default
if colors is None:
colors = [
"#2196F3",
"#4CAF50",
"#FF9800",
"#E91E63",
"#9C27B0",
"#00BCD4",
]
colors = CHART_PALETTE
fig = go.Figure()
@@ -78,19 +79,19 @@ def create_radar_figure(
polar={
"radialaxis": {
"visible": True,
"gridcolor": "rgba(128,128,128,0.3)",
"linecolor": "rgba(128,128,128,0.3)",
"tickfont": {"color": "#c9c9c9"},
"gridcolor": GRID_COLOR_DARK,
"linecolor": GRID_COLOR_DARK,
"tickfont": {"color": TEXT_PRIMARY},
},
"angularaxis": {
"gridcolor": "rgba(128,128,128,0.3)",
"linecolor": "rgba(128,128,128,0.3)",
"tickfont": {"color": "#c9c9c9"},
"gridcolor": GRID_COLOR_DARK,
"linecolor": GRID_COLOR_DARK,
"tickfont": {"color": TEXT_PRIMARY},
},
"bgcolor": "rgba(0,0,0,0)",
"bgcolor": PAPER_BG,
},
paper_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
font_color=TEXT_PRIMARY,
showlegend=len(data) > 1,
legend={"orientation": "h", "yanchor": "bottom", "y": -0.2},
margin={"l": 40, "r": 40, "t": 60, "b": 40},
@@ -133,7 +134,7 @@ def create_comparison_radar(
metrics=metrics,
name_column="__name__",
title=title,
colors=["#4CAF50", "#9E9E9E"],
colors=[CHART_PALETTE[3], TEXT_SECONDARY], # Teal for selected, gray for avg
)
@@ -156,11 +157,11 @@ def _create_empty_figure(title: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
font_color=TEXT_PRIMARY,
)
return fig

View File

@@ -6,6 +6,15 @@ import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from portfolio_app.design import (
CHART_PALETTE,
GRID_COLOR,
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
def create_scatter_figure(
data: list[dict[str, Any]],
@@ -72,21 +81,21 @@ def create_scatter_figure(
if trendline:
fig.update_traces(
selector={"mode": "lines"},
line={"color": "#FF9800", "dash": "dash", "width": 2},
line={"color": CHART_PALETTE[1], "dash": "dash", "width": 2},
)
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
"title": x_title or x_column.replace("_", " ").title(),
"zeroline": False,
},
yaxis={
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
"title": y_title or y_column.replace("_", " ").title(),
"zeroline": False,
},
@@ -140,19 +149,20 @@ def create_bubble_chart(
hover_name=name_column,
size_max=size_max,
opacity=0.7,
color_discrete_sequence=CHART_PALETTE,
)
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
"title": x_title or x_column.replace("_", " ").title(),
},
yaxis={
"gridcolor": "rgba(128,128,128,0.2)",
"gridcolor": GRID_COLOR,
"title": y_title or y_column.replace("_", " ").title(),
},
margin={"l": 10, "r": 10, "t": 40, "b": 10},
@@ -171,13 +181,13 @@ def _create_empty_figure(title: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"visible": False},
yaxis={"visible": False},
)

View File

@@ -4,6 +4,14 @@ from typing import Any
import plotly.graph_objects as go
from portfolio_app.design import (
COLOR_NEGATIVE,
COLOR_POSITIVE,
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
)
def create_metric_card_figure(
value: float | int | str,
@@ -59,8 +67,12 @@ def create_metric_card_figure(
"relative": False,
"valueformat": ".1f",
"suffix": delta_suffix,
"increasing": {"color": "green" if positive_is_good else "red"},
"decreasing": {"color": "red" if positive_is_good else "green"},
"increasing": {
"color": COLOR_POSITIVE if positive_is_good else COLOR_NEGATIVE
},
"decreasing": {
"color": COLOR_NEGATIVE if positive_is_good else COLOR_POSITIVE
},
}
fig.add_trace(go.Indicator(**indicator_config))
@@ -68,9 +80,9 @@ def create_metric_card_figure(
fig.update_layout(
height=120,
margin={"l": 20, "r": 20, "t": 40, "b": 20},
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font={"family": "Inter, sans-serif", "color": "#c9c9c9"},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font={"family": "Inter, sans-serif", "color": TEXT_PRIMARY},
)
return fig

View File

@@ -5,6 +5,15 @@ from typing import Any
import plotly.express as px
import plotly.graph_objects as go
from portfolio_app.design import (
CHART_PALETTE,
GRID_COLOR,
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
def create_price_time_series(
data: list[dict[str, Any]],
@@ -38,14 +47,14 @@ def create_price_time_series(
x=0.5,
y=0.5,
showarrow=False,
font={"color": "#888888"},
font={"color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
height=350,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
)
return fig
@@ -59,6 +68,7 @@ def create_price_time_series(
y=price_column,
color=group_column,
title=title,
color_discrete_sequence=CHART_PALETTE,
)
else:
fig = px.line(
@@ -67,6 +77,7 @@ def create_price_time_series(
y=price_column,
title=title,
)
fig.update_traces(line_color=CHART_PALETTE[0])
fig.update_layout(
height=350,
@@ -76,11 +87,11 @@ def create_price_time_series(
yaxis_tickprefix="$",
yaxis_tickformat=",",
hovermode="x unified",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR, "linecolor": GRID_COLOR},
yaxis={"gridcolor": GRID_COLOR, "linecolor": GRID_COLOR},
)
return fig
@@ -118,14 +129,14 @@ def create_volume_time_series(
x=0.5,
y=0.5,
showarrow=False,
font={"color": "#888888"},
font={"color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
height=350,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
)
return fig
@@ -140,6 +151,7 @@ def create_volume_time_series(
y=volume_column,
color=group_column,
title=title,
color_discrete_sequence=CHART_PALETTE,
)
else:
fig = px.bar(
@@ -148,6 +160,7 @@ def create_volume_time_series(
y=volume_column,
title=title,
)
fig.update_traces(marker_color=CHART_PALETTE[0])
else:
if group_column and group_column in df.columns:
fig = px.line(
@@ -156,6 +169,7 @@ def create_volume_time_series(
y=volume_column,
color=group_column,
title=title,
color_discrete_sequence=CHART_PALETTE,
)
else:
fig = px.line(
@@ -164,6 +178,7 @@ def create_volume_time_series(
y=volume_column,
title=title,
)
fig.update_traces(line_color=CHART_PALETTE[0])
fig.update_layout(
height=350,
@@ -172,11 +187,11 @@ def create_volume_time_series(
yaxis_title=volume_column.replace("_", " ").title(),
yaxis_tickformat=",",
hovermode="x unified",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR, "linecolor": GRID_COLOR},
yaxis={"gridcolor": GRID_COLOR, "linecolor": GRID_COLOR},
)
return fig
@@ -211,14 +226,14 @@ def create_market_comparison_chart(
x=0.5,
y=0.5,
showarrow=False,
font={"color": "#888888"},
font={"color": TEXT_SECONDARY},
)
fig.update_layout(
title=title,
height=400,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
)
return fig
@@ -230,8 +245,6 @@ def create_market_comparison_chart(
fig = make_subplots(specs=[[{"secondary_y": True}]])
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]
for i, metric in enumerate(metrics[:4]):
if metric not in df.columns:
continue
@@ -242,7 +255,7 @@ def create_market_comparison_chart(
x=df[date_column],
y=df[metric],
name=metric.replace("_", " ").title(),
line={"color": colors[i % len(colors)]},
line={"color": CHART_PALETTE[i % len(CHART_PALETTE)]},
),
secondary_y=secondary,
)
@@ -252,18 +265,18 @@ def create_market_comparison_chart(
height=400,
margin={"l": 40, "r": 40, "t": 50, "b": 40},
hovermode="x unified",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR, "linecolor": GRID_COLOR},
yaxis={"gridcolor": GRID_COLOR, "linecolor": GRID_COLOR},
legend={
"orientation": "h",
"yanchor": "bottom",
"y": 1.02,
"xanchor": "right",
"x": 1,
"font": {"color": "#c9c9c9"},
"font": {"color": TEXT_PRIMARY},
},
)
@@ -290,13 +303,13 @@ def add_policy_markers(
if not policy_events:
return fig
# Color mapping for policy categories
# Color mapping for policy categories using design tokens
category_colors = {
"monetary": "#1f77b4", # Blue
"tax": "#2ca02c", # Green
"regulatory": "#ff7f0e", # Orange
"supply": "#9467bd", # Purple
"economic": "#d62728", # Red
"monetary": CHART_PALETTE[0], # Blue
"tax": CHART_PALETTE[3], # Teal/green
"regulatory": CHART_PALETTE[1], # Orange
"supply": CHART_PALETTE[6], # Pink
"economic": CHART_PALETTE[5], # Vermillion
}
# Symbol mapping for expected direction
@@ -313,7 +326,7 @@ def add_policy_markers(
title = event.get("title", "Policy Event")
level = event.get("level", "federal")
color = category_colors.get(category, "#666666")
color = category_colors.get(category, TEXT_SECONDARY)
symbol = direction_symbols.get(direction, "circle")
# Add vertical line for the event
@@ -335,7 +348,7 @@ def add_policy_markers(
"symbol": symbol,
"size": 12,
"color": color,
"line": {"width": 1, "color": "white"},
"line": {"width": 1, "color": TEXT_PRIMARY},
},
name=title,
hovertemplate=(

View File

@@ -5,6 +5,14 @@ import pandas as pd
import plotly.graph_objects as go
from dash import Input, Output, callback
from portfolio_app.design import (
CHART_PALETTE,
GRID_COLOR,
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
from portfolio_app.figures.toronto import (
create_donut_chart,
create_horizontal_bar,
@@ -109,18 +117,18 @@ def update_housing_trend(year: str, neighbourhood_id: int | None) -> go.Figure:
x=[d["year"] for d in data],
y=[d["avg_rent"] for d in data],
mode="lines+markers",
line={"color": "#2196F3", "width": 2},
line={"color": CHART_PALETTE[0], "width": 2},
marker={"size": 8},
name="City Average",
)
)
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "rgba(128,128,128,0.2)"},
yaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": "Avg Rent (2BR)"},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR},
yaxis={"gridcolor": GRID_COLOR, "title": "Avg Rent (2BR)"},
showlegend=False,
margin={"l": 40, "r": 10, "t": 10, "b": 30},
)
@@ -153,7 +161,7 @@ def update_housing_types(year: str) -> go.Figure:
data=data,
name_column="type",
value_column="percentage",
colors=["#4CAF50", "#2196F3"],
colors=[CHART_PALETTE[3], CHART_PALETTE[0]], # Teal for owner, blue for renter
)
@@ -178,19 +186,19 @@ def update_safety_trend(year: str) -> go.Figure:
x=[d["year"] for d in data],
y=[d["crime_rate"] for d in data],
mode="lines+markers",
line={"color": "#FF5722", "width": 2},
line={"color": CHART_PALETTE[5], "width": 2}, # Vermillion
marker={"size": 8},
fill="tozeroy",
fillcolor="rgba(255,87,34,0.1)",
fillcolor="rgba(213, 94, 0, 0.1)", # Vermillion with opacity
)
)
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "rgba(128,128,128,0.2)"},
yaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": "Crime Rate per 100K"},
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"gridcolor": GRID_COLOR},
yaxis={"gridcolor": GRID_COLOR, "title": "Crime Rate per 100K"},
showlegend=False,
margin={"l": 40, "r": 10, "t": 10, "b": 30},
)
@@ -233,7 +241,7 @@ def update_safety_types(year: str) -> go.Figure:
data=data,
name_column="category",
value_column="count",
color="#FF5722",
color=CHART_PALETTE[5], # Vermillion for crime
)
@@ -264,7 +272,11 @@ def update_demographics_age(year: str) -> go.Figure:
data=data,
name_column="age_group",
value_column="percentage",
colors=["#9C27B0", "#673AB7", "#3F51B5"],
colors=[
CHART_PALETTE[2],
CHART_PALETTE[0],
CHART_PALETTE[4],
], # Sky, Blue, Yellow
)
@@ -301,7 +313,7 @@ def update_demographics_income(year: str) -> go.Figure:
data=data,
name_column="bracket",
value_column="count",
color="#4CAF50",
color=CHART_PALETTE[3], # Teal
sort=False,
)
@@ -333,7 +345,7 @@ def update_amenities_breakdown(year: str) -> go.Figure:
data=data,
name_column="type",
value_column="count",
color="#4CAF50",
color=CHART_PALETTE[3], # Teal
)
@@ -387,9 +399,9 @@ def _empty_chart(message: str) -> go.Figure:
"""Create an empty chart with a message."""
fig = go.Figure()
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"visible": False},
yaxis={"visible": False},
)
@@ -400,6 +412,6 @@ def _empty_chart(message: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
return fig

View File

@@ -4,6 +4,12 @@
import plotly.graph_objects as go
from dash import Input, Output, State, callback, no_update
from portfolio_app.design import (
PAPER_BG,
PLOT_BG,
TEXT_PRIMARY,
TEXT_SECONDARY,
)
from portfolio_app.figures.toronto import create_choropleth_figure, create_ranking_bar
from portfolio_app.toronto.services import (
get_amenities_data,
@@ -267,8 +273,8 @@ def _empty_map(message: str) -> go.Figure:
"zoom": 9.5,
},
margin={"l": 0, "r": 0, "t": 0, "b": 0},
paper_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
font_color=TEXT_PRIMARY,
)
fig.add_annotation(
text=message,
@@ -277,7 +283,7 @@ def _empty_map(message: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
return fig
@@ -286,9 +292,9 @@ def _empty_chart(message: str) -> go.Figure:
"""Create an empty chart with a message."""
fig = go.Figure()
fig.update_layout(
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
paper_bgcolor=PAPER_BG,
plot_bgcolor=PLOT_BG,
font_color=TEXT_PRIMARY,
xaxis={"visible": False},
yaxis={"visible": False},
)
@@ -299,6 +305,6 @@ def _empty_chart(message: str) -> go.Figure:
x=0.5,
y=0.5,
showarrow=False,
font={"size": 14, "color": "#888888"},
font={"size": 14, "color": TEXT_SECONDARY},
)
return fig