WIP checkin

This commit is contained in:
Rich Lott / Artful Robot 2025-03-26 12:16:48 +00:00
parent 2ae781f6e3
commit f88fb5f10e
3 changed files with 189 additions and 227 deletions

View file

@ -2,220 +2,6 @@
// Declare a list of dependencies.
angular.module("crmContactcats", CRM.angRequires("crmContactcats"));
angular.module("crmContactcats").component("crmContactCategoryFlows", {
templateUrl: "~/crmContactcats/crmContactCategoryFlows.html",
bindings: {
// things listed here become properties on the controller using
// values from attributes. @ means a fixed string is passed
// e.g. <crm-reset-password token='foo' />
// token: '@',
// & is special. In the child, call ctrl.onMyAction with an
// Object whose keys provide parameter names for the code in the parent.
// onMyAction: '&',
// '<' means one-way binding (parent»child) and I think is what you are
// supposed to use for components.
// v: '<'
},
controller: function($scope, $timeout, crmApi4, crmStatus, $document) {
var ts = ($scope.ts = CRM.ts(null)),
ctrl = this;
// function prepareSankey(analysis) {
// // analysis is like:
// // { string <newCatID>: {
// // sources: {
// // string <previousCatId>: int <n>
// // },
// // now: int <n>, previous: int <n>
// // }}
//
// // Snakes are vertically positioned cumulatively.
// const snakes = ctrl.catDefs.forEach((cat, i) => {
//
//
// });
//
//
// // Flatten to single array, then sort.
// let allLabels = new Set();
// let maxPerLabel = 0;
// let snakes = [];
// console.log('analy', analysis);
//
// Object.entries(analysis).forEach(([newLabel, data]) => {
// console.log({newLabel, data});
// allLabels.add(newLabel);
// maxPerLabel = Math.max(maxPerLabel, data.now, data.previous);
// Object.entries(data.sources).forEach(([previousLabel, n]) => {
// allLabels.add(previousLabel);
// snakes.push({ previousLabel, n, newLabel });
// });
// });
// // Get sorted list of labels for our axis
// allLabels = Array.from(allLabels);
// allLabels.sort(labelSort);
// console.log(allLabels);
//
// // Sort the snakes
// snakes.sort((a, b) => {
// // We want retentions first.
// if (a.previousLabel === a.newLabel && b.previousLabel !== b.newLabel) return -1;
// if (a.previousLabel !== a.newLabel && b.previousLabel === b.newLabel) return 1;
// // Next we want destination labels
// let c = allLabels.indexOf(b.newLabel) - allLabels.indexOf(a.newLabel);
// if (c !== 0) return c;
// c = allLabels.indexOf(a.previousLabel) - allLabels.indexOf(b.previousLabel);
// if (c !== 0) return c;
// // By size.
// return b.n - a.n;
// });
//
// const labelWidth = 150,
// width = Math.max(600, document.getElementById('rfm-width').firstElementChild.clientWidth),
// domainWidth = width - 2*labelWidth;
// ctrl.sankey = {
// height: Math.min(600, allLabels.length * 100), /* allow up to 100px per label */
// width,
// labelWidth,
// rows: snakes
// };
//
// const heightPerLabel = Math.floor(ctrl.sankey.height / allLabels.length);
// const padding = 24;
// const yScale = (heightPerLabel - padding) / maxPerLabel;
// const labelYs = {};
// allLabels.forEach((label, i) => {
// const y = i*heightPerLabel;
// labelYs[label] = { initial: y, previous: y, now: y };
// });
// ctrl.sankey.labels = allLabels.map(label => ({label: label || '(Uncategorised)', y: labelYs[label].initial}));
// console.log("Sankey", {heightPerLabel, padding, yScale, labelYs, width: ctrl.sankey.width, snakes, allLabels});
//
// let g=1; // gradient ID
// ctrl.sankey.rows.forEach(row => {
// let yPrevious = labelYs[row.previousLabel].previous,
// h = yScale * row.n,
// yNow = labelYs[row.newLabel].now;
// labelYs[row.previousLabel].previous += h;
// labelYs[row.newLabel].now += h;
// // Create path
// row.d = `M${labelWidth},${yPrevious} l16,0
// C ${labelWidth + domainWidth/4},${yPrevious} ${labelWidth + domainWidth*3/4},${yNow} ${ctrl.sankey.width - labelWidth -16},${yNow}
// l16,0
// l0,${h}
// l-16,0
// C ${labelWidth + domainWidth*3/4},${yNow+h} ${labelWidth + domainWidth/4},${yPrevious+h} ${labelWidth+16},${yPrevious + h}
// l-16,0 z`;
// row.fill = `hsl(${ctrl.getHueFromLabel(row.previousLabel)}, 50%, 50%)`;
// row.fill = `url(#g${g})`;
// row.gradient = {id: 'g' + g++,
// to: `hsl(${ctrl.getHueFromLabel(row.previousLabel)}, 50%, 50%)`,
// from: `hsl(${ctrl.getHueFromLabel(row.newLabel)}, 50%, 50%)`,
// };
// });
//
// // console.log("Sankey", {sankey: ctrl.sankey});
// }
// this.$onInit gets run after the this controller is called, and after the bindings have been applied.
this.$onInit = async function() {
// Default start date to 1 month ago.
ctrl.loading = true;
ctrl.startDate = new Date();
ctrl.startDate.setMonth(ctrl.startDate.getMonth() - 1);
ctrl.startDate.setHours(0);
ctrl.startDate.setMinutes(0);
ctrl.startDate.setSeconds(0);
ctrl.startDate = new Date('2025-03-10');
ctrl.endDate = null;
ctrl.stats = null;
ctrl.catDefs = await crmStatus({ start: ts('Loading...'), success: ''},
crmApi4("ContactCategoryDefinition", 'get', { orderBy: { presentation_order: 'ASC' }, withLabels: true })
);
ctrl.loading = false;
console.log(ctrl.catDefs);
ctrl.fetchStats = function() {
ctrl.loading = true;
crmApi4('ContactCategory', 'getFlows', {
startDate: ctrl.startDate.toISOString().substring(0, 10),
endDate: ctrl.endDate ? ctrl.endDate.toISOString().substring(0, 10) : '',
}).then(r => {
// Process the results into something useful.
ctrl.loading = false;
console.log("results", r);
if (!(r.length)) {
ctrl.noStats = true;
ctrl.stats = [];
return;
}
ctrl.noStats = false;
// Create main analysis structure, indexed by right hand side of snakes.
const analysis = {}, offsetWithinSourcesByCat = {};
ctrl.catDefs.forEach(cat => {
analysis[cat.id] = { sources: [], now: 0, previous: 0, cat };
offsetWithinSourcesByCat[cat.id] = 0;
});
let totalContacts = 0;
r.forEach(row => {
const {from_category_id, to_category_id, contact_count} = row;
analysis[to_category_id].now += contact_count;
if (!from_category_id) {
// from nowhere: ignore for now.
console.log("Skipping", row);
return;
}
analysis[from_category_id].previous += contact_count;
analysis[to_category_id].sources.push({ cat: ctrl.catDefs[from_category_id], contact_count });
totalContacts += analysis[to_category_id].now;
});
// allowing 50px per cat
let scale = (ctrl.catDefs.length * 50) / totalContacts;
// Make sure that our 'from' snake ends are ordered by category presentation_order
// This avoids one categories snakes overlapping its others.
ctrl.catDefs.forEach(cat => {
analysis[cat.id].sources.sort((a, b) => {
return a.cat.presentation_order - b.cat.presentation_order;
})
});
// Make a list of snakes we need to draw.
const snakes = [];
let yTopR = 0;
ctrl.catDefs.forEach(cat => {
let toCat = analysis[cat.id];
toCat.yTop = yTopR;
toCat.sources.forEach(source => {
snakes.push({
toTop : yTopR,
toHeight : source.contact_count * scale,
toCat,
fromTopOffset: offsetWithinSourcesByCat[source.cat.id],
fromHeight: source.contact_count * scale,
fromCat: source.cat
});
offsetWithinSourcesByCat[source.cat.id] += source.contact_count * scale;
yTopR += source.contact_count * scale;
});
yTopR += 8;// padding
});
ctrl.snakes = snakes;
console.log({snakes, catDefs: ctrl.catDefs, analysis});
});
}
// do initial fetch.
ctrl.fetchStats();
};
},
});
angular.module("crmContactcats").component("crmContactCategorySettings", {
templateUrl: "~/crmContactcats/crmContactCategorySettings.html",
bindings: {