Add dashboard chart animations and SVG integration for dynamic data visualization

This commit is contained in:
Matt Batchelder
2026-02-21 01:45:51 -05:00
parent be30e4d59f
commit f8321568ce
5 changed files with 602 additions and 1 deletions

View File

@@ -2601,6 +2601,140 @@ p:last-child { margin-bottom: 0; }
0% { opacity: 1; }
100% { opacity: 1; }
}
/* ── Dashboard Chart Animations ────────────────────────── */
.dashboard-chart-container {
position: relative;
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 1.5rem;
background: var(--card-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
will-change: opacity;
data-dashboard-container: true;
}
.dashboard-chart {
width: 100%;
height: auto;
display: block;
font-family: var(--font-sans);
user-select: none;
}
/* Bar animation - smooth height transitions */
.dashboard-chart .bar {
fill: url(#barGradient);
will-change: height;
transition: height 0.3s ease-out;
vector-effect: non-scaling-stroke;
}
/* Line path - draw animation */
.dashboard-chart .line-path {
stroke: var(--color-accent);
fill: none;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
will-change: stroke-dashoffset, d;
transition: stroke 0.3s ease;
}
.dashboard-chart .line-fill {
fill: url(#lineGradient);
will-change: d;
opacity: 0.5;
}
/* Pie chart segment animations */
.dashboard-chart .pie-segment {
will-change: transform;
transform-origin: center;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Chart title styling */
.dashboard-chart .chart-title {
font-size: 14px;
font-weight: 600;
fill: var(--color-heading);
letter-spacing: 0.3px;
}
.dashboard-chart .chart-label {
font-size: 12px;
fill: var(--color-text-muted);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.2px;
}
.dashboard-chart .chart-value {
font-size: 13px;
font-weight: 600;
fill: var(--color-text);
transition: fill 0.2s ease;
}
/* Dark mode support */
[data-theme="dark"] .dashboard-chart {
--text-primary: #E0E0E0;
--text-secondary: #9E9E9E;
--primary-color: #66BB6A;
--accent-color: #4CAF50;
--border-color: #333333;
}
[data-theme="light"] .dashboard-chart {
--text-primary: #1a1a1a;
--text-secondary: #666;
--primary-color: #3b82f6;
--accent-color: #10b981;
--border-color: #e5e7eb;
}
/* Performance: disable animations on reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.dashboard-chart .bar,
.dashboard-chart .line-path,
.dashboard-chart .line-fill,
.dashboard-chart .pie-segment {
animation: none !important;
transition: none !important;
}
}
/* Responsive: Adjust for smaller screens */
@media (max-width: 768px) {
.dashboard-chart-container {
padding: 1rem;
border-radius: var(--radius-md);
}
.dashboard-chart .chart-title {
font-size: 13px;
}
.dashboard-chart .chart-label {
font-size: 11px;
}
.dashboard-chart .chart-value {
font-size: 12px;
}
}
/* Touch devices: No hover effects */
@media (hover: none) {
.dashboard-chart-container:hover {
box-shadow: var(--shadow-md);
}
}
.cta-banner {
position: relative;
overflow: hidden;

View File

@@ -0,0 +1,224 @@
/**
* Dashboard Chart Animator
* Continuously animates bar charts, line graphs, and pie charts
* Performance-optimized with IntersectionObserver and requestAnimationFrame
*/
class DashboardAnimator {
constructor(svgElement) {
this.svg = svgElement;
this.animationId = null;
this.isPaused = false;
this.isReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
this.startTime = Date.now();
// Chart dimensions
this.barChartHeight = 120;
this.chartUpdateSpeed = 2000; // Base cycle speed in ms (controls smoothness)
// Cache DOM elements
this.barsGroup1 = svg.querySelectorAll('#bars-group-1 .bar');
this.barsGroup2 = svg.querySelectorAll('#bars-group-2 .bar');
this.valuesGroup1 = svg.querySelectorAll('#values-group-1 .chart-value');
this.valuesGroup2 = svg.querySelectorAll('#values-group-2 .chart-value');
this.linePath = svg.getElementById('line-path');
this.lineFill = svg.getElementById('line-fill');
this.pieSegments = svg.querySelectorAll('.pie-segment');
// Noise generators for smooth, realistic data
this.noisePhase1 = Math.random() * Math.PI * 2;
this.noisePhase2 = Math.random() * Math.PI * 2;
this.noisePhase3 = Math.random() * Math.PI * 2;
this.noisePhase4 = Math.random() * Math.PI * 2;
this.noisePhase5 = Math.random() * Math.PI * 2;
this.init();
}
init() {
if (!this.isReducedMotion) {
this.animate();
this.attachHoverListeners();
}
}
/**
* Perlin-like noise for smooth, natural-looking data
* Uses sine waves at different frequencies
*/
generateNoise(phase, frequency = 0.5) {
return Math.sin(phase * frequency) * 0.5 + 0.5; // Normalize to 0-1
}
/**
* Generate smooth, continuous data values
* Uses combination of sine waves for organic motion
*/
generateValue(basePhase, index, max = 100) {
const time = (Date.now() - this.startTime) / this.chartUpdateSpeed;
const phase = basePhase + time * 0.3 + index * 0.4;
// Multi-frequency sine wave for natural variation
const noise1 = Math.sin(phase * 0.7) * 0.4;
const noise2 = Math.sin(phase * 1.3) * 0.3;
const noise3 = Math.sin(phase * 0.3) * 0.2;
const base = 0.5 + noise1 + noise2 + noise3;
// Ensure value stays in range
return Math.max(10, Math.min(max, base * max * 0.9));
}
/**
* Update bar chart with smooth animation
*/
updateBars(barsElements, valuesElements, maxValue = 100, isPercent = true) {
barsElements.forEach((bar, index) => {
const value = this.generateValue(this.noisePhase1, index, maxValue);
const heightPercent = (value / maxValue) * 100;
const height = (heightPercent / 100) * this.barChartHeight;
bar.setAttribute('height', height);
if (valuesElements[index]) {
valuesElements[index].textContent = isPercent ?
Math.round(value) + '%' :
Math.round(value);
valuesElements[index].setAttribute('data-value', value.toFixed(1));
}
});
}
/**
* Update line graph with smooth curve
*/
updateLineGraph() {
const time = (Date.now() - this.startTime) / this.chartUpdateSpeed;
const points = [];
// Generate 3 points for quadratic bezier curve
for (let i = 0; i < 3; i++) {
const x = i * 100;
const basePhase = this.noisePhase3 + time * 0.2 + i * 0.5;
const noise = Math.sin(basePhase) * 0.3 + Math.sin(basePhase * 0.5) * 0.2;
const y = 80 + noise * 60 - i * 15;
points.push({ x, y });
}
// Build quadratic bezier path
const pathData = `M${points[0].x},${points[0].y} Q${points[1].x},${points[1].y} ${points[2].x},${points[2].y}`;
this.linePath.setAttribute('d', pathData);
this.lineFill.setAttribute('d', `${pathData} L200,160 Q100,140 0,160 Z`);
// Animate stroke-dasharray for drawing effect
const strokeLength = 200;
const offset = ((time * 50) % strokeLength);
this.linePath.setAttribute('stroke-dashoffset', offset);
}
/**
* Update pie chart with smooth segment rotation
*/
updatePieChart() {
const time = (Date.now() - this.startTime) / (this.chartUpdateSpeed * 0.5);
const baseRotation = (time * 15) % 360; // Slow rotation
this.pieSegments.forEach((segment, index) => {
// Add wobble effect to each segment
const wobble = Math.sin(time * 0.5 + index * Math.PI / 2) * 3;
segment.setAttribute('transform', `rotate(${baseRotation + index * 90 + wobble})`);
});
}
/**
* Main animation loop using requestAnimationFrame
*/
animate = () => {
if (!this.isPaused) {
this.updateBars(this.barsGroup1, this.valuesGroup1, 100, true);
this.updateBars(this.barsGroup2, this.valuesGroup2, 5000, false);
this.updateLineGraph();
this.updatePieChart();
}
this.animationId = requestAnimationFrame(this.animate);
}
/**
* Attach hover event listeners to pause/resume animation
*/
attachHoverListeners() {
const container = this.svg.closest('[data-dashboard-container]') || this.svg.parentElement;
if (container) {
container.addEventListener('mouseenter', () => this.pause());
container.addEventListener('mouseleave', () => this.resume());
// Touch support: pause on touch
container.addEventListener('touchstart', () => this.pause());
container.addEventListener('touchend', () => this.resume());
}
}
/**
* Pause animation (on hover)
*/
pause() {
this.isPaused = true;
}
/**
* Resume animation (after hover)
*/
resume() {
this.isPaused = false;
}
/**
* Stop animation and clean up
*/
destroy() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
}
/**
* Initialize dashboard animation when element enters viewport
*/
function initDashboardAnimation() {
const dashboardCharts = document.querySelectorAll('.dashboard-chart');
if (!dashboardCharts.length) return;
const observerOptions = {
root: null,
rootMargin: '50px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (!entry.target._dashboardAnimator) {
entry.target._dashboardAnimator = new DashboardAnimator(entry.target);
}
} else {
// Pause animation when out of view for performance
if (entry.target._dashboardAnimator) {
entry.target._dashboardAnimator.pause();
}
}
});
}, observerOptions);
dashboardCharts.forEach(chart => observer.observe(chart));
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDashboardAnimation);
} else {
initDashboardAnimation();
}

