mirror of
https://codeberg.org/artfulrobot/contactcats.git
synced 2025-06-25 12:58:05 +02:00
Improve vertical layout sankey
This commit is contained in:
parent
12ccb846e8
commit
dde3c8ec96
1 changed files with 69 additions and 42 deletions
|
@ -2,13 +2,14 @@
|
|||
angular.module("crmContactcats").component("crmContactCategoryFlows", {
|
||||
templateUrl: "~/crmContactcats/crmContactCategoryFlows.html",
|
||||
controller: function($scope, $timeout, crmApi4, crmStatus, $document) {
|
||||
const catHeightMin = 50, catHeightGap = 8;
|
||||
var ts = ($scope.ts = CRM.ts(null)),
|
||||
ctrl = this;
|
||||
|
||||
const catDefsIndexed = {};
|
||||
|
||||
// Development: set this true to use a small defined fixture instead of real data.
|
||||
const useTestFixtures = false;
|
||||
const useTestFixtures = true;
|
||||
|
||||
let allFlowsData;
|
||||
|
||||
|
@ -17,7 +18,6 @@
|
|||
? allFlowsData.filter(({from_category_id, to_category_id}) => from_category_id != to_category_id)
|
||||
: allFlowsData;
|
||||
|
||||
// Process the results into something useful.
|
||||
ctrl.loading = false;
|
||||
const labelWidth = ctrl.sankey.labelWidth,
|
||||
width = Math.max(600, document.querySelector('.contact-cats-sankey').clientWidth),
|
||||
|
@ -32,12 +32,32 @@
|
|||
ctrl.noStats = false;
|
||||
|
||||
// Create main analysis structure, indexed by right hand side of snakes.
|
||||
const analysis = {}, offsetWithinSourcesByCat = {};
|
||||
// analysis = {
|
||||
// <to-CatID>: {
|
||||
// {
|
||||
// cat: <the cat def object>, of the 'to' cat.
|
||||
// sources: [
|
||||
// {
|
||||
// cat: <the cat def object>, of the 'from' cat.
|
||||
// contact_count: <int>, the number of contacts moved from here.
|
||||
// },
|
||||
// ...
|
||||
// ],
|
||||
// now: <int>, How many contacts end up in this cat.
|
||||
// previous: <int>, How many contacts started in this cat.
|
||||
// y: <int>,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// offsetWithinSourcesByCat tracks, per source, the place the
|
||||
// next snake's left-top should be, relative to the top for that category.
|
||||
const analysis = {}, analysisArray = [], offsetWithinSourcesByCat = {};
|
||||
ctrl.catDefs.forEach(cat => {
|
||||
analysis[cat.id] = { sources: [], now: 0, previous: 0, cat };
|
||||
offsetWithinSourcesByCat[cat.id] = 0;
|
||||
analysis[cat.id] = { sources: [], now: 0, previous: 0, cat, y:0 };
|
||||
// This is just to make looping in order easier.
|
||||
analysisArray.push(analysis[cat.id]);
|
||||
});
|
||||
|
||||
let totalContacts = 0;
|
||||
flowsData.forEach(row => {
|
||||
const {from_category_id, to_category_id, contact_count} = row;
|
||||
|
@ -56,62 +76,70 @@
|
|||
totalContacts += contact_count;
|
||||
});
|
||||
|
||||
// allowing 50px per cat
|
||||
let scale = (ctrl.sankey.height - (ctrl.catDefs.length * 8)) / totalContacts;
|
||||
|
||||
let previousCatHeight = 0;
|
||||
ctrl.catDefs.forEach(cat => {
|
||||
// Now we know how many contacts per category per side.
|
||||
// Set the from offsets
|
||||
offsetWithinSourcesByCat[cat.id] = previousCatHeight;
|
||||
previousCatHeight += analysis[cat.id].previous * scale +8; // padding TODO: standardise
|
||||
// Allow a cat height to grow from its min to 3x min.
|
||||
let maxCatHeight = 3 * catHeightMin;
|
||||
let maxContactsAtOneCat = Math.max(... analysisArray.map(cat => Math.max(cat.now, cat.previous)));
|
||||
let scale = maxCatHeight / maxContactsAtOneCat;
|
||||
console.log({maxCatHeight, maxContactsAtOneCat,scale});
|
||||
|
||||
// Assign y-offsets for each category.
|
||||
// We start with a minimum so as to accommodate labels, but it may need to grow
|
||||
// to accommodate the total contacts in either end.
|
||||
let accumulatedY = 0;
|
||||
analysisArray.forEach(toAna => {
|
||||
toAna.y = accumulatedY;
|
||||
let thisCatHeight = Math.ceil(Math.max(catHeightMin, scale * Math.max(toAna.now, toAna.previous)));
|
||||
accumulatedY += thisCatHeight + catHeightGap;
|
||||
console.log({toCat: toAna.cat.label, toCatY: toAna.y, accumulatedY, thisCatHeight});
|
||||
// Intialise source offsets for this category.
|
||||
offsetWithinSourcesByCat[toAna.cat.id] = 0;
|
||||
// Make sure that our 'from' snake ends are ordered by category presentation_order
|
||||
// This avoids one categories snakes overlapping its others.
|
||||
analysis[cat.id].sources.sort((a, b) => {
|
||||
toAna.sources.sort((a, b) => {
|
||||
return a.cat.presentation_order - b.cat.presentation_order;
|
||||
})
|
||||
});
|
||||
ctrl.sankey.height = Math.ceil(accumulatedY);
|
||||
|
||||
// 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 => {
|
||||
const sourceHeight = parseInt(source.contact_count * scale);
|
||||
const gradientId = 'contactcats-gradient-' + source.cat.id + '-' + cat.id;
|
||||
analysisArray.forEach(toAna => {
|
||||
let offsetWithinTo = 0;
|
||||
toAna.sources.forEach(source => {
|
||||
const sourceHeight = source.contact_count * scale;
|
||||
const gradientId = 'contactcats-gradient-' + source.cat.id + '-' + toAna.cat.id;
|
||||
snakes.push({
|
||||
source,
|
||||
gradientId,
|
||||
toTop : yTopR,
|
||||
toHeight : Math.max(1,sourceHeight),
|
||||
toCat: toCat.cat,
|
||||
fromTop: offsetWithinSourcesByCat[source.cat.id],
|
||||
fromHeight: Math.max(1,sourceHeight), // from and to heights will be the same!
|
||||
toTop : toAna.y + offsetWithinTo,
|
||||
height : Math.max(1, Math.round(sourceHeight)),
|
||||
toCat: toAna.cat,
|
||||
fromTop: analysis[source.cat.id].y + offsetWithinSourcesByCat[source.cat.id],
|
||||
fromCat: source.cat
|
||||
});
|
||||
offsetWithinTo += sourceHeight;
|
||||
offsetWithinSourcesByCat[source.cat.id] += sourceHeight;
|
||||
yTopR += sourceHeight;
|
||||
});
|
||||
yTopR += 8;// padding TODO: apply this to the lhs too
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Create the SVG data for the snakes.
|
||||
//c${-snakeWidth/4},0,${-snakeWidth},${-snakeWidth*3/4},0,${-snakeWidth}, ${snake.fromTopOffset + snake.fromHeight - snake.toTop - snake.toHeight}
|
||||
const curve1 = snakeWidth/4, curve2 = snakeWidth*3/4;
|
||||
snakes.forEach(snake => {
|
||||
const dyBottom = snake.fromTop + snake.fromHeight - snake.toTop - snake.toHeight;
|
||||
const dyTop = -snake.fromTop + snake.toTop;
|
||||
snake.d = `M${snakeWidth},${snake.toTop} l0,${snake.toHeight}
|
||||
c${-curve1},0,${-curve2},${dyBottom},${-snakeWidth},${dyBottom}
|
||||
l0,${-snake.fromHeight}
|
||||
c${curve1},0,${curve2},${dyTop},${snakeWidth},${dyTop}
|
||||
z
|
||||
`;
|
||||
const dy = snake.fromTop - snake.toTop;
|
||||
snake.d = [
|
||||
// Move to top right of snake
|
||||
`M${snakeWidth},${snake.toTop}`,
|
||||
// Line down height of snake on right.
|
||||
`l0,${snake.height}`,
|
||||
// Curve to left bottom of snake.
|
||||
`c${-curve1},0,${-curve2},${dy},${-snakeWidth},${dy}`,
|
||||
// Up on left side
|
||||
`l0,${-snake.height}`,
|
||||
// Curve back to start.
|
||||
`c${curve1},0,${curve2},${-dy},${snakeWidth},${-dy}`,
|
||||
// Close shape.
|
||||
`z`,
|
||||
].join(' ');
|
||||
snake.fill = `url(#${snake.gradientId})`;
|
||||
});
|
||||
|
||||
|
@ -148,7 +176,6 @@
|
|||
crmApi4("ContactCategoryDefinition", 'get', { orderBy: { presentation_order: 'ASC' }, withLabels: true })
|
||||
);
|
||||
}
|
||||
ctrl.sankey.height = ctrl.catDefs.length * 50;
|
||||
ctrl.loading = false;
|
||||
ctrl.catDefs.forEach(cat => {catDefsIndexed[cat.id] = cat;});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue