Skip to content

Commit

Permalink
feat(explorer): add code layout
Browse files Browse the repository at this point in the history
  • Loading branch information
phodal committed Mar 5, 2021
1 parent 1aff4aa commit e26f220
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 0 deletions.
1 change: 1 addition & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ <h2>Commits Tree</h2>
<script src="public/js/plugins/d3-voronoi-map.js"></script>
<script src="public/js/plugins/d3-voronoi-treemap.js"></script>
<script src="public/js/plugins/seedrandom.min.js"></script>
<script src="public/js/plugins/code-layout.js"></script>

<script src="public/js/graph-config.js"></script>

Expand Down
2 changes: 2 additions & 0 deletions web/public/js/graph/git/file-explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function renderCodeExplorer(freedom, data, elementId) {
}
});

let some = calculateCodeLayout(data);
console.log(some);
let freedom_nest = d3.group(freedom_year, d => d.region_simple)
let data_nested = {key: "freedom_nest", values: freedom_nest}

Expand Down
316 changes: 316 additions & 0 deletions web/public/js/plugins/code-layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
// MIT License
//
// Copyright (c) 2020 Korny Sietsma
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
const debug = false;

function computeCirclingPolygon(points, radius) {
const increment = (2 * Math.PI) / points;
const circlingPolygon = [];

for (let a = 0, i = 0; i < points; i++, a += increment) {
circlingPolygon.push([radius * Math.cos(a), radius * Math.sin(a)]);
}

return circlingPolygon;
}

function flareWeightLoc(d) {
if (d.data === undefined) return 0;
if (d.data.loc === undefined) return 0;
return d.data.loc.code;
}

function pruneWeightlessNodes(hierarchy) {
if (hierarchy.children !== undefined) {
// eslint-disable-next-line no-param-reassign
hierarchy.children = hierarchy.children.filter((node) => node.value > 0);
hierarchy.children.forEach((child) => pruneWeightlessNodes(child));
}
}

function addPaths(pathSoFar, node) {
let path;
if (pathSoFar === null) {
path = ''; // not 'flare' - could use '/' or null - but this is nicer for output
} else {
if (pathSoFar === '') {
path = node.name;
} else {
path = `${pathSoFar}/${node.name}`;
}
}
const children = node.children
? node.children.map((n) => addPaths(path, n))
: undefined;
return {
name: node.name,
path,
children: children,
layout: node.layout,
value: node.value,
data: node.data,
};
}

function calculate_values(node) {
if (node.children) {
for (const n of node.children) {
calculate_values(n);
}
const tot = node.children.map((n) => n.value).reduce((a, b) => a + b, 0);
node.value = tot;
} else {
node.value = flareWeightLoc(node);
}
}

