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:
1 parent
0b49915420
commit
932ec3ac8f
75 files changed
+3032
-56
No files matched your search
+65
-56
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user