KPI cards: Bloomberg stock ticker style

- Redesign KPI cards to look like stock tickers
- Add ticker ID (e.g. ВЫРУЧК:KPI, AOV:KPI)
- Arrow up/down SVG indicators for change direction
- Mini trend-line (50px) inside each card
- Change color: green (#16a34a) for up, red (#e21d1d) for down
- Remove sparkline background area, keep only line
- Fix duplicate arrowUp variable naming (bArrowUp/bArrowDown/bArrowNeutral)
This commit is contained in:
neo committed 2026-06-21 18:14:08 +05:00
1 parent 0b49915420
commit 932ec3ac8f
75 files changed
+3032 -56

No files matched your search

+65 -56
View File
@@ -106,57 +106,58 @@ body {
.tab-btn.active::after { transform: scaleX(1); }
.tab-label { display: flex; align-items: center; gap: 8px; }
/* KPI GRID — Bloomberg ticker style */
/* KPI CARDS — Bloomberg stock ticker style */
.kpi-grid {
display: grid; grid-template-columns: repeat(6, 1fr);
gap: 14px; padding: 0;
gap: 10px; padding: 0;
max-width: 1280px; margin: 0 auto;
}
.kpi {
background: #0e0e0e;
border: 1px solid #1e1e1e;
border-radius: 8px;
padding: 16px 18px 14px;
position: relative;
display: flex; flex-direction: column;
gap: 6px;
transition: all 0.25s var(--ease);
padding: 12px 14px;
transition: all 0.2s var(--ease);
opacity: 0; transform: translateY(8px);
cursor: default;
}
.kpi.visible { opacity: 1; transform: translateY(0); }
.kpi:hover {
border-color: #2e2e2e;
background: #131313;
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
box-shadow: 0 2px 12px rgba(0,0,0,0.25);
}
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
.kpi-name {
font-size: 10px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.1em;
color: #666;
.kpi-header {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 6px;
}
.kpi-badge {
.kpi-ticker {
font-family: var(--mono); font-size: 10px; font-weight: 600;
padding: 2px 7px; border-radius: 4px;
display: inline-flex; align-items: center; gap: 2px;
letter-spacing: 0;
color: #555; letter-spacing: 0.03em;
}
.kpi-change {
font-family: var(--mono); font-size: 11px; font-weight: 600;
display: flex; align-items: center; gap: 3px;
}
.kpi-change.up { color: #16a34a; }
.kpi-change.down { color: #e21d1d; }
.kpi-change.neutral { color: #3b82f6; }
.kpi-trend { display: flex; align-items: center; gap: 6px; }
.kpi-trend-line {
width: 52px; height: 22px; display: block; flex-shrink: 0;
}
.kpi-badge.up { color: #22c55e; background: rgba(34,197,94,0.08); }
.kpi-badge.down { color: #ef4444; background: rgba(239,68,68,0.08); }
.kpi-badge.neutral { color: #3b82f6; background: rgba(59,130,246,0.08); }
.kpi-value {
font-family: var(--mono);
font-size: 24px; font-weight: 700;
font-size: 22px; font-weight: 700;
letter-spacing: -0.5px; line-height: 1.1;
color: #f4f4f4;
}
.sparkline-wrap {
width: 100%;
height: 40px;
flex-shrink: 0;
.kpi-desc {
font-size: 10px; color: #4a4a4a; margin-top: 2px;
}
.kpi-context { font-size: 10px; color: #4a4a4a; line-height: 1.4; }
/* Arrow SVG inline */
.kpi-arrow { width: 10px; height: 8px; fill: currentColor; flex-shrink: 0; }
/* CHARTS WRAPPER */
.charts-wrap {
@@ -438,29 +439,45 @@ function seg(key) {
// Tabs
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.seg === key));
// KPIs
// KPIs — Bloomberg stock ticker card style
const g = document.getElementById('kpi-grid');
const arrowUp = '<svg class="kpi-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const arrowDown = '<svg class="kpi-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
function tickerId(name) {
// Convert "Выручка" → "REVN:KPI", "AOV" → "AOV:KPI"
const short = name.toUpperCase().replace(/[^A-ZА-ЯЁ0-9]/g, '').substring(0, 6);
return `${short}:KPI`;
}
g.innerHTML = s.kpis.map((d, i) => {
const arrow = d.t === 'up' ? '▲' : d.t === 'down' ? '▼' : '●';
const isUp = d.t === 'up';
const isDown = d.t === 'down';
const changeColor = isUp ? 'up' : isDown ? 'down' : 'neutral';
const arrow = isUp ? arrowUp : arrowDown;
const sign = isUp ? '+' : isDown ? '−' : '±';
const trendColor = isUp ? '#16a34a' : isDown ? '#e21d1d' : '#3b82f6';
return `<div class="kpi" style="transition-delay:${i*60}ms">
<div class="kpi-top">
<span class="kpi-name">${d.n}</span>
<span class="kpi-badge ${d.t}">${arrow} ${d.d}</span>
<div class="kpi-header">
<span class="kpi-ticker">${tickerId(d.n)}</span>
<span class="kpi-change ${changeColor}">${arrow} ${sign}${d.d}</span>
</div>
<div class="kpi-value">${d.v}</div>
<div class="sparkline-wrap" id="spark-${i}"></div>
<div class="kpi-context">${d.p}</div>
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-top:4px;">
<div class="kpi-trend-line" id="trend-${i}"></div>
<div class="kpi-desc">${d.p}</div>
</div>
</div>`;
}).join('');
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
// Bloomberg table — channels + marketing
const arrowUp = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const arrowDown = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
const arrowNeutral = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const bArrowUp = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const bArrowDown = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
const bArrowNeutral = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
function changeCell(val, t) {
const cls = t === 'up' ? 'up' : t === 'down' ? 'down' : 'neutral';
const arrow = t === 'up' ? arrowUp : t === 'down' ? arrowDown : arrowNeutral;
const arrow = t === 'up' ? bArrowUp : t === 'down' ? bArrowDown : bArrowNeutral;
const sign = t === 'up' ? '+' : t === 'down' ? '−' : '±';
return `<div class="b-change ${cls}">${arrow} ${sign}${val}</div>`;
}
@@ -501,30 +518,22 @@ function seg(key) {
if (tbody1) renderTable(tbody1, channels);
if (tbody2) renderTable(tbody2, marketing);
// Sparkline charts
const sparkColors = [];
s.kpis.forEach(d => { sparkColors.push(d.t === 'up' ? '#22c55e' : d.t === 'down' ? '#ef4444' : '#3b82f6'); });
// Mini trend lines inside KPI cards (Bloomberg ticker style)
const trendColors = s.kpis.map(d => d.t === 'up' ? '#16a34a' : d.t === 'down' ? '#e21d1d' : '#3b82f6');
s.kpis.forEach((d, i) => {
if (d.spark) {
const sparkEl = document.getElementById(`spark-${i}`);
if (!sparkEl) return;
const sparkChart = echarts.init(sparkEl, null, { renderer: 'canvas' });
const isDownTrend = d.t === 'down' && d.p.includes('стоим') || d.p.includes('дефект') || d.p.includes('за единиц') || d.p.includes('стоим');
sparkChart.setOption({
grid: { left: 0, right: 0, top: 4, bottom: 4 },
const trendEl = document.getElementById(`trend-${i}`);
if (!trendEl) return;
const trendChart = echarts.init(trendEl, null, { width: 50, height: 20, renderer: 'canvas' });
trendChart.setOption({
grid: { left: 0, right: 0, top: 2, bottom: 2 },
xAxis: { type: 'category', show: false, data: d.spark },
yAxis: { type: 'value', show: false },
series: [{
type: 'line', data: d.spark, smooth: 0.4, symbol: 'none',
lineStyle: { width: 2, color: sparkColors[i] },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: sparkColors[i] + '30' },
{ offset: 1, color: sparkColors[i] + '05' }
])
},
type: 'line', data: d.spark, smooth: 0.5, symbol: 'none',
lineStyle: { width: 1.5, color: trendColors[i] },
}],
animationDuration: 800,
animationDuration: 600,
});
}
});