andrey_vetrov

Waterfall

Jan 6th, 2026
78
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.46 KB | None | 0 0
  1. Meta
  2. {
  3. "links": {
  4. "CH_games": "ia9i2gkbe5ok4"
  5. }
  6. }
  7. Params
  8. module.exports = {
  9. name: [], // выбранный игрок (селектор положит сюда ['Александр М'])
  10. };
  11. Sources
  12. module.exports = {
  13. playersSourceData: {
  14. qlConnectionId: Editor.getId('CH_games'),
  15. ui: true,
  16. data: {
  17. sql_query: `
  18. SELECT name
  19. FROM games.poker_log
  20. WHERE date >= toDateTime('2025-01-01 00:00:00')
  21. AND date < toDateTime('2026-01-01 00:00:00')
  22. GROUP BY name
  23. ORDER BY name
  24. LIMIT 1000001
  25. `,
  26. },
  27. },
  28.  
  29. monthlySourceData: {
  30. qlConnectionId: Editor.getId('CH_games'),
  31. data: {
  32. sql_query: `
  33. SELECT
  34. name,
  35. toMonth(toDateTime(date)) AS m,
  36. sum(toInt64(result)) AS profit
  37. FROM games.poker_log
  38. WHERE date >= toDateTime('2025-01-01 00:00:00')
  39. AND date < toDateTime('2026-01-01 00:00:00')
  40. GROUP BY name, m
  41. ORDER BY name, m
  42. LIMIT 1000001
  43. `,
  44. },
  45. },
  46. };
  47. Prepare
  48. // Prepare.js
  49. function getRowsSmart(loadedData, sourceName) {
  50. const src = loadedData?.[sourceName];
  51.  
  52. if (Array.isArray(src) && src.length && typeof src[0] === 'object' && !('event' in src[0])) {
  53. return src;
  54. }
  55.  
  56. if (Array.isArray(src) && src.length && typeof src[0] === 'object' && ('event' in src[0])) {
  57. const columnNames =
  58. src.filter(i => i.event === 'metadata').map(i => i.data?.names)?.[0] || [];
  59.  
  60. const res = [];
  61. src.filter(i => i.event === 'row').forEach(item => {
  62. const row = {};
  63. (item.data || []).forEach((v, i) => (row[columnNames[i]] = v));
  64. res.push(row);
  65. });
  66. return res;
  67. }
  68.  
  69. if (src && Array.isArray(src.data)) return src.data;
  70. return [];
  71. }
  72.  
  73. function norm(s) {
  74. return String(s || '').replace(/\s+/g, ' ').trim();
  75. }
  76.  
  77. function toNum(v) {
  78. const n = Number(v);
  79. return Number.isFinite(n) ? n : 0;
  80. }
  81.  
  82. function expand(v, k) {
  83. // небольшой запас по краям, чтобы подписи/столбцы не упирались
  84. // если v=0 → 0
  85. if (!Number.isFinite(v) || v === 0) return 0;
  86. return v > 0 ? Math.ceil(v * k) : Math.floor(v * k);
  87. }
  88.  
  89. const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','Totals'];
  90. const fmt = new Intl.NumberFormat('ru-RU');
  91.  
  92. const loaded = Editor.getLoadedData() || {};
  93. const players = getRowsSmart(loaded, 'playersSourceData');
  94. const monthly = getRowsSmart(loaded, 'monthlySourceData');
  95.  
  96. const params = (typeof Editor.getParams === 'function') ? (Editor.getParams() || {}) : {};
  97. let selected = norm((Array.isArray(params.name) && params.name[0]) ? params.name[0] : '');
  98. if (!selected && players.length) selected = norm(players[0].name);
  99.  
  100. // 12 месяцев
  101. const byMonth = Array.from({ length: 12 }, () => 0);
  102. for (const r of monthly) {
  103. if (selected && norm(r.name) !== selected) continue;
  104. const m = toNum(r.m);
  105. const p = toNum(r.profit);
  106. if (m >= 1 && m <= 12) byMonth[m - 1] = p;
  107. }
  108.  
  109. // Waterfall data
  110. const wf = byMonth.map((v, i) => ({ y: v, x: i }));
  111. wf.push({ total: true, x: 12 });
  112.  
  113. // Считаем траекторию subtotal, чтобы правильно подобрать min/max оси
  114. let subtotal = 0;
  115. let minSubtotal = 0;
  116. let maxSubtotal = 0;
  117. for (const v of byMonth) {
  118. subtotal += v;
  119. if (subtotal < minSubtotal) minSubtotal = subtotal;
  120. if (subtotal > maxSubtotal) maxSubtotal = subtotal;
  121. }
  122.  
  123. // Определяем режим оси
  124. const hasNeg = minSubtotal < 0;
  125. const hasPos = maxSubtotal > 0;
  126.  
  127. let yMin = null;
  128. let yMax = null;
  129.  
  130. if (hasNeg && !hasPos) {
  131. // только минуса
  132. yMax = 0;
  133. yMin = expand(minSubtotal, 1.02);
  134. } else if (!hasNeg && hasPos) {
  135. // только плюса
  136. yMin = 0;
  137. yMax = expand(maxSubtotal, 1.02);
  138. } else if (hasNeg && hasPos) {
  139. // и плюс и минус
  140. yMin = expand(minSubtotal, 1.02);
  141. yMax = expand(maxSubtotal, 1.02);
  142. } else {
  143. // всё 0
  144. yMin = -1;
  145. yMax = 1;
  146. }
  147.  
  148. module.exports = {
  149. chart: {
  150. margin: { left: 10, right: 10, top: 10, bottom: 10 },
  151. spacingBottom: 35,
  152. },
  153.  
  154. title: {
  155. text: selected ? `Profit 2025: ${selected}` : 'Profit 2025',
  156. },
  157.  
  158. series: {
  159. data: [
  160. {
  161. type: 'waterfall',
  162. data: wf,
  163. name: 'Profit',
  164. dataLabels: {
  165. enabled: true,
  166. formatter: function () {
  167. if (!this.y) return '';
  168. return fmt.format(this.y);
  169. },
  170. },
  171. },
  172. ],
  173. },
  174.  
  175. xAxis: {
  176. type: 'category',
  177. categories: MONTHS,
  178. labels: { rotation: -35 },
  179. },
  180.  
  181. yAxis: [
  182. {
  183. min: yMin,
  184. max: yMax,
  185. startOnTick: true,
  186. endOnTick: true,
  187. maxPadding: 0,
  188. minPadding: 0,
  189.  
  190. ticks: { pixelInterval: 120 },
  191. labels: {
  192. formatter: function () {
  193. return fmt.format(this.value);
  194. },
  195. },
  196. plotLines: [{ value: 0, width: 1 }],
  197. },
  198. ],
  199. };
  200. Controls
  201. // Controls.js
  202. function getRowsSmart(loadedData, sourceName) {
  203. const src = loadedData?.[sourceName];
  204.  
  205. if (Array.isArray(src) && src.length && typeof src[0] === 'object' && !('event' in src[0])) {
  206. return src;
  207. }
  208.  
  209. if (Array.isArray(src) && src.length && typeof src[0] === 'object' && ('event' in src[0])) {
  210. const columnNames =
  211. src.filter(i => i.event === 'metadata').map(i => i.data?.names)?.[0] || [];
  212.  
  213. const res = [];
  214. src.filter(i => i.event === 'row').forEach(item => {
  215. const row = {};
  216. (item.data || []).forEach((v, i) => (row[columnNames[i]] = v));
  217. res.push(row);
  218. });
  219. return res;
  220. }
  221.  
  222. if (src && Array.isArray(src.data)) return src.data;
  223. return [];
  224. }
  225.  
  226. function norm(s) {
  227. return String(s || '').replace(/\s+/g, ' ').trim();
  228. }
  229.  
  230. const loaded = Editor.getLoadedData() || {};
  231. const players = getRowsSmart(loaded, 'playersSourceData');
  232.  
  233. const content = players
  234. .map(p => norm(p.name))
  235. .filter(Boolean)
  236. .map(n => ({ title: n, value: n })); // value обязательно строка
  237.  
  238. module.exports = {
  239. controls: [
  240. {
  241. type: 'select',
  242. label: 'Игрок',
  243. labelPlacement: 'top',
  244. param: 'name',
  245. content,
  246. searchable: true,
  247. multiselect: false,
  248. required: false,
  249. updateOnChange: true, // <-- ВОТ ЭТО включает авто-применение и перерендер
  250. width: 280,
  251. },
  252. ],
  253. };
  254.  
Advertisement
Add Comment
Please, Sign In to add comment