Files
odoo-expertos.com/templates/pillar-page.html
T
2026-08-28 10:11:11 -03:00

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>