182 lines
7.9 KiB
JavaScript
182 lines
7.9 KiB
JavaScript
// Данные дашборда: 17 регионов Казахстана, полигоны карты,
|
|
// генерация стабильных показателей за 12 месяцев.
|
|
|
|
var KT_MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
|
|
|
|
// 17 регионов: id, полное имя, короткая подпись, центр подписи на карте
|
|
var KT_REGIONS = [
|
|
{ id: 'kso', name: 'Костанайская обл.', short: 'Костанайская', cx: 320, cy: 68 },
|
|
{ id: 'sko', name: 'Сев.-Казахстанская обл.', short: 'Сев.-Казах.', cx: 365, cy: 132 },
|
|
{ id: 'kgo', name: 'Павлодарская обл.', short: 'Павлодарская', cx: 490, cy: 88 },
|
|
{ id: 'oko', name: 'Восточно-Казахстанская обл.', short: 'Вост.-Казах.', cx: 575, cy: 184 },
|
|
{ id: 'wko', name: 'Атырауская обл.', short: 'Атырауская', cx: 120, cy: 214 },
|
|
{ id: 'zko', name: 'Зап.-Казахстанская обл.', short: 'Зап.-Казах.', cx: 55, cy: 292 },
|
|
{ id: 'ako', name: 'Актюбинская обл.', short: 'Актюбинская', cx: 152, cy: 324 },
|
|
{ id: 'kko', name: 'Мангистауская обл.', short: 'Мангистау', cx: 46, cy: 146 },
|
|
{ id: 'kzo', name: 'Карагандинская обл.', short: 'Караганда', cx: 305, cy: 274 },
|
|
{ id: 'ulytao',name:'Улытауская обл.', short: 'Улытау', cx: 358, cy: 248 },
|
|
{ id: 'ast', name: 'Астана', short: 'Астана', cx: 398, cy: 182 },
|
|
{ id: 'zho', name: 'Жамбылская обл.', short: 'Жамбылская', cx: 215, cy: 262 },
|
|
{ id: 'tgo', name: 'Туркестанская обл.', short: 'Туркестанская', cx: 270, cy: 342 },
|
|
{ id: 'alo', name: 'Алматинская обл.', short: 'Алматинская', cx: 150, cy: 368 },
|
|
{ id: 'alm', name: 'Алматы', short: 'Алматы', cx: 105, cy: 382 },
|
|
{ id: 'shym', name: 'Шымкент', short: 'Шымкент', cx: 315, cy: 378 },
|
|
{ id: 'symk', name: 'Южно-Казахстанская обл.', short: 'Южно-Казах.', cx: 330, cy: 340 }
|
|
];
|
|
|
|
// Полигоны карты (стилизованное упрощение, viewBox 0 0 1000 420).
|
|
// Грани соседних регионов общие — карта читается как единая территория.
|
|
var KT_MAP = {
|
|
kso: [[275,35],[400,30],[460,55],[450,95],[400,90],[280,95]],
|
|
sko: [[280,95],[400,90],[430,140],[390,175],[300,168],[285,135]],
|
|
kgo: [[460,55],[545,60],[590,105],[565,142],[500,135],[450,95]],
|
|
oko: [[430,140],[540,120],[650,150],[680,210],[610,250],[520,235],[470,215],[430,175]],
|
|
wko: [[60,170],[150,160],[185,205],[185,258],[110,262],[20,270],[20,200]],
|
|
zko: [[20,270],[110,262],[140,298],[110,330],[55,318],[20,300]],
|
|
ako: [[110,262],[185,258],[205,295],[175,335],[110,330],[140,298]],
|
|
kko: [[15,100],[95,90],[130,132],[95,170],[60,170],[20,200],[15,150]],
|
|
kzo: [[185,258],[265,250],[300,285],[275,322],[205,295]],
|
|
ulytao: [[320,238],[390,230],[405,262],[375,285],[345,270],[300,285]],
|
|
ast: [[375,160],[412,155],[418,182],[390,196]],
|
|
zho: [[150,246],[215,240],[245,268],[215,300],[205,295],[185,258]],
|
|
tgo: [[205,295],[275,322],[300,305],[300,340],[255,352],[215,352]],
|
|
alo: [[120,350],[175,335],[195,362],[155,390],[110,378]],
|
|
alm: [[82,362],[120,350],[110,378],[90,390]],
|
|
symk: [[300,305],[340,318],[345,355],[300,340],[300,340]],
|
|
shym: [[305,356],[335,352],[345,375],[315,384],[298,372]]
|
|
};
|
|
|
|
// Сезонный профиль года (доли): декабрь пик, февраль спад
|
|
var KT_SEASON = [0.82, 0.74, 0.95, 1.0, 1.05, 1.0, 0.9, 0.95, 1.1, 1.15, 1.2, 1.38];
|
|
|
|
// Базовые планы, млн ₸ — крупные регионы больше
|
|
var KT_PLAN_BASE = {
|
|
ast: 680, alm: 640, symk: 340, kzo: 320, wko: 250, kko: 230,
|
|
ako: 220, kso: 210, oko: 200, zko: 190, tgo: 185, kgo: 170,
|
|
zho: 170, sko: 160, alo: 150, ulytao: 110, shym: 260
|
|
};
|
|
|
|
// Склонность региона к выполнению плана (дефолт 1.0)
|
|
var KT_BIAS = {
|
|
ast: 1.12, alm: 1.1, shym: 1.05, symk: 1.0, kko: 1.04, kzo: 1.0,
|
|
wko: 0.92, ako: 1.02, kso: 1.08, sko: 0.88, kgo: 0.95, oko: 0.9,
|
|
zko: 1.05, tgo: 0.95, zho: 0.94, alo: 0.97, ulytao: 0.9
|
|
};
|
|
|
|
function ktHash(str) {
|
|
var h = 2166136261;
|
|
for (var i = 0; i < str.length; i++) {
|
|
h ^= str.charCodeAt(i);
|
|
h = Math.imul(h, 16777619);
|
|
}
|
|
return h >>> 0;
|
|
}
|
|
|
|
// Детерминированный псевдослучайный 0..1 по строке
|
|
function ktRand(seed) {
|
|
var x = ktHash(seed);
|
|
x ^= x << 13; x >>>= 0;
|
|
x ^= x >> 17;
|
|
x ^= x << 5; x >>>= 0;
|
|
return x / 4294967296;
|
|
}
|
|
|
|
function ktMonthLabel(offset) {
|
|
var now = new Date();
|
|
var m = new Date(now.getFullYear(), now.getMonth() - offset, 1);
|
|
return KT_MONTHS[m.getMonth()] + ' ' + m.getFullYear();
|
|
}
|
|
|
|
// Данные региона за месяц (offset от текущего)
|
|
function ktRegionMonth(rid, offset) {
|
|
var r = null;
|
|
for (var i = 0; i < KT_REGIONS.length; i++) {
|
|
if (KT_REGIONS[i].id === rid) { r = KT_REGIONS[i]; break; }
|
|
}
|
|
var planBase = KT_PLAN_BASE[rid];
|
|
if (!planBase) planBase = 150;
|
|
var now = new Date();
|
|
var m = new Date(now.getFullYear(), now.getMonth() - offset, 1);
|
|
var monIdx = m.getMonth();
|
|
|
|
var plan = Math.round(planBase * (0.9 + ktRand('plan_' + rid + '_' + m.getFullYear() + '_' + monIdx) * 0.2) * 10) / 10;
|
|
|
|
var bias = KT_BIAS[rid] || 1;
|
|
var seed = 'fact_' + rid + '_' + m.getFullYear() + '_' + monIdx + '_' + (window.KT_SEED || 'base');
|
|
var noise = 0.88 + ktRand(seed) * 0.3;
|
|
var fact = Math.round(plan * bias * KT_SEASON[monIdx] * noise * 10) / 10;
|
|
var percent = Math.round((fact / plan) * 100);
|
|
|
|
var deals = Math.floor(18 + ktRand('deals_' + seed) * 120 * (KT_SEASON[monIdx] - 0.4));
|
|
if (deals < 5) deals = 5;
|
|
|
|
var tone = 'risk';
|
|
if (percent >= 100) tone = 'ok';
|
|
else if (percent >= 90) tone = 'warn';
|
|
|
|
return {
|
|
id: rid,
|
|
name: r ? r.name : rid,
|
|
short: r ? r.short : rid,
|
|
cx: r ? r.cx : 0,
|
|
cy: r ? r.cy : 0,
|
|
plan: plan,
|
|
fact: fact,
|
|
percent: percent,
|
|
deals: deals,
|
|
tone: tone
|
|
};
|
|
}
|
|
|
|
// Свод месяца: { offset, label, rows, kpi }
|
|
function ktMonthData(offset) {
|
|
var rows = [];
|
|
for (var i = 0; i < KT_REGIONS.length; i++) {
|
|
rows.push(ktRegionMonth(KT_REGIONS[i].id, offset));
|
|
}
|
|
rows.sort(function(a, b) { return b.fact - a.fact; });
|
|
|
|
var planSum = 0, factSum = 0, dealsSum = 0;
|
|
for (var j = 0; j < rows.length; j++) {
|
|
planSum += rows[j].plan;
|
|
factSum += rows[j].fact;
|
|
dealsSum += rows[j].deals;
|
|
}
|
|
var top5 = {};
|
|
for (var t = 0; t < Math.min(5, rows.length); t++) top5[rows[t].id] = true;
|
|
var leaders = 0;
|
|
for (var l = 0; l < rows.length; l++) {
|
|
if (top5[rows[l].id] && rows[l].tone === 'ok') leaders++;
|
|
}
|
|
|
|
return {
|
|
offset: offset,
|
|
label: ktMonthLabel(offset),
|
|
rows: rows,
|
|
kpi: {
|
|
plan: Math.round(planSum),
|
|
fact: Math.round(factSum),
|
|
percent: Math.round((factSum / planSum) * 100),
|
|
deals: dealsSum,
|
|
leaders: leaders
|
|
}
|
|
};
|
|
}
|
|
|
|
// Форматирование: m в млн ₸
|
|
function ktFormatMillion(m) {
|
|
if (m >= 1000) {
|
|
var v = Math.round(m / 10) / 100;
|
|
return v.toFixed(2).replace('.', ',') + ' млрд';
|
|
}
|
|
return Math.round(m) + ' млн';
|
|
}
|
|
|
|
function ktFormatMoney(m) {
|
|
if (m >= 1000) {
|
|
var v = Math.round(m / 10) / 100;
|
|
return v.toFixed(2).replace('.', ',') + ' млрд ₸';
|
|
}
|
|
return Math.round(m) + ' млн ₸';
|
|
}
|