dashbord-prodazh-po-regionam/data-kt.js

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) + ' млн ₸';
}