function calculateVoronoi(
nameSoFar,
node,
clipPolygon,
center,
goodenough,
depth
) {
const name = nameSoFar ? `${nameSoFar}/${node.name}` : node.name;
node.layout = {
polygon: clipPolygon,
center,
algorithm: 'voronoi',
};

if (!node.children) {
return;
}
if (depth < 3) {
console.warn(`calculating voronoi for ${name}`);
} else if (depth === 3) {
console.warn(`calculating voronoi for ${name} and descendants`);
}
if (debug) {
console.warn(
`calculating voronoi for ${name} with ${node.children.length} children and a clip polygon with ${clipPolygon.length} vertices`
);
}

const MAX_SIMULATION_COUNT = 200; // we re-run the whole simulation this many times if it fails
const MAX_ITERATION_COUNT = 500; // this is how many times a particular simulation iterates
const MIN_WEIGHT_RATIO = 0.005; // maybe this should be a parameter? Too high, we iterate a lot. Too low, sizes are not proportional to lines of code.
let simulationCount = 0;
let simulationLoopEnded = false;
let bestConvergenceRatio = 1.0;
let bestPolygons = undefined;
while (!simulationLoopEnded) {
try {
var simulation = d3.voronoiMapSimulation(node.children)
.maxIterationCount(MAX_ITERATION_COUNT)
.minWeightRatio(MIN_WEIGHT_RATIO)
.weight((d) => d.value)
.clip(clipPolygon)
.stop();

var state = simulation.state();

let tickCount = 0;
let warningTime = Date.now();
while (!state.ended) {
tickCount += 1;
const now = Date.now();
if (now - warningTime > 10000) {
// every 10 seconds
warningTime = now;
console.warn(
`slow voronoi processing of ${name} with ${node.children.length} children, tick count: ${tickCount}`
);
}
simulation.tick();
state = simulation.state();
}
if (tickCount === MAX_ITERATION_COUNT) {
if (state.convergenceRatio < bestConvergenceRatio) {
if (debug) {
console.warn(
'best iteration result so far',
simulationCount,
state.convergenceRatio
);
}
bestConvergenceRatio = state.convergenceRatio;
bestPolygons = [...state.polygons];
}

if (simulationCount < MAX_SIMULATION_COUNT) {
simulationCount = simulationCount + 1;

console.warn(
`processing ${name} with ${node.children.length} children - Exceeded tick count ${tickCount} - retrying from scratch, try ${simulationCount}`
);
} else {
console.error('Too many meta retries - stopping');
simulationLoopEnded = true;
if (!goodenough) {
throw Error("Too many retries, can't provide good simulation");
} else {
console.warn('returning good-enough result', bestConvergenceRatio);
}
}
} else {
if (bestPolygons) {
console.warn(
'successful converging layout, using real ratio not best-so-far: ',
state.convergenceRatio
);
bestPolygons = undefined;
bestConvergenceRatio = state.convergenceRatio;
}
simulationLoopEnded = true;
}
} catch (e) {
// re-try from scratch but only after predictable exceptions
console.warn('caught e', e.message);
if (!(e instanceof Error) && !(e instanceof TypeError)) {
console.error('not Error or TypeError');
throw e;
}
if (
e.message === 'handleOverweighted1 is looping too much' ||
e.message ===
'at least 1 site has no area, which is not supposed to arise'
) {
simulationCount = simulationCount + 1;
if (simulationCount < MAX_SIMULATION_COUNT) {
console.warn(
`caught ${e.message}, retrying from scratch`,
simulationCount
);
} else {
console.error(
`caught ${e.message}, too many errors!`,
simulationCount
);
simulationLoopEnded = true;
if (!goodenough) {
throw Error("Too many retries, can't provide good simulation");
} else {
console.warn('returning good-enough result', bestConvergenceRatio);
}
}
} else {
console.error(
`unhandled exception ${e.name}:${e.message} - rethrowing`
);
throw e;
}
}
}
var polygons = state.polygons;
if (bestPolygons) {
console.error(
'No good layout found - using best convergence ratio',
bestConvergenceRatio
);
polygons = bestPolygons;
} else {
if (debug) {
console.warn(
'Successful layout - best convergence ratio',
state.convergenceRatio
);
}
}

for (const polygon of polygons) {
const pdata = polygon.map((d) => d);
calculateVoronoi(
name,
polygon.site.originalObject.data.originalData,
pdata,
[polygon.site.x, polygon.site.y],
goodenough,
depth + 1
);
}
}

function calculateCodeLayout(input) {
return codeLayout(input, 128, false);
}

function codeLayout(input, points, circles) {
const parsedData = input
const width = 1024;

console.warn('getting values recursively');
calculate_values(parsedData);
console.warn('pruning empty nodes');
pruneWeightlessNodes(parsedData);

// top level clip shape
if (circles) {
// area = pi r^2 so r = sqrt(area/pi) or just use sqrt(area) for simplicity
const children = parsedData.children.map((child) => {
return { r: Math.sqrt(child.value), originalObject: child };
});
d3.packSiblings(children);
// top level layout
const enclosingCirle = d3.packEnclose(children);
const { x, y, r } = enclosingCirle;
// TODO: offset by x/y
parsedData.layout = {
polygon: computeCirclingPolygon(points, r),
center: [0, 0],
width: r * 2,
height: r * 2,
algorithm: 'circlePack',
};

for (const child of children) {
const clipPolygon = computeCirclingPolygon(
points,
child.r
).map(([x, y]) => [x + child.x, y + child.y]);
const center = [child.x, child.y];

calculateVoronoi(
child.originalObject.name,
child.originalObject,
clipPolygon,
center,
true,
1
);
child.originalObject.layout.width = child.r;
child.originalObject.layout.height = child.r;
}
} else {
const clipPolygon = computeCirclingPolygon(points, width / 2);
const center = [0, 0];

calculateVoronoi(null, parsedData, clipPolygon, center, true, 0);

parsedData.layout.width = width;
parsedData.layout.height = width;
}

const results = addPaths(null, parsedData);

return results;
}

0 comments on commit e26f220

Please sign in to comment.