View File

@@ -0,0 +1,123 @@
<svg viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg" class="dashboard-chart">
<defs>
<style>
.dashboard-chart { background: transparent; }
.chart-title { font-size: 14px; font-weight: 600; fill: var(--text-primary, #1a1a1a); }
.chart-label { font-size: 12px; fill: var(--text-secondary, #666); }
.chart-value { font-size: 13px; font-weight: 500; fill: var(--text-primary, #1a1a1a); }
.bar { fill: url(#barGradient); transition: height 0.3s ease; transform-origin: bottom; }
.line-path { stroke: var(--accent-color, #10b981); fill: none; stroke-width: 2.5; stroke-linecap: round; }
.pie-segment { transition: transform 0.4s ease; }
</style>
<!-- Gradients -->
<linearGradient id="barGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--primary-color, #3b82f6);stop-opacity:1" />
<stop offset="100%" style="stop-color:var(--accent-color, #10b981);stop-opacity:0.7" />
</linearGradient>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--accent-color, #10b981);stop-opacity:0.3" />
<stop offset="100%" style="stop-color:var(--accent-color, #10b981);stop-opacity:0" />
</linearGradient>
</defs>
<!-- Left: Bar Charts -->
<g id="bar-charts" transform="translate(10, 10)">
<!-- Bar Chart 1 -->
<text x="0" y="0" class="chart-title">Performance</text>
<g id="bars-group-1" transform="translate(0, 25)">
<rect class="bar" x="0" y="0" width="18" height="0" data-index="0" data-label="API"/>
<rect class="bar" x="25" y="0" width="18" height="0" data-index="1" data-label="Cache"/>
<rect class="bar" x="50" y="0" width="18" height="0" data-index="2" data-label="DB"/>
<rect class="bar" x="75" y="0" width="18" height="0" data-index="3" data-label="Queue"/>
<rect class="bar" x="100" y="0" width="18" height="0" data-index="4" data-label="Worker"/>
</g>
<g transform="translate(0, 145)">
<text x="0" y="0" class="chart-label">API</text>
<text x="25" y="0" class="chart-label">Cache</text>
<text x="50" y="0" class="chart-label">DB</text>
<text x="75" y="0" class="chart-label">Queue</text>
<text x="100" y="0" class="chart-label">Worker</text>
</g>
<g id="values-group-1" transform="translate(0, 160)">
<text x="9" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="34" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="59" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="84" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="109" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
</g>
<!-- Bar Chart 2 -->
<text x="150" y="0" class="chart-title">Requests/sec</text>
<g id="bars-group-2" transform="translate(150, 25)">
<rect class="bar" x="0" y="0" width="18" height="0" data-index="0" data-label="Read"/>
<rect class="bar" x="25" y="0" width="18" height="0" data-index="1" data-label="Write"/>
<rect class="bar" x="50" y="0" width="18" height="0" data-index="2" data-label="Update"/>
<rect class="bar" x="75" y="0" width="18" height="0" data-index="3" data-label="Delete"/>
</g>
<g transform="translate(150, 145)">
<text x="0" y="0" class="chart-label">Read</text>
<text x="25" y="0" class="chart-label">Write</text>
<text x="50" y="0" class="chart-label">Update</text>
<text x="75" y="0" class="chart-label">Delete</text>
</g>
<g id="values-group-2" transform="translate(150, 160)">
<text x="9" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
<text x="34" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
<text x="59" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
<text x="84" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
</g>
</g>
<!-- Middle: Line Graph -->
<g id="line-graph" transform="translate(320, 10)">
<text x="0" y="0" class="chart-title">Traffic Trend</text>
<g transform="translate(0, 25)">
<!-- Background grid -->
<line x1="0" y1="0" x2="200" y2="0" stroke="var(--border-color, #e5e7eb)" stroke-width="0.5"/>
<line x1="0" y1="40" x2="200" y2="40" stroke="var(--border-color, #e5e7eb)" stroke-width="0.5"/>
<line x1="0" y1="80" x2="200" y2="80" stroke="var(--border-color, #e5e7eb)" stroke-width="0.5"/>
<line x1="0" y1="120" x2="200" y2="120" stroke="var(--border-color, #e5e7eb)" stroke-width="0.5"/>
</g>
<path id="line-path" class="line-path" d="M0,80 Q50,60 100,70 T200,50" stroke-dasharray="200" stroke-dashoffset="200"/>
<path id="line-fill" d="M0,80 Q50,60 100,70 T200,50 L200,160 Q100,140 0,160 Z" fill="url(#lineGradient)"/>
</g>
<!-- Right: Pie Chart -->
<g id="pie-chart" transform="translate(580, 10)">
<text x="60" y="0" class="chart-title" text-anchor="middle">Distribution</text>
<g transform="translate(60, 80)">
<!-- Pie segments with animation -->
<g class="pie-segment" id="pie-seg-1" transform="rotate(0)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="var(--primary-color, #3b82f6)" opacity="0.9"/>
</g>
<g class="pie-segment" id="pie-seg-2" transform="rotate(90)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="var(--accent-color, #10b981)" opacity="0.8"/>
</g>
<g class="pie-segment" id="pie-seg-3" transform="rotate(180)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="#f59e0b" opacity="0.7"/>
</g>
<g class="pie-segment" id="pie-seg-4" transform="rotate(270)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="#ef4444" opacity="0.7"/>
</g>
<!-- Center circle -->
<circle cx="0" cy="0" r="18" fill="white" stroke="var(--border-color, #e5e7eb)" stroke-width="1"/>
<text x="0" y="5" text-anchor="middle" class="chart-value">100%</text>
</g>
<!-- Legend -->
<g transform="translate(0, 210)">
<rect x="0" y="0" width="8" height="8" fill="var(--primary-color, #3b82f6)"/>
<text x="12" y="7" class="chart-label">Service A</text>
<rect x="0" y="15" width="8" height="8" fill="var(--accent-color, #10b981)"/>
<text x="12" y="22" class="chart-label">Service B</text>
<rect x="100" y="0" width="8" height="8" fill="#f59e0b"/>
<text x="112" y="7" class="chart-label">Service C</text>
<rect x="100" y="15" width="8" height="8" fill="#ef4444"/>
<text x="112" y="22" class="chart-label">Service D</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1339,7 +1339,118 @@ function oribi_render_platform_row( $a ) {
$img_alt = ! empty( $a['imgAlt'] ) ? $a['imgAlt'] : '';
$img_w = ! empty( $a['imgWidth'] ) ? intval( $a['imgWidth'] ) : 300;
if ( $img_url ) {
// Check if this is a dashboard card (by heading content or dashboard flag)
$heading_text = $a['heading'] ?? '';
$is_dashboard = ! empty( $a['isDashboard'] ) || stripos( $heading_text, 'dashboard' ) !== false || stripos( $heading_text, 'data' ) !== false;
if ( $is_dashboard ) {
// Render dashboard chart animation
$visual_html = '<div class="dashboard-chart-container" data-dashboard-container="true"><svg viewBox="0 0 800 400" xmlns="http://www.w3.org/2000/svg" class="dashboard-chart" aria-label="Real-time dashboard with animated charts">
<defs>
<style>
.dashboard-chart { background: transparent; }
.chart-title { font-size: 14px; font-weight: 600; fill: var(--color-heading); }
.chart-label { font-size: 12px; fill: var(--color-text-muted); }
.chart-value { font-size: 13px; font-weight: 500; fill: var(--color-text); }
.bar { fill: url(#barGradient); transition: height 0.3s ease; transform-origin: bottom; }
.line-path { stroke: var(--color-accent); fill: none; stroke-width: 2.5; stroke-linecap: round; }
.pie-segment { transition: transform 0.4s ease; }
</style>
<linearGradient id="barGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--color-primary);stop-opacity:1" />
<stop offset="100%" style="stop-color:var(--color-accent);stop-opacity:0.7" />
</linearGradient>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--color-accent);stop-opacity:0.3" />
<stop offset="100%" style="stop-color:var(--color-accent);stop-opacity:0" />
</linearGradient>
</defs>
<g id="bar-charts" transform="translate(10, 10)">
<text x="0" y="0" class="chart-title">Performance</text>
<g id="bars-group-1" transform="translate(0, 25)">
<rect class="bar" x="0" y="0" width="18" height="0" data-index="0" data-label="API"/>
<rect class="bar" x="25" y="0" width="18" height="0" data-index="1" data-label="Cache"/>
<rect class="bar" x="50" y="0" width="18" height="0" data-index="2" data-label="DB"/>
<rect class="bar" x="75" y="0" width="18" height="0" data-index="3" data-label="Queue"/>
<rect class="bar" x="100" y="0" width="18" height="0" data-index="4" data-label="Worker"/>
</g>
<g transform="translate(0, 145)">
<text x="0" y="0" class="chart-label">API</text>
<text x="25" y="0" class="chart-label">Cache</text>
<text x="50" y="0" class="chart-label">DB</text>
<text x="75" y="0" class="chart-label">Queue</text>
<text x="100" y="0" class="chart-label">Worker</text>
</g>
<g id="values-group-1" transform="translate(0, 160)">
<text x="9" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="34" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="59" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="84" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
<text x="109" y="0" class="chart-value" text-anchor="middle" data-value="0">0%</text>
</g>
<text x="150" y="0" class="chart-title">Requests/sec</text>
<g id="bars-group-2" transform="translate(150, 25)">
<rect class="bar" x="0" y="0" width="18" height="0" data-index="0" data-label="Read"/>
<rect class="bar" x="25" y="0" width="18" height="0" data-index="1" data-label="Write"/>
<rect class="bar" x="50" y="0" width="18" height="0" data-index="2" data-label="Update"/>
<rect class="bar" x="75" y="0" width="18" height="0" data-index="3" data-label="Delete"/>
</g>
<g transform="translate(150, 145)">
<text x="0" y="0" class="chart-label">Read</text>
<text x="25" y="0" class="chart-label">Write</text>
<text x="50" y="0" class="chart-label">Update</text>
<text x="75" y="0" class="chart-label">Delete</text>
</g>
<g id="values-group-2" transform="translate(150, 160)">
<text x="9" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
<text x="34" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
<text x="59" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
<text x="84" y="0" class="chart-value" text-anchor="middle" data-value="0">0</text>
</g>
</g>
<g id="line-graph" transform="translate(320, 10)">
<text x="0" y="0" class="chart-title">Traffic Trend</text>
<g transform="translate(0, 25)">
<line x1="0" y1="0" x2="200" y2="0" stroke="var(--color-border)" stroke-width="0.5"/>
<line x1="0" y1="40" x2="200" y2="40" stroke="var(--color-border)" stroke-width="0.5"/>
<line x1="0" y1="80" x2="200" y2="80" stroke="var(--color-border)" stroke-width="0.5"/>
<line x1="0" y1="120" x2="200" y2="120" stroke="var(--color-border)" stroke-width="0.5"/>
</g>
<path id="line-path" class="line-path" d="M0,80 Q50,60 100,70 T200,50" stroke-dasharray="200" stroke-dashoffset="200"/>
<path id="line-fill" d="M0,80 Q50,60 100,70 T200,50 L200,160 Q100,140 0,160 Z" fill="url(#lineGradient)"/>
</g>
<g id="pie-chart" transform="translate(580, 10)">
<text x="60" y="0" class="chart-title" text-anchor="middle">Distribution</text>
<g transform="translate(60, 80)">
<g class="pie-segment" id="pie-seg-1" transform="rotate(0)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="var(--color-primary)" opacity="0.9"/>
</g>
<g class="pie-segment" id="pie-seg-2" transform="rotate(90)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="var(--color-accent)" opacity="0.8"/>
</g>
<g class="pie-segment" id="pie-seg-3" transform="rotate(180)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="#f59e0b" opacity="0.7"/>
</g>
<g class="pie-segment" id="pie-seg-4" transform="rotate(270)">
<path d="M 0,0 L 0,-45 A 45,45 0 0,1 31.82,-31.82 Z" fill="#ef4444" opacity="0.7"/>
</g>
<circle cx="0" cy="0" r="18" fill="var(--color-bg)" stroke="var(--color-border)" stroke-width="1"/>
<text x="0" y="5" text-anchor="middle" class="chart-value">100%</text>
</g>
<g transform="translate(0, 210)">
<rect x="0" y="0" width="8" height="8" fill="var(--color-primary)"/>
<text x="12" y="7" class="chart-label">Service A</text>
<rect x="0" y="15" width="8" height="8" fill="var(--color-accent)"/>
<text x="12" y="22" class="chart-label">Service B</text>
<rect x="100" y="0" width="8" height="8" fill="#f59e0b"/>
<text x="112" y="7" class="chart-label">Service C</text>
<rect x="100" y="15" width="8" height="8" fill="#ef4444"/>
<text x="112" y="22" class="chart-label">Service D</text>
</g>
</g>
</svg></div>';
$visual_cls = 'platform-visual has-dashboard';
} elseif ( $img_url ) {
$img_style = 'width:' . $img_w . 'px;max-width:100%;height:auto;border-radius:var(--radius-sm);object-fit:contain;display:block;margin-inline:auto;';
if ( $img_id ) {
$visual_html = wp_get_attachment_image( $img_id, 'full', false, [ 'style' => $img_style, 'alt' => $img_alt ] );

View File

@@ -29,6 +29,15 @@ add_action( 'wp_enqueue_scripts', function () {
true
);
// Dashboard chart animator - smooth continuous animations for dashboard cards
wp_enqueue_script(
'oribi-dashboard-animator',
ORIBI_URI . '/assets/js/dashboard-animator.js',
[],
ORIBI_VERSION,
true
);
// Localize AJAX endpoint for the contact form
wp_localize_script( 'oribi-main', 'oribiAjax', [
'url' => admin_url( 'admin-ajax.php' ),