in lib/@uncharted/strippets/src/strippets.outline.feature.js [185:409]
FeatureContent.prototype.renderEntities = function(entities) {
var $entities;
var entitiesRenderMap = [];
// get the number of pixels we will be working with, which is the height of the container - room for the entity at the bottom.
this.height = this.height || this.$outlineEntityContainer.height();
var entityPercentageHeight = (this.entityHeight / this.height);
var thresholdPercentageHeight = (this.Settings.entityLayoutThreshold / this.height);
// Don't try to use fround when it doesn't exist (IE)
if (Math.fround) {
// fround it to avoid precision errors.
entityPercentageHeight = Math.fround(entityPercentageHeight);
thresholdPercentageHeight = Math.fround(thresholdPercentageHeight);
}
// order entities first by weight then by position (assume between 0-1)
var orderedEntities = _.sortBy(entities || this.entities, function(entity) {
return Number(entity.data.firstPosition) - entity.weight;
});
if (orderedEntities && orderedEntities.length > 0 && entityPercentageHeight > 0 &&
(entityPercentageHeight !== this.entityPercentageHeight || !_.isEqual(this.entitiesShown, entities))) {
this.entityPercentageHeight = entityPercentageHeight;
this.entitiesShown = entities;
// first pass: place the entities if possible
orderedEntities.forEach(function(entity) {
var entityGroup = _.find(entitiesRenderMap, function(map) {
return (map.position.originalFrom <= Number(entity.data.firstPosition) &&
map.position.originalTo >= Number(entity.data.firstPosition)) ||
(map.position.originalFrom <= (Number(entity.data.firstPosition) + entityPercentageHeight) &&
map.position.originalTo >= (Number(entity.data.firstPosition) + entityPercentageHeight));
});
if (!entityGroup) {
entityGroup = {
position: new EntityRenderMap(entity.data.firstPosition, entityPercentageHeight),
entityKeys: {},
entities: [],
};
entitiesRenderMap.push(entityGroup);
}
var key;
if (entity.data.hasOwnProperty(consolidationField)) {
key = entity.data[consolidationField];
} else {
key = Object.keys(entityGroup).toString();
}
if (!entityGroup.entityKeys[key]) {
entityGroup.entityKeys[key] = [entityGroup.entities.length];
} else {
entityGroup.entityKeys[key].push(entityGroup.entities.length);
}
var entityMap = {
position: new EntityRenderMap(entity.data.firstPosition, entityPercentageHeight),
entity: entity,
hiddenEntities: [],
key: key,
};
entityGroup.entities.push(entityMap);
});
// second pass : try to place remaining entities if there is enough space. This can only happen if the configured threshold is greater than the size of entity.
// first make sure that all entities are ordered in ascending position sequence (weighting could have shifted everything around previously).
entitiesRenderMap = _.sortBy(entitiesRenderMap, function(map) {
return map.position.originalFrom;
});
if (thresholdPercentageHeight >= this.entityPercentageHeight) {
var index;
var length = entitiesRenderMap.length;
var list = entitiesRenderMap;
entitiesRenderMap = [];
for (index = 0; index < length; index++) {
// check if there is enough space given the threshold
var currentGroup = list[index];
var groupKeys = Object.keys(currentGroup.entityKeys);
// only reposition if the group has more than one entity
if (groupKeys.length > 1) {
var entityToFit;
var entityIndex;
var currentEntity = currentGroup.entities[0];
// if we already spread some entities around, account for them
var beforeSpace;
if (index > 0) {
var previous = list[index - 1].entities[0];
var lastRepositioned = entitiesRenderMap[entitiesRenderMap.length - 1];
if (lastRepositioned && lastRepositioned.position.finalTo > previous.position.finalTo) {
previous = lastRepositioned;
}
beforeSpace = Math.min(currentEntity.position.originalFrom - previous.position.finalTo, thresholdPercentageHeight);
} else {
beforeSpace = Math.min(currentEntity.position.originalFrom, thresholdPercentageHeight);
}
var afterSpace = Math.min(index < list.length - 1 ? list[index + 1].position.originalFrom - currentEntity.position.originalTo : 1 - currentEntity.position.originalTo, thresholdPercentageHeight);
// get available space, which is the available space before + the available space after + the space the entity takes up.
var availableSpace = beforeSpace
+ afterSpace
+ currentEntity.position.originalTo - currentEntity.position.originalFrom;
// number of entities to fit is however many is allowed in the given space.
var entitiesToFitCount = Math.floor(availableSpace / entityPercentageHeight);
// only reposition if there is enough room for more than 1 entity.
if (entitiesToFitCount > 1) {
var neededSpace = entitiesToFitCount * entityPercentageHeight;
// starting position should be the (available space FROM) + ((Available Space - Needed Space) / 2)
var availableSpaceFrom = currentEntity.position.originalFrom - beforeSpace;
var startingFrom = availableSpaceFrom + ((availableSpace - neededSpace) / 2);
// determine entity positioning (weight and priority don't get taken into account here as it's just placement)
// find which entities should be placed separately and which ones should be merged to the top most entity
var processedKeys = {};
var entitiesToFit = [];
currentGroup.entities.forEach(distributeEntities.bind(null, processedKeys, entitiesToFit, entitiesToFitCount, currentGroup));
// update new position
for (entityIndex = 0; entityIndex < entitiesToFit.length; entityIndex++) {
entityToFit = entitiesToFit[entityIndex];
entityToFit.position.finalFrom = startingFrom + (entityIndex * entityPercentageHeight);
entitiesRenderMap.push(entityToFit);
}
} else {
entitiesRenderMap.push(flattenGroup(currentGroup));
}
} else if (currentGroup.entities.length) {
entitiesRenderMap.push(flattenGroup(currentGroup));
}
}
}
$entities = entitiesRenderMap.map(function(map) {
map.entity.setPosition(map.position.finalFrom * 100);
if (map.hiddenEntities && map.hiddenEntities.length > 0) {
if (map.hiddenEntities[0].entity.data.hasOwnProperty(consolidationField)) {
// For the Uncertainty feature,
// 1. consolidate adjacent entities of the same entity ID
var consolidatedEntities = [map].concat(map.hiddenEntities).sort(function (a, b) {
if (a.entity.data[consolidationField] < b.entity.data[consolidationField]) {
return -1;
}
if (a.entity.data[consolidationField] > b.entity.data[consolidationField]) {
return 1;
}
return compareEntities(a, b);
});
var consolidatedEntityCount = consolidatedEntities.length;
var types = [];
var uniqueEntities = [];
var i;
for (i = 0; i < consolidatedEntityCount; i++) {
if (i < 1 || consolidatedEntities[i].entity.data[consolidationField] !==
consolidatedEntities[i - 1].entity.data[consolidationField]) {
uniqueEntities.push(consolidatedEntities[i]);
pushTypes(uniqueEntities, 2, types);
types = [consolidatedEntities[i].entity.data];
} else {
types.push(consolidatedEntities[i].entity.data);
}
}
pushTypes(uniqueEntities, 1, types);
uniqueEntities.sort(compareEntities);
consolidatedEntityCount = uniqueEntities.length;
// 2. label the resulting entity with its types, in bucket order, e.g. Amazon [LOC, ORG], Samsung, ...
var appendTooltip = function (a, b) {
var result = a;
if (b.type !== uniqueEntities[i].entity.data.name) {
if (a) {
result += ', ';
}
result += b.type;
}
return result;
};
var tooltip = '';
for (i = 0; i < consolidatedEntityCount; i++) {
if (tooltip.length) {
tooltip += ', ';
}
tooltip += uniqueEntities[i].entity.data.name;
if (uniqueEntities[i].types && !identical(uniqueEntities[i].types, getEntityType)) {
tooltip += ' [' + uniqueEntities[i].types.reduce(appendTooltip, '') + ']';
}
}
map.entity.setAttributes({
'data-hidden-entities': consolidatedEntityCount > 1 ? consolidatedEntityCount : null,
'data-entities': tooltip,
});
} else {
map.entity.setAttributes({
'data-hidden-entities': map.hiddenEntities.length,
'data-entities': [map].concat(map.hiddenEntities).sort(compareEntities).reduce(
function(memo, m) {
return (memo !== '' ? memo + ', ' : '') + m.entity.data.name;
}, ''),
});
}
} else {
map.entity.setAttributes({
'data-hidden-entities': null,
'data-entities': map.entity.data.name,
});
}
map.entity.highlight(map.entity.isHighlight, map.entity.color);
return map.entity.$entity;
});
this.$outlineEntityContainer.html($entities);
}
};