Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +23 -12
- app.py +273 -0
- pipeline.html +1072 -0
- requirements.txt +7 -0
README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Auto
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
--
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Auto-Ontology — Automotive Market Intelligence
|
| 3 |
+
emoji: 🏎️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "5.31.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: cc-by-4.0
|
| 11 |
+
datasets:
|
| 12 |
+
- cp500/auto-ontology
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Auto-Ontology — Automotive Market Intelligence
|
| 16 |
+
|
| 17 |
+
Explore a hypergraph of **94,671 market signals** connected to **1,261 vehicles** from the NHTSA vPIC registry.
|
| 18 |
+
|
| 19 |
+
**Pipeline tab** — Scrollytelling walkthrough from Common Crawl to structural analysis, with D3.js visualizations.
|
| 20 |
+
|
| 21 |
+
**Chatbot tab** — Ask questions about the automotive market using a Strands Agent backed by the dataset.
|
| 22 |
+
|
| 23 |
+
Built as an AWS Automotive workshop. Dataset: [cp500/auto-ontology](https://huggingface.co/datasets/cp500/auto-ontology)
|
app.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Auto-Ontology — Automotive Market Intelligence
|
| 3 |
+
HuggingFace Space: Gradio Blocks with pipeline narrative + Strands Agent chatbot.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import pandas as pd
|
| 11 |
+
|
| 12 |
+
# ---------------------------------------------------------------------------
|
| 13 |
+
# Load dataset (parquet files from the HF dataset repo)
|
| 14 |
+
# ---------------------------------------------------------------------------
|
| 15 |
+
|
| 16 |
+
DATA_DIR = Path(__file__).parent / "data"
|
| 17 |
+
HF_DATASET = "cp500/auto-ontology"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _load_parquet(subdir: str, table: str) -> pd.DataFrame | None:
|
| 21 |
+
"""Load a parquet table — try local first, then HF hub."""
|
| 22 |
+
local = DATA_DIR / subdir / f"{table}.parquet"
|
| 23 |
+
if local.exists():
|
| 24 |
+
return pd.read_parquet(local)
|
| 25 |
+
try:
|
| 26 |
+
return pd.read_parquet(f"hf://datasets/{HF_DATASET}/data/{subdir}/{table}.parquet")
|
| 27 |
+
except Exception:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Lazy-load dataframes
|
| 32 |
+
_cache: dict[str, pd.DataFrame | None] = {}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_df(subdir: str, table: str) -> pd.DataFrame:
|
| 36 |
+
key = f"{subdir}/{table}"
|
| 37 |
+
if key not in _cache:
|
| 38 |
+
_cache[key] = _load_parquet(subdir, table)
|
| 39 |
+
df = _cache[key]
|
| 40 |
+
if df is None:
|
| 41 |
+
raise ValueError(f"Table {key} not available")
|
| 42 |
+
return df
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# Strands Agent tools — query the parquet dataset
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
from strands import Agent, tool
|
| 50 |
+
from strands.models.openai import OpenAIModel
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@tool
|
| 54 |
+
def search_products(make: str = "", model: str = "", year: int = 0) -> str:
|
| 55 |
+
"""Search the product index for vehicles by make, model, and/or year.
|
| 56 |
+
Returns matching vehicles with their IDs, make, model, year, and body class."""
|
| 57 |
+
df = get_df("hypergraph", "product_index")
|
| 58 |
+
mask = pd.Series(True, index=df.index)
|
| 59 |
+
if make:
|
| 60 |
+
mask &= df["make"].str.contains(make, case=False, na=False)
|
| 61 |
+
if model:
|
| 62 |
+
mask &= df["model"].str.contains(model, case=False, na=False)
|
| 63 |
+
if year:
|
| 64 |
+
mask &= df["model_year"] == year
|
| 65 |
+
results = df[mask].head(20)
|
| 66 |
+
if results.empty:
|
| 67 |
+
return "No products found matching the criteria."
|
| 68 |
+
return results.to_markdown(index=False)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@tool
|
| 72 |
+
def browse_signals(domain: str = "", sentiment: str = "", keyword: str = "") -> str:
|
| 73 |
+
"""Browse market signals. Filter by L1 domain code (P/T/C/F/S/R/M/ST),
|
| 74 |
+
sentiment (bullish/bearish/neutral/mixed), or keyword in signal name.
|
| 75 |
+
Returns up to 15 matching signals."""
|
| 76 |
+
si = get_df("hypergraph", "signal_index")
|
| 77 |
+
nodes = get_df("hypergraph", "nodes")
|
| 78 |
+
# Merge to get signal names
|
| 79 |
+
signals = si.merge(nodes[nodes["node_type"] == "Signal"][["id", "name"]],
|
| 80 |
+
left_on="signal_id", right_on="id", how="left")
|
| 81 |
+
mask = pd.Series(True, index=signals.index)
|
| 82 |
+
if domain:
|
| 83 |
+
mask &= signals["domain"].str.upper() == domain.upper()
|
| 84 |
+
if sentiment:
|
| 85 |
+
mask &= signals["sentiment"].str.lower() == sentiment.lower()
|
| 86 |
+
if keyword:
|
| 87 |
+
mask &= signals["name"].str.contains(keyword, case=False, na=False)
|
| 88 |
+
results = signals[mask][["signal_id", "name", "domain", "subdomain",
|
| 89 |
+
"sentiment", "impact", "timestamp"]].head(15)
|
| 90 |
+
if results.empty:
|
| 91 |
+
return "No signals found matching the criteria."
|
| 92 |
+
return results.to_markdown(index=False)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@tool
|
| 96 |
+
def get_competitors(product_id: str) -> str:
|
| 97 |
+
"""Get vehicles that compete with a given product.
|
| 98 |
+
Takes a product_id like 'prd_tesla_model_y_2024' and returns competing vehicles."""
|
| 99 |
+
edges = get_df("hypergraph", "edges")
|
| 100 |
+
products = get_df("hypergraph", "product_index")
|
| 101 |
+
# Find COMPETES_WITH edges in both directions
|
| 102 |
+
compete = edges[edges["role"] == "COMPETES_WITH"]
|
| 103 |
+
as_source = compete[compete["source_id"] == product_id]["target_id"]
|
| 104 |
+
as_target = compete[compete["target_id"] == product_id]["source_id"]
|
| 105 |
+
competitor_ids = pd.concat([as_source, as_target]).unique()
|
| 106 |
+
if len(competitor_ids) == 0:
|
| 107 |
+
return f"No competitors found for {product_id}."
|
| 108 |
+
results = products[products["product_id"].isin(competitor_ids)]
|
| 109 |
+
return f"Competitors of {product_id}:\n\n{results.to_markdown(index=False)}"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@tool
|
| 113 |
+
def graph_stats() -> str:
|
| 114 |
+
"""Get summary statistics of the auto-ontology hypergraph —
|
| 115 |
+
node counts by type, edge counts by role, signal domain distribution, etc."""
|
| 116 |
+
nodes = get_df("hypergraph", "nodes")
|
| 117 |
+
edges = get_df("hypergraph", "edges")
|
| 118 |
+
si = get_df("hypergraph", "signal_index")
|
| 119 |
+
|
| 120 |
+
node_counts = nodes["node_type"].value_counts().to_dict()
|
| 121 |
+
edge_counts = edges["role"].value_counts().to_dict()
|
| 122 |
+
domain_counts = si["domain"].value_counts().to_dict()
|
| 123 |
+
sentiment_counts = si["sentiment"].value_counts().to_dict()
|
| 124 |
+
|
| 125 |
+
lines = [
|
| 126 |
+
"## Hypergraph Statistics\n",
|
| 127 |
+
f"**Total nodes:** {len(nodes):,}",
|
| 128 |
+
f"**Total edges:** {len(edges):,}\n",
|
| 129 |
+
"### Node Types",
|
| 130 |
+
]
|
| 131 |
+
for t, c in sorted(node_counts.items(), key=lambda x: -x[1]):
|
| 132 |
+
lines.append(f"- {t}: {c:,}")
|
| 133 |
+
lines.append("\n### Edge Roles")
|
| 134 |
+
for r, c in sorted(edge_counts.items(), key=lambda x: -x[1]):
|
| 135 |
+
lines.append(f"- {r}: {c:,}")
|
| 136 |
+
lines.append("\n### Signal Domains (L1)")
|
| 137 |
+
domain_names = {
|
| 138 |
+
"P": "Product", "C": "Competitive", "T": "Technology", "M": "Market",
|
| 139 |
+
"F": "Financial", "S": "Supply Chain", "R": "Regulatory", "ST": "Strategic",
|
| 140 |
+
}
|
| 141 |
+
for d, c in sorted(domain_counts.items(), key=lambda x: -x[1]):
|
| 142 |
+
lines.append(f"- {d} ({domain_names.get(d, d)}): {c:,}")
|
| 143 |
+
lines.append("\n### Signal Sentiment")
|
| 144 |
+
for s, c in sorted(sentiment_counts.items(), key=lambda x: -x[1]):
|
| 145 |
+
lines.append(f"- {s}: {c:,}")
|
| 146 |
+
return "\n".join(lines)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ---------------------------------------------------------------------------
|
| 150 |
+
# Build Strands Agent
|
| 151 |
+
# ---------------------------------------------------------------------------
|
| 152 |
+
|
| 153 |
+
SYSTEM_PROMPT = """\
|
| 154 |
+
You are an automotive market intelligence analyst with access to the Auto-Ontology \
|
| 155 |
+
hypergraph — 176K nodes and 537K edges connecting 94,671 market signals to 1,261 vehicles.
|
| 156 |
+
|
| 157 |
+
The data was extracted from Common Crawl and resolved against the NHTSA vPIC registry.
|
| 158 |
+
|
| 159 |
+
Use your tools to search products, browse signals, find competitors, and get graph stats. \
|
| 160 |
+
When answering, cite specific data from the tools. Be concise and analytical.
|
| 161 |
+
|
| 162 |
+
Signal domains: P (Product), T (Technology), C (Competitive), F (Financial), \
|
| 163 |
+
S (Supply Chain), R (Regulatory), M (Market), ST (Strategic).
|
| 164 |
+
|
| 165 |
+
Sentiments: bullish, bearish, neutral, mixed.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def _build_agent():
|
| 170 |
+
"""Build the Strands agent with HF Inference API."""
|
| 171 |
+
hf_token = os.environ.get("HF_TOKEN", "")
|
| 172 |
+
model = OpenAIModel(
|
| 173 |
+
client_args={
|
| 174 |
+
"base_url": "https://api-inference.huggingface.co/v1/",
|
| 175 |
+
"api_key": hf_token,
|
| 176 |
+
},
|
| 177 |
+
model_id="Qwen/Qwen2.5-72B-Instruct",
|
| 178 |
+
)
|
| 179 |
+
return Agent(
|
| 180 |
+
model=model,
|
| 181 |
+
tools=[search_products, browse_signals, get_competitors, graph_stats],
|
| 182 |
+
system_prompt=SYSTEM_PROMPT,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
_agent = None
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def get_agent():
|
| 190 |
+
global _agent
|
| 191 |
+
if _agent is None:
|
| 192 |
+
_agent = _build_agent()
|
| 193 |
+
return _agent
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# ---------------------------------------------------------------------------
|
| 197 |
+
# Chat handler
|
| 198 |
+
# ---------------------------------------------------------------------------
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def chat_fn(message: str, history: list[dict]) -> str:
|
| 202 |
+
"""Handle a chat message using the Strands agent."""
|
| 203 |
+
try:
|
| 204 |
+
agent = get_agent()
|
| 205 |
+
result = agent(message)
|
| 206 |
+
return str(result)
|
| 207 |
+
except Exception as e:
|
| 208 |
+
return f"Error: {e}\n\nMake sure the HF_TOKEN secret is configured in Space settings."
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
# ---------------------------------------------------------------------------
|
| 212 |
+
# Pipeline narrative HTML
|
| 213 |
+
# ---------------------------------------------------------------------------
|
| 214 |
+
|
| 215 |
+
PIPELINE_HTML_PATH = Path(__file__).parent / "pipeline.html"
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def load_pipeline_html() -> str:
|
| 219 |
+
if PIPELINE_HTML_PATH.exists():
|
| 220 |
+
html = PIPELINE_HTML_PATH.read_text()
|
| 221 |
+
# Wrap in iframe for isolation
|
| 222 |
+
import base64
|
| 223 |
+
encoded = base64.b64encode(html.encode()).decode()
|
| 224 |
+
return f'<iframe src="data:text/html;base64,{encoded}" width="100%" height="900" style="border:none; border-radius:12px;"></iframe>'
|
| 225 |
+
return "<p>Pipeline narrative not found. Check pipeline.html.</p>"
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# ---------------------------------------------------------------------------
|
| 229 |
+
# Gradio App
|
| 230 |
+
# ---------------------------------------------------------------------------
|
| 231 |
+
|
| 232 |
+
DESCRIPTION = """\
|
| 233 |
+
# Auto-Ontology — Automotive Market Intelligence
|
| 234 |
+
|
| 235 |
+
Explore a hypergraph of **94,671 market signals** connected to **1,261 vehicles** \
|
| 236 |
+
from the NHTSA vPIC registry. Built from Common Crawl data using an AWS pipeline \
|
| 237 |
+
with NuExtract structured extraction and vPIC entity resolution.
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
with gr.Blocks(
|
| 241 |
+
title="Auto-Ontology",
|
| 242 |
+
theme=gr.themes.Base(
|
| 243 |
+
primary_hue="indigo",
|
| 244 |
+
secondary_hue="emerald",
|
| 245 |
+
neutral_hue="slate",
|
| 246 |
+
),
|
| 247 |
+
) as demo:
|
| 248 |
+
gr.Markdown(DESCRIPTION)
|
| 249 |
+
|
| 250 |
+
with gr.Tabs():
|
| 251 |
+
with gr.Tab("The Pipeline"):
|
| 252 |
+
gr.HTML(load_pipeline_html())
|
| 253 |
+
|
| 254 |
+
with gr.Tab("Ask the Ontology"):
|
| 255 |
+
gr.Markdown(
|
| 256 |
+
"Chat with a **Strands Agent** that can search products, "
|
| 257 |
+
"browse market signals, find competitors, and query graph statistics."
|
| 258 |
+
)
|
| 259 |
+
gr.ChatInterface(
|
| 260 |
+
fn=chat_fn,
|
| 261 |
+
type="messages",
|
| 262 |
+
examples=[
|
| 263 |
+
"What are the graph statistics?",
|
| 264 |
+
"Search for Tesla vehicles in the dataset",
|
| 265 |
+
"Show me bearish signals in the technology domain",
|
| 266 |
+
"What competes with the Tesla Model Y 2024?",
|
| 267 |
+
"Find signals about battery technology",
|
| 268 |
+
],
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
if __name__ == "__main__":
|
| 273 |
+
demo.launch()
|
pipeline.html
ADDED
|
@@ -0,0 +1,1072 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Auto-Ontology Pipeline</title>
|
| 7 |
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg: #0f172a;
|
| 11 |
+
--surface: #1e293b;
|
| 12 |
+
--border: #334155;
|
| 13 |
+
--text: #e2e8f0;
|
| 14 |
+
--muted: #94a3b8;
|
| 15 |
+
--accent: #818cf8;
|
| 16 |
+
--accent2: #34d399;
|
| 17 |
+
--accent3: #f472b6;
|
| 18 |
+
--accent4: #fbbf24;
|
| 19 |
+
--bearish: #ef4444;
|
| 20 |
+
--bullish: #22c55e;
|
| 21 |
+
--neutral: #64748b;
|
| 22 |
+
}
|
| 23 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 24 |
+
body {
|
| 25 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
| 26 |
+
background: var(--bg);
|
| 27 |
+
color: var(--text);
|
| 28 |
+
line-height: 1.7;
|
| 29 |
+
overflow-x: hidden;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* ─── Hero ─── */
|
| 33 |
+
.hero {
|
| 34 |
+
min-height: 100vh;
|
| 35 |
+
display: flex;
|
| 36 |
+
flex-direction: column;
|
| 37 |
+
justify-content: center;
|
| 38 |
+
align-items: center;
|
| 39 |
+
text-align: center;
|
| 40 |
+
padding: 2rem;
|
| 41 |
+
position: relative;
|
| 42 |
+
overflow: hidden;
|
| 43 |
+
}
|
| 44 |
+
.hero::before {
|
| 45 |
+
content: '';
|
| 46 |
+
position: absolute;
|
| 47 |
+
inset: 0;
|
| 48 |
+
background: radial-gradient(ellipse 80% 60% at 50% 40%, rgba(99,102,241,0.15), transparent);
|
| 49 |
+
}
|
| 50 |
+
.hero h1 {
|
| 51 |
+
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
| 52 |
+
font-weight: 800;
|
| 53 |
+
letter-spacing: -0.02em;
|
| 54 |
+
background: linear-gradient(135deg, #818cf8, #34d399);
|
| 55 |
+
-webkit-background-clip: text;
|
| 56 |
+
-webkit-text-fill-color: transparent;
|
| 57 |
+
background-clip: text;
|
| 58 |
+
position: relative;
|
| 59 |
+
}
|
| 60 |
+
.hero .subtitle {
|
| 61 |
+
font-size: clamp(1rem, 2.5vw, 1.5rem);
|
| 62 |
+
color: var(--muted);
|
| 63 |
+
margin-top: 1rem;
|
| 64 |
+
position: relative;
|
| 65 |
+
}
|
| 66 |
+
.hero .stats-row {
|
| 67 |
+
display: flex;
|
| 68 |
+
gap: 3rem;
|
| 69 |
+
margin-top: 3rem;
|
| 70 |
+
flex-wrap: wrap;
|
| 71 |
+
justify-content: center;
|
| 72 |
+
position: relative;
|
| 73 |
+
}
|
| 74 |
+
.stat { text-align: center; }
|
| 75 |
+
.stat .num {
|
| 76 |
+
font-size: 2.5rem;
|
| 77 |
+
font-weight: 700;
|
| 78 |
+
color: var(--accent);
|
| 79 |
+
font-variant-numeric: tabular-nums;
|
| 80 |
+
}
|
| 81 |
+
.stat .label { color: var(--muted); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
| 82 |
+
.scroll-hint {
|
| 83 |
+
position: absolute;
|
| 84 |
+
bottom: 2rem;
|
| 85 |
+
animation: bounce 2s infinite;
|
| 86 |
+
color: var(--muted);
|
| 87 |
+
font-size: 0.875rem;
|
| 88 |
+
}
|
| 89 |
+
@keyframes bounce {
|
| 90 |
+
0%, 100% { transform: translateY(0); }
|
| 91 |
+
50% { transform: translateY(8px); }
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ─── Sections ─── */
|
| 95 |
+
.section {
|
| 96 |
+
min-height: 100vh;
|
| 97 |
+
padding: 6rem 2rem;
|
| 98 |
+
max-width: 1100px;
|
| 99 |
+
margin: 0 auto;
|
| 100 |
+
display: flex;
|
| 101 |
+
flex-direction: column;
|
| 102 |
+
justify-content: center;
|
| 103 |
+
}
|
| 104 |
+
.section-num {
|
| 105 |
+
font-size: 0.75rem;
|
| 106 |
+
text-transform: uppercase;
|
| 107 |
+
letter-spacing: 0.15em;
|
| 108 |
+
color: var(--accent);
|
| 109 |
+
margin-bottom: 0.5rem;
|
| 110 |
+
}
|
| 111 |
+
.section h2 {
|
| 112 |
+
font-size: clamp(1.75rem, 4vw, 3rem);
|
| 113 |
+
font-weight: 700;
|
| 114 |
+
margin-bottom: 1rem;
|
| 115 |
+
}
|
| 116 |
+
.section .lead {
|
| 117 |
+
font-size: 1.125rem;
|
| 118 |
+
color: var(--muted);
|
| 119 |
+
margin-bottom: 2rem;
|
| 120 |
+
max-width: 700px;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* ─── Cards ─── */
|
| 124 |
+
.card {
|
| 125 |
+
background: var(--surface);
|
| 126 |
+
border: 1px solid var(--border);
|
| 127 |
+
border-radius: 12px;
|
| 128 |
+
padding: 1.5rem;
|
| 129 |
+
margin-bottom: 1.5rem;
|
| 130 |
+
}
|
| 131 |
+
.card-header {
|
| 132 |
+
font-size: 0.75rem;
|
| 133 |
+
text-transform: uppercase;
|
| 134 |
+
letter-spacing: 0.1em;
|
| 135 |
+
color: var(--accent);
|
| 136 |
+
margin-bottom: 0.75rem;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* ─── Viz containers ─── */
|
| 140 |
+
.viz {
|
| 141 |
+
background: var(--surface);
|
| 142 |
+
border: 1px solid var(--border);
|
| 143 |
+
border-radius: 12px;
|
| 144 |
+
padding: 1.5rem;
|
| 145 |
+
margin: 2rem 0;
|
| 146 |
+
overflow: hidden;
|
| 147 |
+
}
|
| 148 |
+
.viz svg { width: 100%; display: block; }
|
| 149 |
+
|
| 150 |
+
/* ─── Waterfall ─── */
|
| 151 |
+
.waterfall-container {
|
| 152 |
+
height: 300px;
|
| 153 |
+
position: relative;
|
| 154 |
+
overflow: hidden;
|
| 155 |
+
border-radius: 8px;
|
| 156 |
+
background: #0c1222;
|
| 157 |
+
}
|
| 158 |
+
.waterfall-url {
|
| 159 |
+
position: absolute;
|
| 160 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 161 |
+
font-size: 0.7rem;
|
| 162 |
+
color: var(--accent);
|
| 163 |
+
opacity: 0;
|
| 164 |
+
white-space: nowrap;
|
| 165 |
+
animation: fall linear infinite;
|
| 166 |
+
}
|
| 167 |
+
@keyframes fall {
|
| 168 |
+
0% { opacity: 0; transform: translateY(-20px); }
|
| 169 |
+
10% { opacity: 0.7; }
|
| 170 |
+
90% { opacity: 0.7; }
|
| 171 |
+
100% { opacity: 0; transform: translateY(320px); }
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* ─── Before/After ─── */
|
| 175 |
+
.before-after {
|
| 176 |
+
display: grid;
|
| 177 |
+
grid-template-columns: 1fr auto 1fr;
|
| 178 |
+
gap: 1rem;
|
| 179 |
+
align-items: start;
|
| 180 |
+
}
|
| 181 |
+
.arrow-col {
|
| 182 |
+
display: flex;
|
| 183 |
+
align-items: center;
|
| 184 |
+
justify-content: center;
|
| 185 |
+
padding-top: 3rem;
|
| 186 |
+
font-size: 2rem;
|
| 187 |
+
color: var(--accent);
|
| 188 |
+
}
|
| 189 |
+
pre {
|
| 190 |
+
background: #0c1222;
|
| 191 |
+
border: 1px solid var(--border);
|
| 192 |
+
border-radius: 8px;
|
| 193 |
+
padding: 1rem;
|
| 194 |
+
font-size: 0.75rem;
|
| 195 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 196 |
+
overflow-x: auto;
|
| 197 |
+
line-height: 1.5;
|
| 198 |
+
}
|
| 199 |
+
pre .key { color: var(--accent); }
|
| 200 |
+
pre .str { color: var(--accent2); }
|
| 201 |
+
pre .num { color: var(--accent4); }
|
| 202 |
+
pre .comment { color: var(--neutral); }
|
| 203 |
+
|
| 204 |
+
/* ─── Two-col layout ─── */
|
| 205 |
+
.two-col {
|
| 206 |
+
display: grid;
|
| 207 |
+
grid-template-columns: 1fr 1fr;
|
| 208 |
+
gap: 1.5rem;
|
| 209 |
+
}
|
| 210 |
+
@media (max-width: 768px) {
|
| 211 |
+
.two-col, .before-after { grid-template-columns: 1fr; }
|
| 212 |
+
.arrow-col { transform: rotate(90deg); padding: 0; }
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/* ─── Stats pills ─── */
|
| 216 |
+
.pill-row {
|
| 217 |
+
display: flex;
|
| 218 |
+
gap: 0.75rem;
|
| 219 |
+
flex-wrap: wrap;
|
| 220 |
+
margin: 1rem 0;
|
| 221 |
+
}
|
| 222 |
+
.pill {
|
| 223 |
+
display: inline-flex;
|
| 224 |
+
align-items: center;
|
| 225 |
+
gap: 0.5rem;
|
| 226 |
+
padding: 0.5rem 1rem;
|
| 227 |
+
border-radius: 999px;
|
| 228 |
+
font-size: 0.8rem;
|
| 229 |
+
font-weight: 600;
|
| 230 |
+
border: 1px solid var(--border);
|
| 231 |
+
background: var(--surface);
|
| 232 |
+
}
|
| 233 |
+
.pill .dot {
|
| 234 |
+
width: 8px; height: 8px;
|
| 235 |
+
border-radius: 50%;
|
| 236 |
+
display: inline-block;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* ─── Flow diagram ─── */
|
| 240 |
+
.flow-step {
|
| 241 |
+
display: flex;
|
| 242 |
+
align-items: center;
|
| 243 |
+
gap: 1rem;
|
| 244 |
+
margin: 0.5rem 0;
|
| 245 |
+
}
|
| 246 |
+
.flow-icon {
|
| 247 |
+
width: 48px; height: 48px;
|
| 248 |
+
border-radius: 12px;
|
| 249 |
+
display: flex;
|
| 250 |
+
align-items: center;
|
| 251 |
+
justify-content: center;
|
| 252 |
+
font-size: 1.5rem;
|
| 253 |
+
flex-shrink: 0;
|
| 254 |
+
}
|
| 255 |
+
.flow-connector {
|
| 256 |
+
width: 2px;
|
| 257 |
+
height: 24px;
|
| 258 |
+
background: var(--border);
|
| 259 |
+
margin-left: 23px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* ─── Persona cards ─── */
|
| 263 |
+
.persona-grid {
|
| 264 |
+
display: grid;
|
| 265 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 266 |
+
gap: 1rem;
|
| 267 |
+
margin-top: 1.5rem;
|
| 268 |
+
}
|
| 269 |
+
.persona {
|
| 270 |
+
background: var(--surface);
|
| 271 |
+
border: 1px solid var(--border);
|
| 272 |
+
border-radius: 12px;
|
| 273 |
+
padding: 1.25rem;
|
| 274 |
+
transition: border-color 0.2s;
|
| 275 |
+
}
|
| 276 |
+
.persona:hover { border-color: var(--accent); }
|
| 277 |
+
.persona .icon { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
| 278 |
+
.persona .name { font-weight: 600; margin-bottom: 0.25rem; }
|
| 279 |
+
.persona .desc { font-size: 0.8rem; color: var(--muted); }
|
| 280 |
+
|
| 281 |
+
/* ─── Footer ─── */
|
| 282 |
+
.footer {
|
| 283 |
+
text-align: center;
|
| 284 |
+
padding: 4rem 2rem;
|
| 285 |
+
color: var(--muted);
|
| 286 |
+
font-size: 0.875rem;
|
| 287 |
+
}
|
| 288 |
+
.footer a { color: var(--accent); text-decoration: none; }
|
| 289 |
+
|
| 290 |
+
/* ─── Intersection observer fade-in ─── */
|
| 291 |
+
.reveal {
|
| 292 |
+
opacity: 0;
|
| 293 |
+
transform: translateY(30px);
|
| 294 |
+
transition: opacity 0.8s ease, transform 0.8s ease;
|
| 295 |
+
}
|
| 296 |
+
.reveal.visible {
|
| 297 |
+
opacity: 1;
|
| 298 |
+
transform: translateY(0);
|
| 299 |
+
}
|
| 300 |
+
</style>
|
| 301 |
+
</head>
|
| 302 |
+
<body>
|
| 303 |
+
|
| 304 |
+
<!-- ═══════════ HERO ═══════════ -->
|
| 305 |
+
<div class="hero">
|
| 306 |
+
<h1>Structural Intelligence</h1>
|
| 307 |
+
<p class="subtitle">From Common Crawl to Automotive Market Intelligence</p>
|
| 308 |
+
<div class="stats-row">
|
| 309 |
+
<div class="stat"><div class="num" data-count="176324">0</div><div class="label">Nodes</div></div>
|
| 310 |
+
<div class="stat"><div class="num" data-count="537747">0</div><div class="label">Edges</div></div>
|
| 311 |
+
<div class="stat"><div class="num" data-count="94671">0</div><div class="label">Signals</div></div>
|
| 312 |
+
<div class="stat"><div class="num" data-count="1261">0</div><div class="label">Vehicles</div></div>
|
| 313 |
+
</div>
|
| 314 |
+
<div class="scroll-hint">Scroll to explore the pipeline ↓</div>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<!-- ═══════════ SECTION 1: Common Crawl ═══════════ -->
|
| 318 |
+
<div class="section reveal" id="s1">
|
| 319 |
+
<div class="section-num">Step 01</div>
|
| 320 |
+
<h2>Common Crawl Scraping</h2>
|
| 321 |
+
<p class="lead">Billions of web pages archived by Common Crawl. We query the CC index for automotive domains, download WARC records, and extract clean article text.</p>
|
| 322 |
+
|
| 323 |
+
<div class="viz">
|
| 324 |
+
<div class="card-header">Live URL Waterfall</div>
|
| 325 |
+
<div class="waterfall-container" id="waterfall"></div>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
<div class="two-col">
|
| 329 |
+
<div class="card">
|
| 330 |
+
<div class="card-header">The Funnel</div>
|
| 331 |
+
<div id="funnel-chart" style="height:200px;"></div>
|
| 332 |
+
</div>
|
| 333 |
+
<div class="card">
|
| 334 |
+
<div class="card-header">Sample Documents</div>
|
| 335 |
+
<pre><span class="comment">// Real URLs from the dataset</span>
|
| 336 |
+
<span class="str">"autocar.co.uk/car-news/business-electric-vehicles/
|
| 337 |
+
hydrogen-car-dream-good-dead"</span>
|
| 338 |
+
|
| 339 |
+
<span class="str">"autocar.co.uk/car-news/business-electric-vehicles/
|
| 340 |
+
kia-uk-boss-calls-clarity-hybrid-sales-after-2030"</span>
|
| 341 |
+
|
| 342 |
+
<span class="str">"autocar.co.uk/car-news/business-environment-and-energy/
|
| 343 |
+
kia-europe-reuse-electric-car-batteries-energy-storage"</span>
|
| 344 |
+
|
| 345 |
+
<span class="comment">// Crawl: CC-MAIN-2026-04</span></pre>
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
<!-- ═══════════ SECTION 2: Extraction ═══════════ -->
|
| 351 |
+
<div class="section reveal" id="s2">
|
| 352 |
+
<div class="section-num">Step 02</div>
|
| 353 |
+
<h2>Structured Extraction</h2>
|
| 354 |
+
<p class="lead">NuExtract-2.0 with an ontology-enforced JSON schema transforms raw article text into structured market signals — products, events, companies, and feature-level sentiment.</p>
|
| 355 |
+
|
| 356 |
+
<div class="before-after">
|
| 357 |
+
<div>
|
| 358 |
+
<div class="card-header">Raw Article Text</div>
|
| 359 |
+
<pre style="min-height:280px"><span class="comment">// From autocar.co.uk</span>
|
| 360 |
+
|
| 361 |
+
H2 Mobility has announced it is
|
| 362 |
+
shutting 22 of its hydrogen fuel
|
| 363 |
+
stations in Germany, dealing a
|
| 364 |
+
significant blow to the hydrogen
|
| 365 |
+
car movement in Europe.
|
| 366 |
+
|
| 367 |
+
The company cited low demand and
|
| 368 |
+
high operational costs. Only about
|
| 369 |
+
100 hydrogen fuel cell vehicles
|
| 370 |
+
are registered in Germany...</pre>
|
| 371 |
+
</div>
|
| 372 |
+
<div class="arrow-col">→</div>
|
| 373 |
+
<div>
|
| 374 |
+
<div class="card-header">NuExtract JSON Output</div>
|
| 375 |
+
<pre style="min-height:280px">{
|
| 376 |
+
<span class="key">"market_relevance"</span>: <span class="str">"high"</span>,
|
| 377 |
+
<span class="key">"signals"</span>: [{
|
| 378 |
+
<span class="key">"event_description"</span>:
|
| 379 |
+
<span class="str">"H2 Mobility announced shutting
|
| 380 |
+
22 fuel stations in Germany"</span>,
|
| 381 |
+
<span class="key">"l1_domain"</span>: <span class="str">"C"</span>,
|
| 382 |
+
<span class="key">"sentiment"</span>: <span class="str">"bearish"</span>,
|
| 383 |
+
<span class="key">"impact"</span>: <span class="str">"high"</span>,
|
| 384 |
+
<span class="key">"confidence"</span>: <span class="str">"confirmed"</span>
|
| 385 |
+
}],
|
| 386 |
+
<span class="key">"companies"</span>: [{
|
| 387 |
+
<span class="key">"name"</span>: <span class="str">"H2 Mobility"</span>,
|
| 388 |
+
<span class="key">"role"</span>: <span class="str">"subject"</span>
|
| 389 |
+
}]
|
| 390 |
+
}</pre>
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
| 393 |
+
|
| 394 |
+
<div class="card" style="margin-top:1.5rem;">
|
| 395 |
+
<div class="card-header">Signal Taxonomy (8 L1 Domains)</div>
|
| 396 |
+
<div id="domain-chart" style="height:220px;"></div>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
|
| 400 |
+
<!-- ═══════════ SECTION 3: Normalization ═══════════ -->
|
| 401 |
+
<div class="section reveal" id="s3">
|
| 402 |
+
<div class="section-num">Step 03</div>
|
| 403 |
+
<h2>Entity Resolution</h2>
|
| 404 |
+
<p class="lead">The "Ralph Wiggum" quality loop validates extractions against the NHTSA vPIC database. Fuzzy matching resolves messy text to canonical product IDs through up to 6 depth levels.</p>
|
| 405 |
+
|
| 406 |
+
<div class="viz">
|
| 407 |
+
<div class="card-header">Fuzzy Match Resolution</div>
|
| 408 |
+
<div id="resolution-viz" style="height:220px;"></div>
|
| 409 |
+
</div>
|
| 410 |
+
|
| 411 |
+
<div class="two-col">
|
| 412 |
+
<div class="card">
|
| 413 |
+
<div class="card-header">Resolution Depth Levels</div>
|
| 414 |
+
<div class="flow-step">
|
| 415 |
+
<div class="flow-icon" style="background:rgba(129,140,248,0.15); color:var(--accent);">1</div>
|
| 416 |
+
<div><strong>Make only</strong> <span style="color:var(--muted)">— "Tesla" → confidence 0.2</span></div>
|
| 417 |
+
</div>
|
| 418 |
+
<div class="flow-connector"></div>
|
| 419 |
+
<div class="flow-step">
|
| 420 |
+
<div class="flow-icon" style="background:rgba(129,140,248,0.2); color:var(--accent);">2</div>
|
| 421 |
+
<div><strong>+ Model</strong> <span style="color:var(--muted)">— "Model Y" → confidence 0.4</span></div>
|
| 422 |
+
</div>
|
| 423 |
+
<div class="flow-connector"></div>
|
| 424 |
+
<div class="flow-step">
|
| 425 |
+
<div class="flow-icon" style="background:rgba(129,140,248,0.25); color:var(--accent);">3</div>
|
| 426 |
+
<div><strong>+ Year</strong> <span style="color:var(--muted)">— "2024" → confidence 0.6</span></div>
|
| 427 |
+
</div>
|
| 428 |
+
<div class="flow-connector"></div>
|
| 429 |
+
<div class="flow-step">
|
| 430 |
+
<div class="flow-icon" style="background:rgba(129,140,248,0.3); color:var(--accent);">4</div>
|
| 431 |
+
<div><strong>+ Body Class</strong> <span style="color:var(--muted)">— "SUV" → confidence 0.7</span></div>
|
| 432 |
+
</div>
|
| 433 |
+
<div class="flow-connector"></div>
|
| 434 |
+
<div class="flow-step">
|
| 435 |
+
<div class="flow-icon" style="background:rgba(129,140,248,0.35); color:var(--accent);">5</div>
|
| 436 |
+
<div><strong>+ Powertrain</strong> <span style="color:var(--muted)">— "BEV" → confidence 0.8</span></div>
|
| 437 |
+
</div>
|
| 438 |
+
<div class="flow-connector"></div>
|
| 439 |
+
<div class="flow-step">
|
| 440 |
+
<div class="flow-icon" style="background:rgba(129,140,248,0.4); color:var(--accent);">6</div>
|
| 441 |
+
<div><strong>+ Trim</strong> <span style="color:var(--muted)">— "Long Range" → confidence 0.95</span></div>
|
| 442 |
+
</div>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="card">
|
| 445 |
+
<div class="card-header">vPIC Feature Groups (160 Elements)</div>
|
| 446 |
+
<div class="pill-row">
|
| 447 |
+
<span class="pill"><span class="dot" style="background:#818cf8"></span>Powertrain</span>
|
| 448 |
+
<span class="pill"><span class="dot" style="background:#34d399"></span>Electrification</span>
|
| 449 |
+
<span class="pill"><span class="dot" style="background:#f472b6"></span>Safety (Active)</span>
|
| 450 |
+
<span class="pill"><span class="dot" style="background:#ef4444"></span>Safety (Passive)</span>
|
| 451 |
+
<span class="pill"><span class="dot" style="background:#fbbf24"></span>Body</span>
|
| 452 |
+
<span class="pill"><span class="dot" style="background:#38bdf8"></span>Interior</span>
|
| 453 |
+
<span class="pill"><span class="dot" style="background:#a78bfa"></span>Wheels & Tires</span>
|
| 454 |
+
<span class="pill"><span class="dot" style="background:#fb923c"></span>Vehicle ID</span>
|
| 455 |
+
<span class="pill"><span class="dot" style="background:#64748b"></span>Specialty</span>
|
| 456 |
+
</div>
|
| 457 |
+
<pre style="margin-top:1rem"><span class="comment">// Example resolution</span>
|
| 458 |
+
<span class="str">"Tesla Model Y Long Range"</span>
|
| 459 |
+
→ make: <span class="str">"Tesla"</span>
|
| 460 |
+
→ model: <span class="str">"Model Y"</span>
|
| 461 |
+
→ year: <span class="num">2024</span>
|
| 462 |
+
→ body: <span class="str">"SUV"</span>
|
| 463 |
+
→ powertrain: <span class="str">"BEV"</span>
|
| 464 |
+
→ id: <span class="key">prd_tesla_model_y_2024</span></pre>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
</div>
|
| 468 |
+
|
| 469 |
+
<!-- ═══════════ SECTION 4: Temporal Stitching ═══════════ -->
|
| 470 |
+
<div class="section reveal" id="s4">
|
| 471 |
+
<div class="section-num">Step 04</div>
|
| 472 |
+
<h2>Temporal Stitching</h2>
|
| 473 |
+
<p class="lead">Signals sharing a product are connected with NEXT edges ordered by timestamp, creating event chains that reveal how stories evolve over time.</p>
|
| 474 |
+
|
| 475 |
+
<div class="viz">
|
| 476 |
+
<div class="card-header">Signal Timeline — Event Chain</div>
|
| 477 |
+
<div id="timeline-viz" style="height:280px;"></div>
|
| 478 |
+
</div>
|
| 479 |
+
|
| 480 |
+
<div class="two-col">
|
| 481 |
+
<div class="card">
|
| 482 |
+
<div class="card-header">How Stitching Works</div>
|
| 483 |
+
<pre><span class="comment">// For each product, order signals by date</span>
|
| 484 |
+
<span class="comment">// Connect consecutive signals with NEXT edges</span>
|
| 485 |
+
<span class="comment">// Max gap: 90 days</span>
|
| 486 |
+
|
| 487 |
+
sig_001 (<span class="str">"Production starts"</span>, Jan 15)
|
| 488 |
+
<span class="key">─NEXT→</span>
|
| 489 |
+
sig_002 (<span class="str">"First deliveries"</span>, Mar 22)
|
| 490 |
+
<span class="key">─NEXT→</span>
|
| 491 |
+
sig_003 (<span class="str">"Recall issued"</span>, May 10)
|
| 492 |
+
<span class="key">─NEXT→</span>
|
| 493 |
+
sig_004 (<span class="str">"Software update"</span>, Jun 03)</pre>
|
| 494 |
+
</div>
|
| 495 |
+
<div class="card">
|
| 496 |
+
<div class="card-header">Temporal Edge Stats</div>
|
| 497 |
+
<div style="margin-top:0.5rem;">
|
| 498 |
+
<div class="stat" style="text-align:left; margin-bottom:1rem;">
|
| 499 |
+
<div class="num" style="font-size:2rem;">84,981</div>
|
| 500 |
+
<div class="label">NEXT edges</div>
|
| 501 |
+
</div>
|
| 502 |
+
<p style="color:var(--muted); font-size:0.875rem;">
|
| 503 |
+
Temporal chains let analysts track how market events cascade —
|
| 504 |
+
from a product launch through reviews, sales data, competitive
|
| 505 |
+
responses, and eventual recalls or updates.
|
| 506 |
+
</p>
|
| 507 |
+
<p style="color:var(--muted); font-size:0.875rem; margin-top:0.5rem;">
|
| 508 |
+
The longest chains span 4+ years of signal history for
|
| 509 |
+
established products like the Tesla Model 3 and Toyota RAV4.
|
| 510 |
+
</p>
|
| 511 |
+
</div>
|
| 512 |
+
</div>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<!-- ═══════════ SECTION 5: Hypergraph ═══════════ -->
|
| 517 |
+
<div class="section reveal" id="s5">
|
| 518 |
+
<div class="section-num">Step 05</div>
|
| 519 |
+
<h2>Building the Hypergraph</h2>
|
| 520 |
+
<p class="lead">Products, Features, Actors, Signals, Locations, and Documents are woven together through 13 typed edge roles into a unified knowledge graph — the dual-star topology.</p>
|
| 521 |
+
|
| 522 |
+
<div class="viz">
|
| 523 |
+
<div class="card-header">Dual-Star Graph Assembly</div>
|
| 524 |
+
<div id="graph-viz" style="height:400px;"></div>
|
| 525 |
+
</div>
|
| 526 |
+
|
| 527 |
+
<div class="two-col">
|
| 528 |
+
<div class="card">
|
| 529 |
+
<div class="card-header">Node Breakdown</div>
|
| 530 |
+
<div id="node-chart" style="height:200px;"></div>
|
| 531 |
+
</div>
|
| 532 |
+
<div class="card">
|
| 533 |
+
<div class="card-header">Edge Role Distribution</div>
|
| 534 |
+
<div id="edge-chart" style="height:200px;"></div>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
|
| 539 |
+
<!-- ═══════════ SECTION 6: Structure ═══════════ -->
|
| 540 |
+
<div class="section reveal" id="s6">
|
| 541 |
+
<div class="section-num">Step 06</div>
|
| 542 |
+
<h2>Finding Structure</h2>
|
| 543 |
+
<p class="lead">Four mathematical lenses reveal patterns invisible to keyword search: competitive clusters, regime changes, signal consistency, and perception gaps.</p>
|
| 544 |
+
|
| 545 |
+
<div class="two-col">
|
| 546 |
+
<div class="card">
|
| 547 |
+
<div class="card-header">Signal Sentiment Distribution</div>
|
| 548 |
+
<div id="sentiment-chart" style="height:200px;"></div>
|
| 549 |
+
</div>
|
| 550 |
+
<div class="card">
|
| 551 |
+
<div class="card-header">Competition Clusters (COMPETES_WITH)</div>
|
| 552 |
+
<div id="compete-chart" style="height:200px;"></div>
|
| 553 |
+
</div>
|
| 554 |
+
</div>
|
| 555 |
+
|
| 556 |
+
<div class="persona-grid">
|
| 557 |
+
<div class="persona">
|
| 558 |
+
<div class="icon">📊</div>
|
| 559 |
+
<div class="name">Spectral Analysis</div>
|
| 560 |
+
<div class="desc">Community detection via graph Laplacian — finds competitive clusters and market segments</div>
|
| 561 |
+
</div>
|
| 562 |
+
<div class="persona">
|
| 563 |
+
<div class="icon">🔮</div>
|
| 564 |
+
<div class="name">Topology</div>
|
| 565 |
+
<div class="desc">Persistent homology detects regime changes — when market structure fundamentally shifts</div>
|
| 566 |
+
</div>
|
| 567 |
+
<div class="persona">
|
| 568 |
+
<div class="icon">🌐</div>
|
| 569 |
+
<div class="name">Sheaf Cohomology</div>
|
| 570 |
+
<div class="desc">Signal consistency across regions — where do narratives diverge?</div>
|
| 571 |
+
</div>
|
| 572 |
+
<div class="persona">
|
| 573 |
+
<div class="icon">🔬</div>
|
| 574 |
+
<div class="name">Functor Analysis</div>
|
| 575 |
+
<div class="desc">Feature perception vs reality gap — what the specs say vs what signals reveal</div>
|
| 576 |
+
</div>
|
| 577 |
+
</div>
|
| 578 |
+
|
| 579 |
+
<div class="card" style="margin-top:2rem;">
|
| 580 |
+
<div class="card-header">What Structural Intelligence Reveals</div>
|
| 581 |
+
<p style="color:var(--muted); margin-bottom:1rem;">Move beyond keyword search. The hypergraph reveals:</p>
|
| 582 |
+
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:0.75rem;">
|
| 583 |
+
<div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
|
| 584 |
+
<strong>Competitive Blind Spots</strong>
|
| 585 |
+
<span style="color:var(--muted); font-size:0.8rem;">Products competing on features but not recognized as rivals</span>
|
| 586 |
+
</div>
|
| 587 |
+
<div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
|
| 588 |
+
<strong>Signal Cascades</strong>
|
| 589 |
+
<span style="color:var(--muted); font-size:0.8rem;">How a supply chain disruption propagates through the market</span>
|
| 590 |
+
</div>
|
| 591 |
+
<div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
|
| 592 |
+
<strong>Regime Detection</strong>
|
| 593 |
+
<span style="color:var(--muted); font-size:0.8rem;">When market topology changes — new entrants, exits, mergers</span>
|
| 594 |
+
</div>
|
| 595 |
+
<div class="pill" style="border-radius:8px; flex-direction:column; align-items:flex-start; padding:1rem;">
|
| 596 |
+
<strong>Narrative Divergence</strong>
|
| 597 |
+
<span style="color:var(--muted); font-size:0.8rem;">Where media sentiment conflicts with financial reality</span>
|
| 598 |
+
</div>
|
| 599 |
+
</div>
|
| 600 |
+
</div>
|
| 601 |
+
</div>
|
| 602 |
+
|
| 603 |
+
<!-- ═══════════ FOOTER ═══════════ -->
|
| 604 |
+
<div class="footer">
|
| 605 |
+
<p>Data: <a href="https://huggingface.co/datasets/cp500/auto-ontology">cp500/auto-ontology</a> · Signals extracted from <a href="https://commoncrawl.org/">Common Crawl</a> (CC-BY-4.0) · Vehicles from <a href="https://vpic.nhtsa.dot.gov/api/">NHTSA vPIC</a></p>
|
| 606 |
+
<p style="margin-top:0.5rem;">Built as an AWS Automotive Workshop</p>
|
| 607 |
+
</div>
|
| 608 |
+
|
| 609 |
+
<script>
|
| 610 |
+
// ─── Intersection Observer for fade-in ───
|
| 611 |
+
const observer = new IntersectionObserver((entries) => {
|
| 612 |
+
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
|
| 613 |
+
}, { threshold: 0.15 });
|
| 614 |
+
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
| 615 |
+
|
| 616 |
+
// ─── Hero counter animation ───
|
| 617 |
+
function animateCounters() {
|
| 618 |
+
document.querySelectorAll('[data-count]').forEach(el => {
|
| 619 |
+
const target = +el.dataset.count;
|
| 620 |
+
const duration = 2000;
|
| 621 |
+
const start = performance.now();
|
| 622 |
+
function tick(now) {
|
| 623 |
+
const t = Math.min((now - start) / duration, 1);
|
| 624 |
+
const ease = 1 - Math.pow(1 - t, 3);
|
| 625 |
+
el.textContent = Math.floor(target * ease).toLocaleString();
|
| 626 |
+
if (t < 1) requestAnimationFrame(tick);
|
| 627 |
+
}
|
| 628 |
+
requestAnimationFrame(tick);
|
| 629 |
+
});
|
| 630 |
+
}
|
| 631 |
+
setTimeout(animateCounters, 300);
|
| 632 |
+
|
| 633 |
+
// ─── Waterfall URLs ───
|
| 634 |
+
(function() {
|
| 635 |
+
const urls = [
|
| 636 |
+
"autocar.co.uk/car-news/electric-vehicles/...",
|
| 637 |
+
"motortrend.com/news/2025-model-comparison/...",
|
| 638 |
+
"electrek.co/2025/01/tesla-model-y-refresh/...",
|
| 639 |
+
"caranddriver.com/reviews/porsche-macan-ev/...",
|
| 640 |
+
"reuters.com/business/autos/toyota-ev-plan/...",
|
| 641 |
+
"insideevs.com/news/rivian-r2-production/...",
|
| 642 |
+
"autoblog.com/hyundai-ioniq-6-review-2025/...",
|
| 643 |
+
"greencarreports.com/byd-seal-us-launch/...",
|
| 644 |
+
"topgear.com/car-news/bmw-neue-klasse-test/...",
|
| 645 |
+
"thedrive.com/news/lucid-gravity-delivery/...",
|
| 646 |
+
"carscoops.com/mercedes-eqg-off-road-test/...",
|
| 647 |
+
"jalopnik.com/gm-ultium-battery-update/...",
|
| 648 |
+
"motor1.com/nissan-ariya-range-comparison/...",
|
| 649 |
+
"carbuzz.com/volkswagen-id7-tourer-review/...",
|
| 650 |
+
"driving.ca/ford-explorer-ev-winter-test/...",
|
| 651 |
+
"paultan.org/2025/proton-ev-launch-date/...",
|
| 652 |
+
"autoexpress.co.uk/kia-ev9-gt-review/...",
|
| 653 |
+
];
|
| 654 |
+
const container = document.getElementById('waterfall');
|
| 655 |
+
if (!container) return;
|
| 656 |
+
let i = 0;
|
| 657 |
+
function spawn() {
|
| 658 |
+
const el = document.createElement('div');
|
| 659 |
+
el.className = 'waterfall-url';
|
| 660 |
+
el.textContent = urls[i % urls.length];
|
| 661 |
+
el.style.left = (Math.random() * 85) + '%';
|
| 662 |
+
el.style.animationDuration = (3 + Math.random() * 4) + 's';
|
| 663 |
+
el.style.animationDelay = '0s';
|
| 664 |
+
container.appendChild(el);
|
| 665 |
+
el.addEventListener('animationend', () => el.remove());
|
| 666 |
+
i++;
|
| 667 |
+
}
|
| 668 |
+
setInterval(spawn, 400);
|
| 669 |
+
for (let j = 0; j < 8; j++) setTimeout(spawn, j * 200);
|
| 670 |
+
})();
|
| 671 |
+
|
| 672 |
+
// ─── Funnel Chart (D3) ───
|
| 673 |
+
(function() {
|
| 674 |
+
const data = [
|
| 675 |
+
{ label: "Common Crawl Pages", value: 3200000000, color: "#334155" },
|
| 676 |
+
{ label: "Automotive Domains", value: 2400000, color: "#475569" },
|
| 677 |
+
{ label: "High-Relevance Docs", value: 69093, color: "#818cf8" },
|
| 678 |
+
{ label: "Extracted Signals", value: 94671, color: "#34d399" },
|
| 679 |
+
];
|
| 680 |
+
const container = d3.select('#funnel-chart');
|
| 681 |
+
if (container.empty()) return;
|
| 682 |
+
const w = container.node().clientWidth || 400, h = 200;
|
| 683 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 684 |
+
const maxW = w * 0.9;
|
| 685 |
+
const barH = 36;
|
| 686 |
+
const gap = 8;
|
| 687 |
+
const startY = (h - (data.length * barH + (data.length - 1) * gap)) / 2;
|
| 688 |
+
|
| 689 |
+
data.forEach((d, i) => {
|
| 690 |
+
const ratio = i === 0 ? 1 : 0.25 + 0.75 * (1 - i / data.length);
|
| 691 |
+
const bw = maxW * ratio;
|
| 692 |
+
const x = (w - bw) / 2;
|
| 693 |
+
const y = startY + i * (barH + gap);
|
| 694 |
+
svg.append('rect').attr('x', x).attr('y', y).attr('width', bw).attr('height', barH)
|
| 695 |
+
.attr('rx', 6).attr('fill', d.color);
|
| 696 |
+
svg.append('text').attr('x', w / 2).attr('y', y + barH / 2 + 1)
|
| 697 |
+
.attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
|
| 698 |
+
.attr('fill', '#e2e8f0').attr('font-size', '12px').attr('font-weight', '600')
|
| 699 |
+
.text(`${d.label}: ${d.value >= 1e9 ? (d.value/1e9).toFixed(1)+'B' : d.value >= 1e6 ? (d.value/1e6).toFixed(1)+'M' : d.value.toLocaleString()}`);
|
| 700 |
+
});
|
| 701 |
+
})();
|
| 702 |
+
|
| 703 |
+
// ─── Domain Horizontal Bar Chart ───
|
| 704 |
+
(function() {
|
| 705 |
+
const data = [
|
| 706 |
+
{ domain: "P — Product", count: 41116, color: "#818cf8" },
|
| 707 |
+
{ domain: "C — Competitive", count: 40065, color: "#34d399" },
|
| 708 |
+
{ domain: "T — Technology", count: 8080, color: "#f472b6" },
|
| 709 |
+
{ domain: "M — Market", count: 3014, color: "#fbbf24" },
|
| 710 |
+
{ domain: "F — Financial", count: 1030, color: "#38bdf8" },
|
| 711 |
+
{ domain: "S — Supply Chain", count: 731, color: "#fb923c" },
|
| 712 |
+
{ domain: "R — Regulatory", count: 387, color: "#a78bfa" },
|
| 713 |
+
{ domain: "ST — Strategic", count: 248, color: "#64748b" },
|
| 714 |
+
];
|
| 715 |
+
const container = d3.select('#domain-chart');
|
| 716 |
+
if (container.empty()) return;
|
| 717 |
+
const w = container.node().clientWidth || 500, h = 220;
|
| 718 |
+
const margin = { top: 10, right: 60, bottom: 10, left: 130 };
|
| 719 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 720 |
+
|
| 721 |
+
const x = d3.scaleLinear().domain([0, d3.max(data, d => d.count)]).range([0, w - margin.left - margin.right]);
|
| 722 |
+
const y = d3.scaleBand().domain(data.map(d => d.domain)).range([margin.top, h - margin.bottom]).padding(0.3);
|
| 723 |
+
const g = svg.append('g').attr('transform', `translate(${margin.left},0)`);
|
| 724 |
+
|
| 725 |
+
g.selectAll('rect').data(data).join('rect')
|
| 726 |
+
.attr('x', 0).attr('y', d => y(d.domain))
|
| 727 |
+
.attr('width', d => x(d.count)).attr('height', y.bandwidth())
|
| 728 |
+
.attr('rx', 4).attr('fill', d => d.color);
|
| 729 |
+
|
| 730 |
+
g.selectAll('.label').data(data).join('text')
|
| 731 |
+
.attr('x', -8).attr('y', d => y(d.domain) + y.bandwidth() / 2)
|
| 732 |
+
.attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
|
| 733 |
+
.attr('fill', '#e2e8f0').attr('font-size', '11px').text(d => d.domain);
|
| 734 |
+
|
| 735 |
+
g.selectAll('.count').data(data).join('text')
|
| 736 |
+
.attr('x', d => x(d.count) + 6).attr('y', d => y(d.domain) + y.bandwidth() / 2)
|
| 737 |
+
.attr('dominant-baseline', 'middle')
|
| 738 |
+
.attr('fill', '#94a3b8').attr('font-size', '10px').text(d => d.count.toLocaleString());
|
| 739 |
+
})();
|
| 740 |
+
|
| 741 |
+
// ─── Resolution Viz (animated fuzzy match) ───
|
| 742 |
+
(function() {
|
| 743 |
+
const container = d3.select('#resolution-viz');
|
| 744 |
+
if (container.empty()) return;
|
| 745 |
+
const w = container.node().clientWidth || 500, h = 220;
|
| 746 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 747 |
+
|
| 748 |
+
const steps = [
|
| 749 |
+
{ x: 0.08, text: '"Tesla Model Y\nLong Range 2024"', color: '#94a3b8' },
|
| 750 |
+
{ x: 0.35, text: 'Fuzzy Match\nvs vPIC', color: '#818cf8' },
|
| 751 |
+
{ x: 0.65, text: 'Depth 6\nConf: 0.95', color: '#34d399' },
|
| 752 |
+
{ x: 0.92, text: 'prd_tesla_\nmodel_y_2024', color: '#fbbf24' },
|
| 753 |
+
];
|
| 754 |
+
steps.forEach((s, i) => {
|
| 755 |
+
const cx = s.x * w, cy = h / 2;
|
| 756 |
+
svg.append('circle').attr('cx', cx).attr('cy', cy).attr('r', 38)
|
| 757 |
+
.attr('fill', 'none').attr('stroke', s.color).attr('stroke-width', 2);
|
| 758 |
+
const lines = s.text.split('\n');
|
| 759 |
+
lines.forEach((line, li) => {
|
| 760 |
+
svg.append('text').attr('x', cx).attr('y', cy - 6 + li * 16)
|
| 761 |
+
.attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
|
| 762 |
+
.attr('fill', s.color).attr('font-size', '10px').attr('font-weight', '600')
|
| 763 |
+
.text(line);
|
| 764 |
+
});
|
| 765 |
+
if (i < steps.length - 1) {
|
| 766 |
+
const nx = steps[i + 1].x * w;
|
| 767 |
+
svg.append('line')
|
| 768 |
+
.attr('x1', cx + 42).attr('y1', cy).attr('x2', nx - 42).attr('y2', cy)
|
| 769 |
+
.attr('stroke', '#334155').attr('stroke-width', 2).attr('marker-end', 'url(#arrow)');
|
| 770 |
+
}
|
| 771 |
+
});
|
| 772 |
+
svg.append('defs').append('marker').attr('id', 'arrow').attr('viewBox', '0 0 10 10')
|
| 773 |
+
.attr('refX', 9).attr('refY', 5).attr('markerWidth', 6).attr('markerHeight', 6)
|
| 774 |
+
.attr('orient', 'auto-start-reverse')
|
| 775 |
+
.append('path').attr('d', 'M 0 0 L 10 5 L 0 10 z').attr('fill', '#334155');
|
| 776 |
+
})();
|
| 777 |
+
|
| 778 |
+
// ─── Timeline Viz ───
|
| 779 |
+
(function() {
|
| 780 |
+
const container = d3.select('#timeline-viz');
|
| 781 |
+
if (container.empty()) return;
|
| 782 |
+
const w = container.node().clientWidth || 500, h = 280;
|
| 783 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 784 |
+
|
| 785 |
+
const events = [
|
| 786 |
+
{ date: "2024-01", label: "Production\nannouncement", sentiment: "bullish" },
|
| 787 |
+
{ date: "2024-04", label: "Factory\ngroundbreaking", sentiment: "bullish" },
|
| 788 |
+
{ date: "2024-08", label: "Pre-orders\nopen", sentiment: "bullish" },
|
| 789 |
+
{ date: "2024-11", label: "Supply chain\ndelay reported", sentiment: "bearish" },
|
| 790 |
+
{ date: "2025-02", label: "First deliveries\nbegin", sentiment: "bullish" },
|
| 791 |
+
{ date: "2025-06", label: "OTA update\nexpands range", sentiment: "bullish" },
|
| 792 |
+
{ date: "2025-09", label: "Recall for\nbrake sensor", sentiment: "bearish" },
|
| 793 |
+
];
|
| 794 |
+
const colors = { bullish: '#22c55e', bearish: '#ef4444', neutral: '#64748b' };
|
| 795 |
+
const margin = { left: 40, right: 40, top: 40, bottom: 60 };
|
| 796 |
+
const x = d3.scaleLinear().domain([0, events.length - 1]).range([margin.left, w - margin.right]);
|
| 797 |
+
const cy = h / 2 - 10;
|
| 798 |
+
|
| 799 |
+
// Timeline line
|
| 800 |
+
svg.append('line').attr('x1', margin.left).attr('y1', cy).attr('x2', w - margin.right).attr('y2', cy)
|
| 801 |
+
.attr('stroke', '#334155').attr('stroke-width', 2);
|
| 802 |
+
|
| 803 |
+
// NEXT edge labels
|
| 804 |
+
for (let i = 0; i < events.length - 1; i++) {
|
| 805 |
+
const mx = (x(i) + x(i + 1)) / 2;
|
| 806 |
+
svg.append('text').attr('x', mx).attr('y', cy - 24)
|
| 807 |
+
.attr('text-anchor', 'middle').attr('fill', '#818cf8').attr('font-size', '9px').attr('font-weight', '600')
|
| 808 |
+
.text('NEXT');
|
| 809 |
+
svg.append('line').attr('x1', x(i) + 10).attr('y1', cy).attr('x2', x(i + 1) - 10).attr('y2', cy)
|
| 810 |
+
.attr('stroke', '#818cf8').attr('stroke-width', 1.5)
|
| 811 |
+
.attr('stroke-dasharray', '4,3');
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
// Event nodes
|
| 815 |
+
events.forEach((ev, i) => {
|
| 816 |
+
const cx2 = x(i);
|
| 817 |
+
svg.append('circle').attr('cx', cx2).attr('cy', cy).attr('r', 8)
|
| 818 |
+
.attr('fill', colors[ev.sentiment]).attr('stroke', '#0f172a').attr('stroke-width', 2);
|
| 819 |
+
const lines = ev.label.split('\n');
|
| 820 |
+
lines.forEach((line, li) => {
|
| 821 |
+
svg.append('text').attr('x', cx2).attr('y', cy + 24 + li * 14)
|
| 822 |
+
.attr('text-anchor', 'middle').attr('fill', '#e2e8f0').attr('font-size', '9px')
|
| 823 |
+
.text(line);
|
| 824 |
+
});
|
| 825 |
+
svg.append('text').attr('x', cx2).attr('y', cy + 24 + lines.length * 14 + 4)
|
| 826 |
+
.attr('text-anchor', 'middle').attr('fill', '#64748b').attr('font-size', '8px')
|
| 827 |
+
.text(ev.date);
|
| 828 |
+
});
|
| 829 |
+
|
| 830 |
+
// Legend
|
| 831 |
+
const legend = svg.append('g').attr('transform', `translate(${margin.left}, ${h - 20})`);
|
| 832 |
+
[['bullish', 'Bullish'], ['bearish', 'Bearish']].forEach(([k, label], i) => {
|
| 833 |
+
legend.append('circle').attr('cx', i * 90).attr('cy', 0).attr('r', 5).attr('fill', colors[k]);
|
| 834 |
+
legend.append('text').attr('x', i * 90 + 10).attr('y', 1)
|
| 835 |
+
.attr('dominant-baseline', 'middle').attr('fill', '#94a3b8').attr('font-size', '10px').text(label);
|
| 836 |
+
});
|
| 837 |
+
})();
|
| 838 |
+
|
| 839 |
+
// ─── Graph Force Simulation (Dual-Star) ───
|
| 840 |
+
(function() {
|
| 841 |
+
const container = d3.select('#graph-viz');
|
| 842 |
+
if (container.empty()) return;
|
| 843 |
+
const w = container.node().clientWidth || 600, h = 400;
|
| 844 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 845 |
+
|
| 846 |
+
const types = [
|
| 847 |
+
{ type: 'Product', color: '#818cf8', count: 8, r: 10 },
|
| 848 |
+
{ type: 'Signal', color: '#34d399', count: 30, r: 4 },
|
| 849 |
+
{ type: 'Feature', color: '#f472b6', count: 12, r: 6 },
|
| 850 |
+
{ type: 'Actor', color: '#fbbf24', count: 10, r: 7 },
|
| 851 |
+
{ type: 'Document', color: '#64748b', count: 15, r: 3 },
|
| 852 |
+
];
|
| 853 |
+
|
| 854 |
+
const nodes = [];
|
| 855 |
+
const links = [];
|
| 856 |
+
types.forEach(t => {
|
| 857 |
+
for (let i = 0; i < t.count; i++) {
|
| 858 |
+
nodes.push({ id: `${t.type}_${i}`, type: t.type, color: t.color, r: t.r });
|
| 859 |
+
}
|
| 860 |
+
});
|
| 861 |
+
|
| 862 |
+
// Connect signals to products (ABOUT_PRODUCT)
|
| 863 |
+
const products = nodes.filter(n => n.type === 'Product');
|
| 864 |
+
const signals = nodes.filter(n => n.type === 'Signal');
|
| 865 |
+
const features = nodes.filter(n => n.type === 'Feature');
|
| 866 |
+
const actors = nodes.filter(n => n.type === 'Actor');
|
| 867 |
+
const docs = nodes.filter(n => n.type === 'Document');
|
| 868 |
+
|
| 869 |
+
signals.forEach(s => {
|
| 870 |
+
const p = products[Math.floor(Math.random() * products.length)];
|
| 871 |
+
links.push({ source: s.id, target: p.id, color: '#818cf840' });
|
| 872 |
+
if (Math.random() > 0.5) {
|
| 873 |
+
const d = docs[Math.floor(Math.random() * docs.length)];
|
| 874 |
+
links.push({ source: d.id, target: s.id, color: '#64748b30' });
|
| 875 |
+
}
|
| 876 |
+
});
|
| 877 |
+
products.forEach(p => {
|
| 878 |
+
const nf = 2 + Math.floor(Math.random() * 3);
|
| 879 |
+
for (let i = 0; i < nf; i++) {
|
| 880 |
+
const f = features[Math.floor(Math.random() * features.length)];
|
| 881 |
+
links.push({ source: p.id, target: f.id, color: '#f472b640' });
|
| 882 |
+
}
|
| 883 |
+
const a = actors[Math.floor(Math.random() * actors.length)];
|
| 884 |
+
links.push({ source: p.id, target: a.id, color: '#fbbf2440' });
|
| 885 |
+
});
|
| 886 |
+
|
| 887 |
+
const sim = d3.forceSimulation(nodes)
|
| 888 |
+
.force('link', d3.forceLink(links).id(d => d.id).distance(50))
|
| 889 |
+
.force('charge', d3.forceManyBody().strength(-40))
|
| 890 |
+
.force('center', d3.forceCenter(w / 2, h / 2))
|
| 891 |
+
.force('collision', d3.forceCollide().radius(d => d.r + 2));
|
| 892 |
+
|
| 893 |
+
const link = svg.append('g').selectAll('line').data(links).join('line')
|
| 894 |
+
.attr('stroke', d => d.color).attr('stroke-width', 1);
|
| 895 |
+
const node = svg.append('g').selectAll('circle').data(nodes).join('circle')
|
| 896 |
+
.attr('r', d => d.r).attr('fill', d => d.color).attr('stroke', '#0f172a').attr('stroke-width', 1);
|
| 897 |
+
|
| 898 |
+
node.append('title').text(d => d.type);
|
| 899 |
+
|
| 900 |
+
sim.on('tick', () => {
|
| 901 |
+
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
| 902 |
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
| 903 |
+
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
| 904 |
+
});
|
| 905 |
+
|
| 906 |
+
// Legend
|
| 907 |
+
const lg = svg.append('g').attr('transform', `translate(16, ${h - 80})`);
|
| 908 |
+
types.forEach((t, i) => {
|
| 909 |
+
lg.append('circle').attr('cx', 0).attr('cy', i * 16).attr('r', 5).attr('fill', t.color);
|
| 910 |
+
lg.append('text').attr('x', 12).attr('y', i * 16 + 1)
|
| 911 |
+
.attr('dominant-baseline', 'middle').attr('fill', '#94a3b8').attr('font-size', '10px')
|
| 912 |
+
.text(t.type);
|
| 913 |
+
});
|
| 914 |
+
})();
|
| 915 |
+
|
| 916 |
+
// ─── Node Type Donut ───
|
| 917 |
+
(function() {
|
| 918 |
+
const data = [
|
| 919 |
+
{ type: 'Signal', count: 94671, color: '#34d399' },
|
| 920 |
+
{ type: 'Document', count: 69093, color: '#64748b' },
|
| 921 |
+
{ type: 'Actor', count: 11189, color: '#fbbf24' },
|
| 922 |
+
{ type: 'Product', count: 1261, color: '#818cf8' },
|
| 923 |
+
{ type: 'Feature', count: 110, color: '#f472b6' },
|
| 924 |
+
];
|
| 925 |
+
const container = d3.select('#node-chart');
|
| 926 |
+
if (container.empty()) return;
|
| 927 |
+
const w = container.node().clientWidth || 300, h = 200;
|
| 928 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 929 |
+
const r = Math.min(w, h) / 2 - 20;
|
| 930 |
+
const g = svg.append('g').attr('transform', `translate(${w/2 - 40}, ${h/2})`);
|
| 931 |
+
|
| 932 |
+
const pie = d3.pie().value(d => d.count).sort(null);
|
| 933 |
+
const arc = d3.arc().innerRadius(r * 0.55).outerRadius(r);
|
| 934 |
+
|
| 935 |
+
g.selectAll('path').data(pie(data)).join('path')
|
| 936 |
+
.attr('d', arc).attr('fill', d => d.data.color).attr('stroke', '#1e293b').attr('stroke-width', 2);
|
| 937 |
+
|
| 938 |
+
// Legend on right
|
| 939 |
+
const lg = svg.append('g').attr('transform', `translate(${w/2 + r - 20}, ${h/2 - data.length * 9})`);
|
| 940 |
+
data.forEach((d, i) => {
|
| 941 |
+
lg.append('circle').attr('cx', 0).attr('cy', i * 20).attr('r', 4).attr('fill', d.color);
|
| 942 |
+
lg.append('text').attr('x', 10).attr('y', i * 20 + 1)
|
| 943 |
+
.attr('dominant-baseline', 'middle').attr('fill', '#e2e8f0').attr('font-size', '10px')
|
| 944 |
+
.text(`${d.type} (${d.count.toLocaleString()})`);
|
| 945 |
+
});
|
| 946 |
+
})();
|
| 947 |
+
|
| 948 |
+
// ─── Edge Role Bar Chart ───
|
| 949 |
+
(function() {
|
| 950 |
+
const data = [
|
| 951 |
+
{ role: 'AFFECTS', count: 186696 },
|
| 952 |
+
{ role: 'SOURCES', count: 94671 },
|
| 953 |
+
{ role: 'NEXT', count: 84981 },
|
| 954 |
+
{ role: 'EMITS', count: 61151 },
|
| 955 |
+
{ role: 'ABOUT_PRODUCT', count: 55051 },
|
| 956 |
+
{ role: 'HAS_FEATURE', count: 22521 },
|
| 957 |
+
{ role: 'COMPETES_WITH', count: 17835 },
|
| 958 |
+
{ role: 'ABOUT_FEATURE', count: 12297 },
|
| 959 |
+
];
|
| 960 |
+
const container = d3.select('#edge-chart');
|
| 961 |
+
if (container.empty()) return;
|
| 962 |
+
const w = container.node().clientWidth || 300, h = 200;
|
| 963 |
+
const margin = { top: 5, right: 50, bottom: 5, left: 100 };
|
| 964 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 965 |
+
|
| 966 |
+
const x = d3.scaleLinear().domain([0, d3.max(data, d => d.count)]).range([0, w - margin.left - margin.right]);
|
| 967 |
+
const y = d3.scaleBand().domain(data.map(d => d.role)).range([margin.top, h - margin.bottom]).padding(0.25);
|
| 968 |
+
const g = svg.append('g').attr('transform', `translate(${margin.left},0)`);
|
| 969 |
+
|
| 970 |
+
const colorScale = d3.scaleOrdinal().domain(data.map(d => d.role))
|
| 971 |
+
.range(['#818cf8','#34d399','#f472b6','#fbbf24','#38bdf8','#fb923c','#a78bfa','#64748b']);
|
| 972 |
+
|
| 973 |
+
g.selectAll('rect').data(data).join('rect')
|
| 974 |
+
.attr('x', 0).attr('y', d => y(d.role))
|
| 975 |
+
.attr('width', d => x(d.count)).attr('height', y.bandwidth())
|
| 976 |
+
.attr('rx', 3).attr('fill', d => colorScale(d.role));
|
| 977 |
+
|
| 978 |
+
g.selectAll('.lbl').data(data).join('text')
|
| 979 |
+
.attr('x', -6).attr('y', d => y(d.role) + y.bandwidth() / 2)
|
| 980 |
+
.attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
|
| 981 |
+
.attr('fill', '#e2e8f0').attr('font-size', '9px').text(d => d.role);
|
| 982 |
+
|
| 983 |
+
g.selectAll('.cnt').data(data).join('text')
|
| 984 |
+
.attr('x', d => x(d.count) + 4).attr('y', d => y(d.role) + y.bandwidth() / 2)
|
| 985 |
+
.attr('dominant-baseline', 'middle')
|
| 986 |
+
.attr('fill', '#94a3b8').attr('font-size', '8px').text(d => (d.count / 1000).toFixed(0) + 'K');
|
| 987 |
+
})();
|
| 988 |
+
|
| 989 |
+
// ─── Sentiment Donut ───
|
| 990 |
+
(function() {
|
| 991 |
+
const data = [
|
| 992 |
+
{ label: 'Neutral', count: 72816, color: '#64748b' },
|
| 993 |
+
{ label: 'Bullish', count: 13902, color: '#22c55e' },
|
| 994 |
+
{ label: 'Bearish', count: 4776, color: '#ef4444' },
|
| 995 |
+
{ label: 'Mixed', count: 3177, color: '#fbbf24' },
|
| 996 |
+
];
|
| 997 |
+
const container = d3.select('#sentiment-chart');
|
| 998 |
+
if (container.empty()) return;
|
| 999 |
+
const w = container.node().clientWidth || 300, h = 200;
|
| 1000 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 1001 |
+
const r = Math.min(w, h) / 2 - 20;
|
| 1002 |
+
const g = svg.append('g').attr('transform', `translate(${w/2 - 30}, ${h/2})`);
|
| 1003 |
+
|
| 1004 |
+
const pie = d3.pie().value(d => d.count).sort(null);
|
| 1005 |
+
const arc = d3.arc().innerRadius(r * 0.55).outerRadius(r);
|
| 1006 |
+
|
| 1007 |
+
g.selectAll('path').data(pie(data)).join('path')
|
| 1008 |
+
.attr('d', arc).attr('fill', d => d.data.color).attr('stroke', '#1e293b').attr('stroke-width', 2);
|
| 1009 |
+
|
| 1010 |
+
g.append('text').attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
|
| 1011 |
+
.attr('fill', '#e2e8f0').attr('font-size', '18px').attr('font-weight', '700').text('94.7K');
|
| 1012 |
+
g.append('text').attr('text-anchor', 'middle').attr('y', 18)
|
| 1013 |
+
.attr('fill', '#94a3b8').attr('font-size', '9px').text('signals');
|
| 1014 |
+
|
| 1015 |
+
const lg = svg.append('g').attr('transform', `translate(${w/2 + r - 10}, ${h/2 - data.length * 10})`);
|
| 1016 |
+
data.forEach((d, i) => {
|
| 1017 |
+
lg.append('circle').attr('cx', 0).attr('cy', i * 22).attr('r', 4).attr('fill', d.color);
|
| 1018 |
+
lg.append('text').attr('x', 10).attr('y', i * 22 + 1)
|
| 1019 |
+
.attr('dominant-baseline', 'middle').attr('fill', '#e2e8f0').attr('font-size', '10px')
|
| 1020 |
+
.text(`${d.label} (${(d.count/1000).toFixed(1)}K)`);
|
| 1021 |
+
});
|
| 1022 |
+
})();
|
| 1023 |
+
|
| 1024 |
+
// ─── Competition Clusters Mini Graph ───
|
| 1025 |
+
(function() {
|
| 1026 |
+
const container = d3.select('#compete-chart');
|
| 1027 |
+
if (container.empty()) return;
|
| 1028 |
+
const w = container.node().clientWidth || 300, h = 200;
|
| 1029 |
+
const svg = container.append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
| 1030 |
+
|
| 1031 |
+
const clusters = [
|
| 1032 |
+
{ name: 'Electric SUV', vehicles: ['Tesla Y', 'ID.4', 'Ioniq 5', 'EV6', 'Ariya'], x: 0.3, y: 0.4 },
|
| 1033 |
+
{ name: 'Luxury EV', vehicles: ['Model S', 'EQS', 'i7', 'Air'], x: 0.7, y: 0.35 },
|
| 1034 |
+
{ name: 'Pickup', vehicles: ['F-150L', 'Cybertruck', 'R1T', 'Silverado'], x: 0.5, y: 0.75 },
|
| 1035 |
+
];
|
| 1036 |
+
const colors = ['#818cf8', '#34d399', '#fbbf24'];
|
| 1037 |
+
|
| 1038 |
+
clusters.forEach((c, ci) => {
|
| 1039 |
+
const cx = c.x * w, cy = c.y * h;
|
| 1040 |
+
// Cluster background
|
| 1041 |
+
svg.append('circle').attr('cx', cx).attr('cy', cy)
|
| 1042 |
+
.attr('r', 55).attr('fill', colors[ci]).attr('fill-opacity', 0.07)
|
| 1043 |
+
.attr('stroke', colors[ci]).attr('stroke-opacity', 0.3).attr('stroke-dasharray', '4,3');
|
| 1044 |
+
|
| 1045 |
+
// Vehicle nodes
|
| 1046 |
+
c.vehicles.forEach((v, vi) => {
|
| 1047 |
+
const angle = (vi / c.vehicles.length) * Math.PI * 2 - Math.PI / 2;
|
| 1048 |
+
const vx = cx + Math.cos(angle) * 30;
|
| 1049 |
+
const vy = cy + Math.sin(angle) * 30;
|
| 1050 |
+
svg.append('circle').attr('cx', vx).attr('cy', vy).attr('r', 4)
|
| 1051 |
+
.attr('fill', colors[ci]).attr('stroke', '#0f172a').attr('stroke-width', 1);
|
| 1052 |
+
// Connect to neighbors
|
| 1053 |
+
if (vi > 0) {
|
| 1054 |
+
const pa = ((vi - 1) / c.vehicles.length) * Math.PI * 2 - Math.PI / 2;
|
| 1055 |
+
const px = cx + Math.cos(pa) * 30, py = cy + Math.sin(pa) * 30;
|
| 1056 |
+
svg.append('line').attr('x1', px).attr('y1', py).attr('x2', vx).attr('y2', vy)
|
| 1057 |
+
.attr('stroke', colors[ci]).attr('stroke-opacity', 0.3).attr('stroke-width', 1);
|
| 1058 |
+
}
|
| 1059 |
+
});
|
| 1060 |
+
// Label
|
| 1061 |
+
svg.append('text').attr('x', cx).attr('y', cy - 50)
|
| 1062 |
+
.attr('text-anchor', 'middle').attr('fill', colors[ci]).attr('font-size', '10px').attr('font-weight', '600')
|
| 1063 |
+
.text(c.name);
|
| 1064 |
+
});
|
| 1065 |
+
|
| 1066 |
+
svg.append('text').attr('x', w / 2).attr('y', h - 8)
|
| 1067 |
+
.attr('text-anchor', 'middle').attr('fill', '#64748b').attr('font-size', '9px')
|
| 1068 |
+
.text('17,835 COMPETES_WITH edges');
|
| 1069 |
+
})();
|
| 1070 |
+
</script>
|
| 1071 |
+
</body>
|
| 1072 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=5.0
|
| 2 |
+
strands-agents
|
| 3 |
+
strands-agents-tools
|
| 4 |
+
datasets
|
| 5 |
+
pandas
|
| 6 |
+
pyarrow
|
| 7 |
+
openai
|