{"spec_id":"timeseries-decomposition","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ntimeseries-decomposition: Time Series Decomposition Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\nimport sys\n\n\n# Fix sys.path to avoid circular import: remove current dir and script location\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir and os.path.abspath(p) != os.getcwd()]\nsys.path.insert(0, \"/usr/lib/python3.13\")\nsys.path.insert(0, \"/usr/local/lib/python3.13/dist-packages\")\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom statsmodels.tsa.seasonal import seasonal_decompose\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Okabe-Ito palette (component colors)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data - Monthly airline passengers\nnp.random.seed(42)\ndates = pd.date_range(\"2018-01-01\", periods=96, freq=\"MS\")\ntrend = np.linspace(100, 180, 96)\nseasonal = 30 * np.sin(2 * np.pi * np.arange(96) / 12)\nnoise = np.random.normal(0, 8, 96)\nvalues = trend + seasonal + noise\n\n# Decompose time series\nseries = pd.Series(values, index=dates)\ndecomposition = seasonal_decompose(series, model=\"additive\", period=12)\n\n# Create dataframe with all components\ndf_decomp = pd.DataFrame(\n    {\n        \"date\": dates,\n        \"Original\": values,\n        \"Trend\": decomposition.trend,\n        \"Seasonal\": decomposition.seasonal,\n        \"Residual\": decomposition.resid,\n    }\n)\n\n# Melt for faceted plotting\ndf_long = df_decomp.melt(id_vars=[\"date\"], var_name=\"component\", value_name=\"value\")\n\n# Component order\ncomponent_order = [\"Original\", \"Trend\", \"Seasonal\", \"Residual\"]\n\n# Color mapping using Okabe-Ito palette\ncolor_map = {component: IMPRINT[i] for i, component in enumerate(component_order)}\n\n# Base chart with encoding\nbase_chart = (\n    alt.Chart(df_long)\n    .mark_line(strokeWidth=2.5)\n    .encode(\n        x=alt.X(\n            \"date:T\", title=\"Date\", axis=alt.Axis(labelFontSize=18, titleFontSize=22, labelAngle=-45, tickCount=12)\n        ),\n        y=alt.Y(\"value:Q\", title=\"\", axis=alt.Axis(labelFontSize=16, titleFontSize=18)),\n        color=alt.Color(\n            \"component:N\", scale=alt.Scale(domain=component_order, range=list(color_map.values())), legend=None\n        ),\n        tooltip=[\"date:T\", \"value:Q\", \"component:N\"],\n    )\n)\n\n# Create faceted chart with grid lines\nchart = (\n    base_chart.properties(width=1600, height=200)\n    .facet(\n        row=alt.Row(\n            \"component:N\",\n            sort=component_order,\n            title=None,\n            header=alt.Header(\n                labelFontSize=22, labelFontWeight=\"bold\", labelOrient=\"left\", labelAlign=\"left\", labelPadding=10\n            ),\n        )\n    )\n    .properties(title=alt.Title(\"timeseries-decomposition · altair · anyplot.ai\", fontSize=28, anchor=\"middle\", dy=-10))\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_facet(spacing=20)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.12, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_title(color=INK, fontSize=28)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .resolve_scale(y=\"independent\")\n    .interactive()\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}