1158 lines
36 KiB
HTML
1158 lines
36 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="{{LANG}}">
|
|
<head>
|
|
<!-- Performance: Preconnect to critical origins -->
|
|
<link rel="preconnect" href="https://d3js.org" crossorigin>
|
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
<link rel="preconnect" href="https://images.pexels.com" crossorigin>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{TITLE}} | Odoo Expertos</title>
|
|
|
|
<meta name="description" content="{{META_DESCRIPTION}}">
|
|
<meta name="keywords" content="{{META_KEYWORDS}}">
|
|
<meta name="author" content="Equipo Odoo Expertos">
|
|
<meta name="robots" content="index, follow">
|
|
<meta name="language" content="{{LANG}}">
|
|
|
|
<!-- Canonical URL -->
|
|
<link rel="canonical" href="{{CANONICAL_URL}}">
|
|
|
|
<!-- Open Graph -->
|
|
<meta property="og:title" content="{{TITLE}}">
|
|
<meta property="og:description" content="{{META_DESCRIPTION}}">
|
|
<meta property="og:type" content="article">
|
|
<meta property="og:url" content="{{CANONICAL_URL}}">
|
|
<meta property="og:image" content="https://www.odoo-expertos.com/images/{{OG_IMAGE}}">
|
|
<meta property="og:site_name" content="Odoo Expertos">
|
|
<meta property="og:locale" content="{{OG_LOCALE}}">
|
|
|
|
<!-- Twitter Card -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="{{TITLE}}">
|
|
<meta name="twitter:description" content="{{META_DESCRIPTION}}">
|
|
|
|
<!-- Schema.org Article -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "Article",
|
|
"headline": "{{TITLE}}",
|
|
"description": "{{META_DESCRIPTION}}",
|
|
"author": {
|
|
"@type": "Person",
|
|
"name": "Equipo Odoo Expertos",
|
|
"url": "https://www.odoo-expertos.com/autor/maria-elena-rodriguez/",
|
|
"jobTitle": "{{AUTHOR_TITLE}}",
|
|
"sameAs": [
|
|
"https://www.linkedin.com/in/maria-elena-rodriguez-erp/"
|
|
]
|
|
},
|
|
"publisher": {
|
|
"@type": "Organization",
|
|
"name": "Odoo Expertos",
|
|
"logo": {
|
|
"@type": "ImageObject",
|
|
"url": "https://www.odoo-expertos.com/logo.png"
|
|
}
|
|
},
|
|
"datePublished": "{{DATE_PUBLISHED}}",
|
|
"dateModified": "{{DATE_MODIFIED}}",
|
|
"mainEntityOfPage": {
|
|
"@type": "WebPage",
|
|
"@id": "{{CANONICAL_URL}}"
|
|
},
|
|
"image": "https://www.odoo-expertos.com/images/{{OG_IMAGE}}"
|
|
}
|
|
</script>
|
|
|
|
<!-- FAQ Schema -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "FAQPage",
|
|
"mainEntity": [
|
|
{{FAQ_SCHEMA}}
|
|
]
|
|
}
|
|
</script>
|
|
|
|
<!-- Product Comparison Schema -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "ItemList",
|
|
"name": "{{COMPARISON_LIST_NAME}}",
|
|
"itemListElement": [
|
|
{{PRODUCT_LIST_SCHEMA}}
|
|
]
|
|
}
|
|
</script>
|
|
|
|
<!-- BreadcrumbList Schema -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "BreadcrumbList",
|
|
"itemListElement": [
|
|
{{BREADCRUMB_SCHEMA}}
|
|
]
|
|
}
|
|
</script>
|
|
|
|
<link rel="stylesheet" href="/css/main.min.css">
|
|
<style>
|
|
/* Pillar Page Specific Styles */
|
|
.pillar-hero {
|
|
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 50%, #1a1a1a 100%);
|
|
padding: 4rem 2rem;
|
|
text-align: center;
|
|
border-bottom: 3px solid #FF6B35;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.pillar-hero::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: radial-gradient(ellipse at center, rgba(255,107,53,0.1) 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.pillar-hero h1 {
|
|
font-size: clamp(2rem, 5vw, 3.5rem);
|
|
color: #FF6B35;
|
|
margin-bottom: 1rem;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.pillar-subtitle {
|
|
font-size: 1.3rem;
|
|
color: #b0b0b0;
|
|
max-width: 800px;
|
|
margin: 0 auto 2rem;
|
|
}
|
|
|
|
.pillar-meta {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 2rem;
|
|
flex-wrap: wrap;
|
|
font-size: 0.9rem;
|
|
color: #888;
|
|
}
|
|
|
|
.pillar-meta span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* Quick Stats Bar */
|
|
.quick-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.5rem;
|
|
padding: 2rem;
|
|
background: #2a2a2a;
|
|
border-radius: 12px;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.stat-card {
|
|
text-align: center;
|
|
padding: 1.5rem;
|
|
background: rgba(255,107,53,0.1);
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(255,107,53,0.2);
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
color: #FF6B35;
|
|
display: block;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #b0b0b0;
|
|
font-size: 0.9rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
/* Table of Contents */
|
|
.toc {
|
|
background: #2a2a2a;
|
|
border: 2px solid #FF6B35;
|
|
border-radius: 12px;
|
|
padding: 2rem;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.toc h2 {
|
|
color: #FF6B35;
|
|
margin-bottom: 1rem;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.toc-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
columns: 2;
|
|
column-gap: 2rem;
|
|
}
|
|
|
|
.toc-list li {
|
|
margin-bottom: 0.75rem;
|
|
break-inside: avoid;
|
|
}
|
|
|
|
.toc-list a {
|
|
color: #e0e0e0;
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.toc-list a:hover {
|
|
color: #FF6B35;
|
|
}
|
|
|
|
.toc-list a::before {
|
|
content: '→';
|
|
color: #FF6B35;
|
|
}
|
|
|
|
/* D3 Chart Containers */
|
|
.chart-section {
|
|
margin: 3rem 0;
|
|
padding: 2rem;
|
|
background: #2a2a2a;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.chart-section h3 {
|
|
color: #FF8A50;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.chart-container {
|
|
width: 100%;
|
|
min-height: 400px;
|
|
position: relative;
|
|
}
|
|
|
|
.chart-container svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Scatter Plot Styles */
|
|
.scatter-dot {
|
|
cursor: pointer;
|
|
transition: r 0.3s ease, fill-opacity 0.3s ease;
|
|
}
|
|
|
|
.scatter-dot:hover {
|
|
fill-opacity: 1;
|
|
}
|
|
|
|
/* Radar Chart Styles */
|
|
.radar-area {
|
|
fill-opacity: 0.3;
|
|
stroke-width: 2;
|
|
transition: fill-opacity 0.3s ease;
|
|
}
|
|
|
|
.radar-area:hover {
|
|
fill-opacity: 0.5;
|
|
}
|
|
|
|
/* Heatmap Styles */
|
|
.heatmap-cell {
|
|
stroke: #1a1a1a;
|
|
stroke-width: 2;
|
|
cursor: pointer;
|
|
transition: stroke 0.3s ease;
|
|
}
|
|
|
|
.heatmap-cell:hover {
|
|
stroke: #FF6B35;
|
|
stroke-width: 3;
|
|
}
|
|
|
|
/* Decision Tree Styles */
|
|
.tree-node {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tree-node circle {
|
|
fill: #FF6B35;
|
|
stroke: #fff;
|
|
stroke-width: 2;
|
|
transition: r 0.3s ease;
|
|
}
|
|
|
|
.tree-node:hover circle {
|
|
r: 12;
|
|
}
|
|
|
|
.tree-link {
|
|
fill: none;
|
|
stroke: #555;
|
|
stroke-width: 2;
|
|
}
|
|
|
|
/* Comparison Table */
|
|
.comparison-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 2rem 0;
|
|
background: #2a2a2a;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.comparison-table th,
|
|
.comparison-table td {
|
|
padding: 1rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid #3a3a3a;
|
|
}
|
|
|
|
.comparison-table th {
|
|
background: rgba(255,107,53,0.2);
|
|
color: #FF6B35;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.comparison-table tr:hover {
|
|
background: rgba(255,107,53,0.05);
|
|
}
|
|
|
|
.comparison-table .check {
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.comparison-table .cross {
|
|
color: #f44336;
|
|
}
|
|
|
|
/* Provider Cards */
|
|
.provider-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 1.5rem;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.provider-card {
|
|
background: #2a2a2a;
|
|
border-radius: 12px;
|
|
padding: 1.5rem;
|
|
border: 1px solid rgba(255,107,53,0.2);
|
|
transition: transform 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
.provider-card:hover {
|
|
transform: translateY(-5px);
|
|
border-color: #FF6B35;
|
|
}
|
|
|
|
.provider-card.featured {
|
|
border-color: #FF6B35;
|
|
background: linear-gradient(135deg, rgba(255,107,53,0.1) 0%, #2a2a2a 100%);
|
|
}
|
|
|
|
.provider-card .badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
background: #FF6B35;
|
|
color: #fff;
|
|
border-radius: 20px;
|
|
font-size: 0.8rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.provider-card h4 {
|
|
color: #fff;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.provider-card .price {
|
|
font-size: 1.5rem;
|
|
color: #FF6B35;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.provider-card .price-period {
|
|
color: #888;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.provider-card ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.provider-card li {
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #3a3a3a;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.provider-card li::before {
|
|
content: '✓';
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.provider-card .cta-btn {
|
|
display: block;
|
|
text-align: center;
|
|
padding: 0.75rem 1.5rem;
|
|
background: #FF6B35;
|
|
color: #fff;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
transition: background 0.3s ease;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.provider-card .cta-btn:hover {
|
|
background: #FF8A50;
|
|
}
|
|
|
|
/* FAQ Section */
|
|
.faq-section {
|
|
margin: 3rem 0;
|
|
}
|
|
|
|
.faq-item {
|
|
background: #2a2a2a;
|
|
border-radius: 8px;
|
|
margin-bottom: 1rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.faq-question {
|
|
padding: 1.25rem;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
color: #fff;
|
|
font-weight: 500;
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.faq-question:hover {
|
|
background: rgba(255,107,53,0.1);
|
|
}
|
|
|
|
.faq-question::after {
|
|
content: '+';
|
|
font-size: 1.5rem;
|
|
color: #FF6B35;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.faq-item.open .faq-question::after {
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.faq-answer {
|
|
padding: 0 1.25rem;
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease, padding 0.3s ease;
|
|
}
|
|
|
|
.faq-item.open .faq-answer {
|
|
padding: 1.25rem;
|
|
max-height: 500px;
|
|
}
|
|
|
|
/* Related Articles */
|
|
.related-articles {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.related-card {
|
|
background: #2a2a2a;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.related-card:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.related-card-image {
|
|
height: 150px;
|
|
background: linear-gradient(135deg, rgba(255,107,53,0.3), rgba(255,138,80,0.3));
|
|
}
|
|
|
|
.related-card-content {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.related-card h4 {
|
|
color: #fff;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.related-card a {
|
|
color: #FF6B35;
|
|
text-decoration: none;
|
|
}
|
|
|
|
/* Tooltip */
|
|
.d3-tooltip {
|
|
position: absolute;
|
|
background: rgba(0,0,0,0.9);
|
|
color: #fff;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
font-size: 0.9rem;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
max-width: 250px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
border: 1px solid rgba(255,107,53,0.3);
|
|
}
|
|
|
|
.d3-tooltip strong {
|
|
color: #FF6B35;
|
|
}
|
|
|
|
/* CTA Banner */
|
|
.cta-banner {
|
|
background: linear-gradient(135deg, rgba(255,107,53,0.2) 0%, rgba(255,138,80,0.1) 100%);
|
|
border: 2px solid rgba(255,107,53,0.5);
|
|
border-radius: 12px;
|
|
padding: 2rem;
|
|
text-align: center;
|
|
margin: 3rem 0;
|
|
}
|
|
|
|
.cta-banner h3 {
|
|
color: #FF6B35;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.cta-banner p {
|
|
margin-bottom: 1.5rem;
|
|
color: #b0b0b0;
|
|
}
|
|
|
|
.cta-banner .cta-btn {
|
|
display: inline-block;
|
|
padding: 1rem 2rem;
|
|
background: #FF6B35;
|
|
color: #fff;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
transition: background 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.cta-banner .cta-btn:hover {
|
|
background: #FF8A50;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.toc-list {
|
|
columns: 1;
|
|
}
|
|
|
|
.pillar-meta {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.chart-container {
|
|
min-height: 300px;
|
|
}
|
|
}
|
|
|
|
/* Animation Classes */
|
|
.fade-in {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
|
}
|
|
|
|
.fade-in.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="header"></div>
|
|
|
|
<!-- Breadcrumbs -->
|
|
<nav class="breadcrumbs">
|
|
<div class="container">
|
|
<ol class="breadcrumb-list" itemscope itemtype="https://schema.org/BreadcrumbList">
|
|
{{BREADCRUMB_HTML}}
|
|
</ol>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Hero Section -->
|
|
<section class="pillar-hero">
|
|
<div class="container">
|
|
<h1>{{H1_TITLE}}</h1>
|
|
<p class="pillar-subtitle">{{SUBTITLE}}</p>
|
|
<div class="pillar-meta">
|
|
<span>📅 {{DATE_LABEL}}: {{DATE_PUBLISHED}}</span>
|
|
<span>🔄 {{UPDATE_LABEL}}: {{DATE_MODIFIED}}</span>
|
|
<span>⏱️ {{READ_TIME}} {{READ_LABEL}}</span>
|
|
<span>👤 Equipo Odoo Expertos</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<main class="container">
|
|
<!-- Quick Stats -->
|
|
<section class="quick-stats fade-in">
|
|
{{QUICK_STATS_HTML}}
|
|
</section>
|
|
|
|
<!-- Table of Contents -->
|
|
<nav class="toc fade-in">
|
|
<h2>{{TOC_TITLE}}</h2>
|
|
<ol class="toc-list">
|
|
{{TOC_HTML}}
|
|
</ol>
|
|
</nav>
|
|
|
|
<!-- Executive Summary -->
|
|
<section id="resumen" class="content-section fade-in">
|
|
<h2>{{SUMMARY_TITLE}}</h2>
|
|
{{EXECUTIVE_SUMMARY}}
|
|
</section>
|
|
|
|
<!-- Quick Comparison Table -->
|
|
<section id="comparacion-rapida" class="content-section fade-in">
|
|
<h2>{{COMPARISON_TITLE}}</h2>
|
|
{{COMPARISON_TABLE_HTML}}
|
|
</section>
|
|
|
|
<!-- D3.js Scatter Plot: Price vs Features -->
|
|
<section class="chart-section fade-in">
|
|
<h3>{{SCATTER_TITLE}}</h3>
|
|
<p>{{SCATTER_DESCRIPTION}}</p>
|
|
<div class="chart-container" id="scatter-chart"></div>
|
|
</section>
|
|
|
|
<!-- Provider Cards Grid -->
|
|
<section id="proveedores" class="content-section fade-in">
|
|
<h2>{{PROVIDERS_TITLE}}</h2>
|
|
<div class="provider-grid">
|
|
{{PROVIDER_CARDS_HTML}}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- D3.js Radar Chart: Feature Comparison -->
|
|
<section class="chart-section fade-in">
|
|
<h3>{{RADAR_TITLE}}</h3>
|
|
<p>{{RADAR_DESCRIPTION}}</p>
|
|
<div class="chart-container" id="radar-chart"></div>
|
|
</section>
|
|
|
|
<!-- Detailed Content Sections -->
|
|
{{CONTENT_SECTIONS}}
|
|
|
|
<!-- D3.js Heatmap: Performance Matrix -->
|
|
<section class="chart-section fade-in">
|
|
<h3>{{HEATMAP_TITLE}}</h3>
|
|
<p>{{HEATMAP_DESCRIPTION}}</p>
|
|
<div class="chart-container" id="heatmap-chart"></div>
|
|
</section>
|
|
|
|
<!-- D3.js Decision Tree -->
|
|
<section class="chart-section fade-in">
|
|
<h3>{{TREE_TITLE}}</h3>
|
|
<p>{{TREE_DESCRIPTION}}</p>
|
|
<div class="chart-container" id="decision-tree"></div>
|
|
</section>
|
|
|
|
<!-- CTA Banner -->
|
|
<section class="cta-banner fade-in">
|
|
<h3>{{CTA_TITLE}}</h3>
|
|
<p>{{CTA_DESCRIPTION}}</p>
|
|
<a href="{{CTA_URL}}" class="cta-btn" target="_blank" rel="noopener">{{CTA_BUTTON}}</a>
|
|
</section>
|
|
|
|
<!-- FAQ Section -->
|
|
<section id="faq" class="faq-section fade-in">
|
|
<h2>{{FAQ_TITLE}}</h2>
|
|
{{FAQ_HTML}}
|
|
</section>
|
|
|
|
<!-- Related Articles -->
|
|
<section id="relacionados" class="content-section fade-in">
|
|
<h2>{{RELATED_TITLE}}</h2>
|
|
<div class="related-articles">
|
|
{{RELATED_ARTICLES_HTML}}
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- EEAT Author Section -->
|
|
<div id="eeat-section"></div>
|
|
|
|
<div id="footer"></div>
|
|
|
|
<!-- Scripts -->
|
|
<script src="/js/language-router.js"></script>
|
|
<script src="/components/header.min.js"></script>
|
|
<script src="/components/footer.min.js"></script>
|
|
<script src="/components/eeat.min.js"></script>
|
|
<script src="/js/cookie-banner.min.js"></script>
|
|
|
|
<!-- D3.js -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
<script>
|
|
// Initialize components
|
|
document.getElementById('header').innerHTML = createHeader();
|
|
document.getElementById('footer').innerHTML = createFooter();
|
|
if (typeof createEEAT === 'function') {
|
|
document.getElementById('eeat-section').innerHTML = createEEAT();
|
|
}
|
|
|
|
// Scroll animation observer
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
}
|
|
});
|
|
}, { threshold: 0.1 });
|
|
|
|
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
|
|
|
|
// FAQ Toggle
|
|
document.querySelectorAll('.faq-question').forEach(question => {
|
|
question.addEventListener('click', () => {
|
|
const item = question.parentElement;
|
|
item.classList.toggle('open');
|
|
});
|
|
});
|
|
|
|
// D3.js Chart Data (to be populated per page)
|
|
const chartData = {{CHART_DATA_JSON}};
|
|
|
|
// Tooltip
|
|
const tooltip = d3.select('body').append('div')
|
|
.attr('class', 'd3-tooltip')
|
|
.style('opacity', 0);
|
|
|
|
// Color scale
|
|
const colorScale = d3.scaleOrdinal()
|
|
.range(['#FF6B35', '#FF8A50', '#FFA726', '#FFB74D', '#4CAF50', '#2196F3']);
|
|
|
|
// Scatter Plot
|
|
function renderScatterPlot() {
|
|
const container = d3.select('#scatter-chart');
|
|
const width = container.node().getBoundingClientRect().width;
|
|
const height = 400;
|
|
const margin = { top: 40, right: 40, bottom: 60, left: 60 };
|
|
|
|
container.html('');
|
|
|
|
const svg = container.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height);
|
|
|
|
const xScale = d3.scaleLinear()
|
|
.domain([0, d3.max(chartData.scatter, d => d.price) * 1.1])
|
|
.range([margin.left, width - margin.right]);
|
|
|
|
const yScale = d3.scaleLinear()
|
|
.domain([0, 100])
|
|
.range([height - margin.bottom, margin.top]);
|
|
|
|
const sizeScale = d3.scaleSqrt()
|
|
.domain([0, d3.max(chartData.scatter, d => d.users)])
|
|
.range([5, 30]);
|
|
|
|
// Axes
|
|
svg.append('g')
|
|
.attr('transform', `translate(0,${height - margin.bottom})`)
|
|
.call(d3.axisBottom(xScale).tickFormat(d => `$${d}`))
|
|
.attr('color', '#888');
|
|
|
|
svg.append('g')
|
|
.attr('transform', `translate(${margin.left},0)`)
|
|
.call(d3.axisLeft(yScale))
|
|
.attr('color', '#888');
|
|
|
|
// Axis labels
|
|
svg.append('text')
|
|
.attr('x', width / 2)
|
|
.attr('y', height - 10)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('fill', '#888')
|
|
.text(chartData.scatterLabels?.x || 'Price ($/month)');
|
|
|
|
svg.append('text')
|
|
.attr('transform', 'rotate(-90)')
|
|
.attr('x', -height / 2)
|
|
.attr('y', 20)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('fill', '#888')
|
|
.text(chartData.scatterLabels?.y || 'Feature Score');
|
|
|
|
// Dots
|
|
svg.selectAll('circle')
|
|
.data(chartData.scatter)
|
|
.join('circle')
|
|
.attr('class', 'scatter-dot')
|
|
.attr('cx', d => xScale(d.price))
|
|
.attr('cy', d => yScale(d.score))
|
|
.attr('r', 0)
|
|
.attr('fill', (d, i) => colorScale(i))
|
|
.attr('fill-opacity', 0.7)
|
|
.on('mouseover', function(event, d) {
|
|
d3.select(this).attr('fill-opacity', 1);
|
|
tooltip.transition().duration(200).style('opacity', 1);
|
|
tooltip.html(`<strong>${d.name}</strong><br>Price: $${d.price}/mo<br>Score: ${d.score}/100<br>Users: ${d.users}+`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function() {
|
|
d3.select(this).attr('fill-opacity', 0.7);
|
|
tooltip.transition().duration(200).style('opacity', 0);
|
|
})
|
|
.transition()
|
|
.duration(800)
|
|
.delay((d, i) => i * 100)
|
|
.ease(d3.easeElastic)
|
|
.attr('r', d => sizeScale(d.users));
|
|
}
|
|
|
|
// Radar Chart
|
|
function renderRadarChart() {
|
|
const container = d3.select('#radar-chart');
|
|
const width = container.node().getBoundingClientRect().width;
|
|
const height = 400;
|
|
const margin = 80;
|
|
const radius = Math.min(width, height) / 2 - margin;
|
|
|
|
container.html('');
|
|
|
|
const svg = container.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height)
|
|
.append('g')
|
|
.attr('transform', `translate(${width/2},${height/2})`);
|
|
|
|
const categories = chartData.radar.categories;
|
|
const angleSlice = Math.PI * 2 / categories.length;
|
|
|
|
// Radial scale
|
|
const rScale = d3.scaleLinear()
|
|
.domain([0, 100])
|
|
.range([0, radius]);
|
|
|
|
// Grid circles
|
|
const levels = 5;
|
|
for (let i = 1; i <= levels; i++) {
|
|
svg.append('circle')
|
|
.attr('r', radius * i / levels)
|
|
.attr('fill', 'none')
|
|
.attr('stroke', '#3a3a3a')
|
|
.attr('stroke-dasharray', '3,3');
|
|
}
|
|
|
|
// Axis lines and labels
|
|
categories.forEach((cat, i) => {
|
|
const angle = angleSlice * i - Math.PI / 2;
|
|
svg.append('line')
|
|
.attr('x1', 0)
|
|
.attr('y1', 0)
|
|
.attr('x2', radius * Math.cos(angle))
|
|
.attr('y2', radius * Math.sin(angle))
|
|
.attr('stroke', '#3a3a3a');
|
|
|
|
svg.append('text')
|
|
.attr('x', (radius + 20) * Math.cos(angle))
|
|
.attr('y', (radius + 20) * Math.sin(angle))
|
|
.attr('text-anchor', 'middle')
|
|
.attr('fill', '#888')
|
|
.attr('font-size', '12px')
|
|
.text(cat);
|
|
});
|
|
|
|
// Radar areas
|
|
const radarLine = d3.lineRadial()
|
|
.radius(d => rScale(d))
|
|
.angle((d, i) => angleSlice * i)
|
|
.curve(d3.curveLinearClosed);
|
|
|
|
chartData.radar.providers.forEach((provider, idx) => {
|
|
svg.append('path')
|
|
.datum(provider.values)
|
|
.attr('class', 'radar-area')
|
|
.attr('d', radarLine)
|
|
.attr('fill', colorScale(idx))
|
|
.attr('stroke', colorScale(idx))
|
|
.attr('fill-opacity', 0)
|
|
.on('mouseover', function() {
|
|
d3.select(this).attr('fill-opacity', 0.5);
|
|
tooltip.transition().duration(200).style('opacity', 1);
|
|
tooltip.html(`<strong>${provider.name}</strong>`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function() {
|
|
d3.select(this).attr('fill-opacity', 0.3);
|
|
tooltip.transition().duration(200).style('opacity', 0);
|
|
})
|
|
.transition()
|
|
.duration(800)
|
|
.delay(idx * 200)
|
|
.attr('fill-opacity', 0.3);
|
|
});
|
|
|
|
// Legend
|
|
const legend = svg.append('g')
|
|
.attr('transform', `translate(${radius + 30}, ${-radius + 20})`);
|
|
|
|
chartData.radar.providers.forEach((provider, idx) => {
|
|
const g = legend.append('g')
|
|
.attr('transform', `translate(0, ${idx * 25})`);
|
|
|
|
g.append('rect')
|
|
.attr('width', 15)
|
|
.attr('height', 15)
|
|
.attr('fill', colorScale(idx));
|
|
|
|
g.append('text')
|
|
.attr('x', 20)
|
|
.attr('y', 12)
|
|
.attr('fill', '#888')
|
|
.attr('font-size', '12px')
|
|
.text(provider.name);
|
|
});
|
|
}
|
|
|
|
// Heatmap
|
|
function renderHeatmap() {
|
|
const container = d3.select('#heatmap-chart');
|
|
const width = container.node().getBoundingClientRect().width;
|
|
const height = 400;
|
|
const margin = { top: 60, right: 40, bottom: 40, left: 120 };
|
|
|
|
container.html('');
|
|
|
|
const svg = container.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height);
|
|
|
|
const providers = chartData.heatmap.providers;
|
|
const features = chartData.heatmap.features;
|
|
const data = chartData.heatmap.data;
|
|
|
|
const xScale = d3.scaleBand()
|
|
.domain(features)
|
|
.range([margin.left, width - margin.right])
|
|
.padding(0.05);
|
|
|
|
const yScale = d3.scaleBand()
|
|
.domain(providers)
|
|
.range([margin.top, height - margin.bottom])
|
|
.padding(0.05);
|
|
|
|
const colorHeat = d3.scaleSequential()
|
|
.domain([0, 100])
|
|
.interpolator(d3.interpolateRgb('#2a2a2a', '#FF6B35'));
|
|
|
|
// X axis
|
|
svg.append('g')
|
|
.attr('transform', `translate(0,${margin.top - 5})`)
|
|
.call(d3.axisTop(xScale))
|
|
.selectAll('text')
|
|
.attr('transform', 'rotate(-45)')
|
|
.attr('text-anchor', 'start')
|
|
.attr('fill', '#888');
|
|
|
|
// Y axis
|
|
svg.append('g')
|
|
.attr('transform', `translate(${margin.left - 5},0)`)
|
|
.call(d3.axisLeft(yScale))
|
|
.selectAll('text')
|
|
.attr('fill', '#888');
|
|
|
|
// Cells
|
|
data.forEach((row, i) => {
|
|
row.forEach((value, j) => {
|
|
svg.append('rect')
|
|
.attr('class', 'heatmap-cell')
|
|
.attr('x', xScale(features[j]))
|
|
.attr('y', yScale(providers[i]))
|
|
.attr('width', xScale.bandwidth())
|
|
.attr('height', yScale.bandwidth())
|
|
.attr('fill', colorHeat(value))
|
|
.attr('opacity', 0)
|
|
.on('mouseover', function(event) {
|
|
tooltip.transition().duration(200).style('opacity', 1);
|
|
tooltip.html(`<strong>${providers[i]}</strong><br>${features[j]}: ${value}/100`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
})
|
|
.on('mouseout', function() {
|
|
tooltip.transition().duration(200).style('opacity', 0);
|
|
})
|
|
.transition()
|
|
.duration(500)
|
|
.delay((i * features.length + j) * 30)
|
|
.attr('opacity', 1);
|
|
|
|
// Value text
|
|
svg.append('text')
|
|
.attr('x', xScale(features[j]) + xScale.bandwidth() / 2)
|
|
.attr('y', yScale(providers[i]) + yScale.bandwidth() / 2)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('dominant-baseline', 'middle')
|
|
.attr('fill', value > 50 ? '#fff' : '#888')
|
|
.attr('font-size', '12px')
|
|
.attr('opacity', 0)
|
|
.text(value)
|
|
.transition()
|
|
.duration(500)
|
|
.delay((i * features.length + j) * 30 + 200)
|
|
.attr('opacity', 1);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Decision Tree
|
|
function renderDecisionTree() {
|
|
const container = d3.select('#decision-tree');
|
|
const width = container.node().getBoundingClientRect().width;
|
|
const height = 500;
|
|
|
|
container.html('');
|
|
|
|
const svg = container.append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height);
|
|
|
|
const g = svg.append('g')
|
|
.attr('transform', `translate(${width/2},40)`);
|
|
|
|
const treeLayout = d3.tree()
|
|
.size([width - 100, height - 100]);
|
|
|
|
const root = d3.hierarchy(chartData.decisionTree);
|
|
treeLayout(root);
|
|
|
|
// Links
|
|
g.selectAll('.tree-link')
|
|
.data(root.links())
|
|
.join('path')
|
|
.attr('class', 'tree-link')
|
|
.attr('d', d3.linkVertical()
|
|
.x(d => d.x - width/2 + 50)
|
|
.y(d => d.y))
|
|
.attr('stroke-dasharray', function() {
|
|
const length = this.getTotalLength();
|
|
return `${length} ${length}`;
|
|
})
|
|
.attr('stroke-dashoffset', function() {
|
|
return this.getTotalLength();
|
|
})
|
|
.transition()
|
|
.duration(1000)
|
|
.attr('stroke-dashoffset', 0);
|
|
|
|
// Nodes
|
|
const nodes = g.selectAll('.tree-node')
|
|
.data(root.descendants())
|
|
.join('g')
|
|
.attr('class', 'tree-node')
|
|
.attr('transform', d => `translate(${d.x - width/2 + 50},${d.y})`)
|
|
.style('opacity', 0);
|
|
|
|
nodes.append('circle')
|
|
.attr('r', 8)
|
|
.attr('fill', d => d.children ? '#FF6B35' : '#4CAF50');
|
|
|
|
nodes.append('text')
|
|
.attr('dy', d => d.children ? -15 : 25)
|
|
.attr('text-anchor', 'middle')
|
|
.attr('fill', '#e0e0e0')
|
|
.attr('font-size', '12px')
|
|
.text(d => d.data.name);
|
|
|
|
nodes.on('mouseover', function(event, d) {
|
|
if (d.data.description) {
|
|
tooltip.transition().duration(200).style('opacity', 1);
|
|
tooltip.html(`<strong>${d.data.name}</strong><br>${d.data.description}`)
|
|
.style('left', (event.pageX + 10) + 'px')
|
|
.style('top', (event.pageY - 28) + 'px');
|
|
}
|
|
})
|
|
.on('mouseout', function() {
|
|
tooltip.transition().duration(200).style('opacity', 0);
|
|
});
|
|
|
|
nodes.transition()
|
|
.duration(500)
|
|
.delay((d, i) => i * 100)
|
|
.style('opacity', 1);
|
|
}
|
|
|
|
// Initialize charts when visible
|
|
const chartObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const id = entry.target.id;
|
|
if (id === 'scatter-chart' && typeof chartData.scatter !== 'undefined') renderScatterPlot();
|
|
if (id === 'radar-chart' && typeof chartData.radar !== 'undefined') renderRadarChart();
|
|
if (id === 'heatmap-chart' && typeof chartData.heatmap !== 'undefined') renderHeatmap();
|
|
if (id === 'decision-tree' && typeof chartData.decisionTree !== 'undefined') renderDecisionTree();
|
|
chartObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.2 });
|
|
|
|
document.querySelectorAll('.chart-container').forEach(el => chartObserver.observe(el));
|
|
|
|
// Resize handler
|
|
let resizeTimeout;
|
|
window.addEventListener('resize', () => {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(() => {
|
|
if (typeof chartData.scatter !== 'undefined') renderScatterPlot();
|
|
if (typeof chartData.radar !== 'undefined') renderRadarChart();
|
|
if (typeof chartData.heatmap !== 'undefined') renderHeatmap();
|
|
if (typeof chartData.decisionTree !== 'undefined') renderDecisionTree();
|
|
}, 250);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|