layout.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.scaleSolution = exports.normalizeSolution = exports.disjointCluster = exports.lossFunction = exports.greedyLayout = exports.constrainedMDSLayout = exports.bestInitialLayout = exports.getDistanceMatrices = exports.distanceFromIntersectArea = exports.venn = void 0;
  4. var fmin_1 = require("fmin");
  5. var circleintersection_1 = require("./circleintersection");
  6. /** given a list of set objects, and their corresponding overlaps.
  7. updates the (x, y, radius) attribute on each set such that their positions
  8. roughly correspond to the desired overlaps */
  9. function venn(areas, parameters) {
  10. parameters = parameters || {};
  11. parameters.maxIterations = parameters.maxIterations || 500;
  12. var initialLayout = parameters.initialLayout || bestInitialLayout;
  13. var loss = parameters.lossFunction || lossFunction;
  14. // add in missing pairwise areas as having 0 size
  15. areas = addMissingAreas(areas);
  16. // initial layout is done greedily
  17. var circles = initialLayout(areas, parameters);
  18. // transform x/y coordinates to a vector to optimize
  19. var initial = [], setids = [];
  20. var setid;
  21. for (setid in circles) {
  22. // eslint-disable-next-line
  23. if (circles.hasOwnProperty(setid)) {
  24. initial.push(circles[setid].x);
  25. initial.push(circles[setid].y);
  26. setids.push(setid);
  27. }
  28. }
  29. // optimize initial layout from our loss function
  30. var solution = (0, fmin_1.nelderMead)(function (values) {
  31. var current = {};
  32. for (var i = 0; i < setids.length; ++i) {
  33. var setid_1 = setids[i];
  34. current[setid_1] = {
  35. x: values[2 * i],
  36. y: values[2 * i + 1],
  37. radius: circles[setid_1].radius,
  38. // size : circles[setid].size
  39. };
  40. }
  41. return loss(current, areas);
  42. }, initial, parameters);
  43. // transform solution vector back to x/y points
  44. var positions = solution.x;
  45. for (var i = 0; i < setids.length; ++i) {
  46. setid = setids[i];
  47. circles[setid].x = positions[2 * i];
  48. circles[setid].y = positions[2 * i + 1];
  49. }
  50. return circles;
  51. }
  52. exports.venn = venn;
  53. var SMALL = 1e-10;
  54. /** Returns the distance necessary for two circles of radius r1 + r2 to
  55. have the overlap area 'overlap' */
  56. function distanceFromIntersectArea(r1, r2, overlap) {
  57. // handle complete overlapped circles
  58. if (Math.min(r1, r2) * Math.min(r1, r2) * Math.PI <= overlap + SMALL) {
  59. return Math.abs(r1 - r2);
  60. }
  61. return (0, fmin_1.bisect)(function (distance) {
  62. return (0, circleintersection_1.circleOverlap)(r1, r2, distance) - overlap;
  63. }, 0, r1 + r2);
  64. }
  65. exports.distanceFromIntersectArea = distanceFromIntersectArea;
  66. /** Missing pair-wise intersection area data can cause problems:
  67. treating as an unknown means that sets will be laid out overlapping,
  68. which isn't what people expect. To reflect that we want disjoint sets
  69. here, set the overlap to 0 for all missing pairwise set intersections */
  70. function addMissingAreas(areas) {
  71. areas = areas.slice();
  72. // two circle intersections that aren't defined
  73. var ids = [], pairs = {};
  74. var i, j, a, b;
  75. for (i = 0; i < areas.length; ++i) {
  76. var area = areas[i];
  77. if (area.sets.length == 1) {
  78. ids.push(area.sets[0]);
  79. }
  80. else if (area.sets.length == 2) {
  81. a = area.sets[0];
  82. b = area.sets[1];
  83. // @ts-ignore
  84. pairs[[a, b]] = true;
  85. // @ts-ignore
  86. pairs[[b, a]] = true;
  87. }
  88. }
  89. ids.sort(function (a, b) {
  90. return a > b ? 1 : -1;
  91. });
  92. for (i = 0; i < ids.length; ++i) {
  93. a = ids[i];
  94. for (j = i + 1; j < ids.length; ++j) {
  95. b = ids[j];
  96. // @ts-ignore
  97. if (!([a, b] in pairs)) {
  98. areas.push({ sets: [a, b], size: 0 });
  99. }
  100. }
  101. }
  102. return areas;
  103. }
  104. /// Returns two matrices, one of the euclidean distances between the sets
  105. /// and the other indicating if there are subset or disjoint set relationships
  106. function getDistanceMatrices(areas, sets, setids) {
  107. // initialize an empty distance matrix between all the points
  108. var distances = (0, fmin_1.zerosM)(sets.length, sets.length), constraints = (0, fmin_1.zerosM)(sets.length, sets.length);
  109. // compute required distances between all the sets such that
  110. // the areas match
  111. areas
  112. .filter(function (x) {
  113. return x.sets.length == 2;
  114. })
  115. .map(function (current) {
  116. var left = setids[current.sets[0]], right = setids[current.sets[1]], r1 = Math.sqrt(sets[left].size / Math.PI), r2 = Math.sqrt(sets[right].size / Math.PI), distance = distanceFromIntersectArea(r1, r2, current.size);
  117. distances[left][right] = distances[right][left] = distance;
  118. // also update constraints to indicate if its a subset or disjoint
  119. // relationship
  120. var c = 0;
  121. if (current.size + 1e-10 >= Math.min(sets[left].size, sets[right].size)) {
  122. c = 1;
  123. }
  124. else if (current.size <= 1e-10) {
  125. c = -1;
  126. }
  127. constraints[left][right] = constraints[right][left] = c;
  128. });
  129. return { distances: distances, constraints: constraints };
  130. }
  131. exports.getDistanceMatrices = getDistanceMatrices;
  132. /// computes the gradient and loss simulatenously for our constrained MDS optimizer
  133. function constrainedMDSGradient(x, fxprime, distances, constraints) {
  134. var loss = 0, i;
  135. for (i = 0; i < fxprime.length; ++i) {
  136. fxprime[i] = 0;
  137. }
  138. for (i = 0; i < distances.length; ++i) {
  139. var xi = x[2 * i], yi = x[2 * i + 1];
  140. for (var j = i + 1; j < distances.length; ++j) {
  141. var xj = x[2 * j], yj = x[2 * j + 1], dij = distances[i][j], constraint = constraints[i][j];
  142. var squaredDistance = (xj - xi) * (xj - xi) + (yj - yi) * (yj - yi), distance_1 = Math.sqrt(squaredDistance), delta = squaredDistance - dij * dij;
  143. if ((constraint > 0 && distance_1 <= dij) || (constraint < 0 && distance_1 >= dij)) {
  144. continue;
  145. }
  146. loss += 2 * delta * delta;
  147. fxprime[2 * i] += 4 * delta * (xi - xj);
  148. fxprime[2 * i + 1] += 4 * delta * (yi - yj);
  149. fxprime[2 * j] += 4 * delta * (xj - xi);
  150. fxprime[2 * j + 1] += 4 * delta * (yj - yi);
  151. }
  152. }
  153. return loss;
  154. }
  155. /// takes the best working variant of either constrained MDS or greedy
  156. function bestInitialLayout(areas, params) {
  157. var initial = greedyLayout(areas, params);
  158. var loss = params.lossFunction || lossFunction;
  159. // greedylayout is sufficient for all 2/3 circle cases. try out
  160. // constrained MDS for higher order problems, take its output
  161. // if it outperforms. (greedy is aesthetically better on 2/3 circles
  162. // since it axis aligns)
  163. if (areas.length >= 8) {
  164. var constrained = constrainedMDSLayout(areas, params), constrainedLoss = loss(constrained, areas), greedyLoss = loss(initial, areas);
  165. if (constrainedLoss + 1e-8 < greedyLoss) {
  166. initial = constrained;
  167. }
  168. }
  169. return initial;
  170. }
  171. exports.bestInitialLayout = bestInitialLayout;
  172. /// use the constrained MDS variant to generate an initial layout
  173. function constrainedMDSLayout(areas, params) {
  174. params = params || {};
  175. var restarts = params.restarts || 10;
  176. // bidirectionally map sets to a rowid (so we can create a matrix)
  177. var sets = [], setids = {};
  178. var i;
  179. for (i = 0; i < areas.length; ++i) {
  180. var area = areas[i];
  181. if (area.sets.length == 1) {
  182. setids[area.sets[0]] = sets.length;
  183. sets.push(area);
  184. }
  185. }
  186. var matrices = getDistanceMatrices(areas, sets, setids);
  187. var distances = matrices.distances;
  188. var constraints = matrices.constraints;
  189. // keep distances bounded, things get messed up otherwise.
  190. // TODO: proper preconditioner?
  191. var norm = (0, fmin_1.norm2)(distances.map(fmin_1.norm2)) / distances.length;
  192. distances = distances.map(function (row) {
  193. return row.map(function (value) {
  194. return value / norm;
  195. });
  196. });
  197. var obj = function (x, fxprime) {
  198. return constrainedMDSGradient(x, fxprime, distances, constraints);
  199. };
  200. var best, current;
  201. for (i = 0; i < restarts; ++i) {
  202. var initial = (0, fmin_1.zeros)(distances.length * 2).map(Math.random);
  203. current = (0, fmin_1.conjugateGradient)(obj, initial, params);
  204. if (!best || current.fx < best.fx) {
  205. best = current;
  206. }
  207. }
  208. var positions = best.x;
  209. // translate rows back to (x,y,radius) coordinates
  210. var circles = {};
  211. for (i = 0; i < sets.length; ++i) {
  212. var set = sets[i];
  213. circles[set.sets[0]] = {
  214. x: positions[2 * i] * norm,
  215. y: positions[2 * i + 1] * norm,
  216. radius: Math.sqrt(set.size / Math.PI),
  217. };
  218. }
  219. if (params.history) {
  220. for (i = 0; i < params.history.length; ++i) {
  221. (0, fmin_1.scale)(params.history[i].x, norm);
  222. }
  223. }
  224. return circles;
  225. }
  226. exports.constrainedMDSLayout = constrainedMDSLayout;
  227. /** Lays out a Venn diagram greedily, going from most overlapped sets to
  228. least overlapped, attempting to position each new set such that the
  229. overlapping areas to already positioned sets are basically right */
  230. function greedyLayout(areas, params) {
  231. var loss = params && params.lossFunction ? params.lossFunction : lossFunction;
  232. // define a circle for each set
  233. var circles = {}, setOverlaps = {};
  234. var set;
  235. for (var i = 0; i < areas.length; ++i) {
  236. var area = areas[i];
  237. if (area.sets.length == 1) {
  238. set = area.sets[0];
  239. circles[set] = {
  240. x: 1e10,
  241. y: 1e10,
  242. // rowid: circles.length, // fix to ->
  243. rowid: Object.keys(circles).length,
  244. size: area.size,
  245. radius: Math.sqrt(area.size / Math.PI),
  246. };
  247. setOverlaps[set] = [];
  248. }
  249. }
  250. areas = areas.filter(function (a) {
  251. return a.sets.length == 2;
  252. });
  253. // map each set to a list of all the other sets that overlap it
  254. for (var i = 0; i < areas.length; ++i) {
  255. var current = areas[i];
  256. // eslint-disable-next-line
  257. var weight = current.hasOwnProperty('weight') ? current.weight : 1.0;
  258. var left = current.sets[0], right = current.sets[1];
  259. // completely overlapped circles shouldn't be positioned early here
  260. if (current.size + SMALL >= Math.min(circles[left].size, circles[right].size)) {
  261. weight = 0;
  262. }
  263. setOverlaps[left].push({ set: right, size: current.size, weight: weight });
  264. setOverlaps[right].push({ set: left, size: current.size, weight: weight });
  265. }
  266. // get list of most overlapped sets
  267. var mostOverlapped = [];
  268. for (set in setOverlaps) {
  269. // eslint-disable-next-line
  270. if (setOverlaps.hasOwnProperty(set)) {
  271. var size = 0;
  272. for (var i = 0; i < setOverlaps[set].length; ++i) {
  273. size += setOverlaps[set][i].size * setOverlaps[set][i].weight;
  274. }
  275. mostOverlapped.push({ set: set, size: size });
  276. }
  277. }
  278. // sort by size desc
  279. function sortOrder(a, b) {
  280. return b.size - a.size;
  281. }
  282. mostOverlapped.sort(sortOrder);
  283. // keep track of what sets have been laid out
  284. var positioned = {};
  285. function isPositioned(element) {
  286. return element.set in positioned;
  287. }
  288. // adds a point to the output
  289. function positionSet(point, index) {
  290. circles[index].x = point.x;
  291. circles[index].y = point.y;
  292. positioned[index] = true;
  293. }
  294. // add most overlapped set at (0,0)
  295. positionSet({ x: 0, y: 0 }, mostOverlapped[0].set);
  296. // get distances between all points. TODO, necessary?
  297. // answer: probably not
  298. // var distances = venn.getDistanceMatrices(circles, areas).distances;
  299. for (var i = 1; i < mostOverlapped.length; ++i) {
  300. var setIndex = mostOverlapped[i].set, overlap = setOverlaps[setIndex].filter(isPositioned);
  301. set = circles[setIndex];
  302. overlap.sort(sortOrder);
  303. if (overlap.length === 0) {
  304. // this shouldn't happen anymore with addMissingAreas
  305. throw 'ERROR: missing pairwise overlap information';
  306. }
  307. var points = [];
  308. for (var j = 0; j < overlap.length; ++j) {
  309. // get appropriate distance from most overlapped already added set
  310. var p1 = circles[overlap[j].set], d1 = distanceFromIntersectArea(set.radius, p1.radius, overlap[j].size);
  311. // sample positions at 90 degrees for maximum aesthetics
  312. points.push({ x: p1.x + d1, y: p1.y });
  313. points.push({ x: p1.x - d1, y: p1.y });
  314. points.push({ y: p1.y + d1, x: p1.x });
  315. points.push({ y: p1.y - d1, x: p1.x });
  316. // if we have at least 2 overlaps, then figure out where the
  317. // set should be positioned analytically and try those too
  318. for (var k = j + 1; k < overlap.length; ++k) {
  319. var p2 = circles[overlap[k].set], d2 = distanceFromIntersectArea(set.radius, p2.radius, overlap[k].size);
  320. var extraPoints = (0, circleintersection_1.circleCircleIntersection)({ x: p1.x, y: p1.y, radius: d1 }, { x: p2.x, y: p2.y, radius: d2 });
  321. for (var l = 0; l < extraPoints.length; ++l) {
  322. points.push(extraPoints[l]);
  323. }
  324. }
  325. }
  326. // we have some candidate positions for the set, examine loss
  327. // at each position to figure out where to put it at
  328. var bestLoss = 1e50, bestPoint = points[0];
  329. for (var j = 0; j < points.length; ++j) {
  330. circles[setIndex].x = points[j].x;
  331. circles[setIndex].y = points[j].y;
  332. var localLoss = loss(circles, areas);
  333. if (localLoss < bestLoss) {
  334. bestLoss = localLoss;
  335. bestPoint = points[j];
  336. }
  337. }
  338. positionSet(bestPoint, setIndex);
  339. }
  340. return circles;
  341. }
  342. exports.greedyLayout = greedyLayout;
  343. /** Given a bunch of sets, and the desired overlaps between these sets - computes
  344. the distance from the actual overlaps to the desired overlaps. Note that
  345. this method ignores overlaps of more than 2 circles */
  346. function lossFunction(sets, overlaps) {
  347. var output = 0;
  348. function getCircles(indices) {
  349. return indices.map(function (i) {
  350. return sets[i];
  351. });
  352. }
  353. for (var i = 0; i < overlaps.length; ++i) {
  354. var area = overlaps[i];
  355. var overlap = void 0;
  356. if (area.sets.length == 1) {
  357. continue;
  358. }
  359. else if (area.sets.length == 2) {
  360. var left = sets[area.sets[0]], right = sets[area.sets[1]];
  361. overlap = (0, circleintersection_1.circleOverlap)(left.radius, right.radius, (0, circleintersection_1.distance)(left, right));
  362. }
  363. else {
  364. overlap = (0, circleintersection_1.intersectionArea)(getCircles(area.sets));
  365. }
  366. // eslint-disable-next-line
  367. var weight = area.hasOwnProperty('weight') ? area.weight : 1.0;
  368. output += weight * (overlap - area.size) * (overlap - area.size);
  369. }
  370. return output;
  371. }
  372. exports.lossFunction = lossFunction;
  373. // orientates a bunch of circles to point in orientation
  374. function orientateCircles(circles, orientation, orientationOrder) {
  375. if (orientationOrder === null) {
  376. circles.sort(function (a, b) {
  377. return b.radius - a.radius;
  378. });
  379. }
  380. else {
  381. circles.sort(orientationOrder);
  382. }
  383. var i;
  384. // shift circles so largest circle is at (0, 0)
  385. if (circles.length > 0) {
  386. var largestX = circles[0].x, largestY = circles[0].y;
  387. for (i = 0; i < circles.length; ++i) {
  388. circles[i].x -= largestX;
  389. circles[i].y -= largestY;
  390. }
  391. }
  392. if (circles.length == 2) {
  393. // if the second circle is a subset of the first, arrange so that
  394. // it is off to one side. hack for https://github.com/benfred/venn.js/issues/120
  395. var dist = (0, circleintersection_1.distance)(circles[0], circles[1]);
  396. if (dist < Math.abs(circles[1].radius - circles[0].radius)) {
  397. circles[1].x = circles[0].x + circles[0].radius - circles[1].radius - 1e-10;
  398. circles[1].y = circles[0].y;
  399. }
  400. }
  401. // rotate circles so that second largest is at an angle of 'orientation'
  402. // from largest
  403. if (circles.length > 1) {
  404. var rotation = Math.atan2(circles[1].x, circles[1].y) - orientation;
  405. var x = void 0, y = void 0;
  406. var c = Math.cos(rotation), s = Math.sin(rotation);
  407. for (i = 0; i < circles.length; ++i) {
  408. x = circles[i].x;
  409. y = circles[i].y;
  410. circles[i].x = c * x - s * y;
  411. circles[i].y = s * x + c * y;
  412. }
  413. }
  414. // mirror solution if third solution is above plane specified by
  415. // first two circles
  416. if (circles.length > 2) {
  417. var angle = Math.atan2(circles[2].x, circles[2].y) - orientation;
  418. while (angle < 0) {
  419. angle += 2 * Math.PI;
  420. }
  421. while (angle > 2 * Math.PI) {
  422. angle -= 2 * Math.PI;
  423. }
  424. if (angle > Math.PI) {
  425. var slope = circles[1].y / (1e-10 + circles[1].x);
  426. for (i = 0; i < circles.length; ++i) {
  427. var d = (circles[i].x + slope * circles[i].y) / (1 + slope * slope);
  428. circles[i].x = 2 * d - circles[i].x;
  429. circles[i].y = 2 * d * slope - circles[i].y;
  430. }
  431. }
  432. }
  433. }
  434. function disjointCluster(circles) {
  435. // union-find clustering to get disjoint sets
  436. circles.map(function (circle) {
  437. circle.parent = circle;
  438. });
  439. // path compression step in union find
  440. function find(circle) {
  441. if (circle.parent !== circle) {
  442. circle.parent = find(circle.parent);
  443. }
  444. return circle.parent;
  445. }
  446. function union(x, y) {
  447. var xRoot = find(x), yRoot = find(y);
  448. xRoot.parent = yRoot;
  449. }
  450. // get the union of all overlapping sets
  451. for (var i = 0; i < circles.length; ++i) {
  452. for (var j = i + 1; j < circles.length; ++j) {
  453. var maxDistance = circles[i].radius + circles[j].radius;
  454. if ((0, circleintersection_1.distance)(circles[i], circles[j]) + 1e-10 < maxDistance) {
  455. union(circles[j], circles[i]);
  456. }
  457. }
  458. }
  459. // find all the disjoint clusters and group them together
  460. var disjointClusters = {};
  461. var setid;
  462. for (var i = 0; i < circles.length; ++i) {
  463. setid = find(circles[i]).parent.setid;
  464. if (!(setid in disjointClusters)) {
  465. disjointClusters[setid] = [];
  466. }
  467. disjointClusters[setid].push(circles[i]);
  468. }
  469. // cleanup bookkeeping
  470. circles.map(function (circle) {
  471. delete circle.parent;
  472. });
  473. // return in more usable form
  474. var ret = [];
  475. for (setid in disjointClusters) {
  476. // eslint-disable-next-line
  477. if (disjointClusters.hasOwnProperty(setid)) {
  478. ret.push(disjointClusters[setid]);
  479. }
  480. }
  481. return ret;
  482. }
  483. exports.disjointCluster = disjointCluster;
  484. function getBoundingBox(circles) {
  485. var minMax = function (d) {
  486. var hi = Math.max.apply(null, circles.map(function (c) {
  487. return c[d] + c.radius;
  488. })), lo = Math.min.apply(null, circles.map(function (c) {
  489. return c[d] - c.radius;
  490. }));
  491. return { max: hi, min: lo };
  492. };
  493. return { xRange: minMax('x'), yRange: minMax('y') };
  494. }
  495. function normalizeSolution(solution, orientation, orientationOrder) {
  496. if (orientation === null) {
  497. orientation = Math.PI / 2;
  498. }
  499. // work with a list instead of a dictionary, and take a copy so we
  500. // don't mutate input
  501. var circles = [], i, setid;
  502. for (setid in solution) {
  503. // eslint-disable-next-line
  504. if (solution.hasOwnProperty(setid)) {
  505. var previous = solution[setid];
  506. circles.push({ x: previous.x, y: previous.y, radius: previous.radius, setid: setid });
  507. }
  508. }
  509. // get all the disjoint clusters
  510. var clusters = disjointCluster(circles);
  511. // orientate all disjoint sets, get sizes
  512. for (i = 0; i < clusters.length; ++i) {
  513. orientateCircles(clusters[i], orientation, orientationOrder);
  514. var bounds = getBoundingBox(clusters[i]);
  515. clusters[i].size = (bounds.xRange.max - bounds.xRange.min) * (bounds.yRange.max - bounds.yRange.min);
  516. clusters[i].bounds = bounds;
  517. }
  518. clusters.sort(function (a, b) {
  519. return b.size - a.size;
  520. });
  521. // orientate the largest at 0,0, and get the bounds
  522. circles = clusters[0];
  523. // @ts-ignore fixme 从逻辑上看似乎是不对的,后续看看
  524. var returnBounds = circles.bounds;
  525. var spacing = (returnBounds.xRange.max - returnBounds.xRange.min) / 50;
  526. function addCluster(cluster, right, bottom) {
  527. if (!cluster)
  528. return;
  529. var bounds = cluster.bounds;
  530. var xOffset, yOffset, centreing;
  531. if (right) {
  532. xOffset = returnBounds.xRange.max - bounds.xRange.min + spacing;
  533. }
  534. else {
  535. xOffset = returnBounds.xRange.max - bounds.xRange.max;
  536. centreing = (bounds.xRange.max - bounds.xRange.min) / 2 - (returnBounds.xRange.max - returnBounds.xRange.min) / 2;
  537. if (centreing < 0)
  538. xOffset += centreing;
  539. }
  540. if (bottom) {
  541. yOffset = returnBounds.yRange.max - bounds.yRange.min + spacing;
  542. }
  543. else {
  544. yOffset = returnBounds.yRange.max - bounds.yRange.max;
  545. centreing = (bounds.yRange.max - bounds.yRange.min) / 2 - (returnBounds.yRange.max - returnBounds.yRange.min) / 2;
  546. if (centreing < 0)
  547. yOffset += centreing;
  548. }
  549. for (var j = 0; j < cluster.length; ++j) {
  550. cluster[j].x += xOffset;
  551. cluster[j].y += yOffset;
  552. circles.push(cluster[j]);
  553. }
  554. }
  555. var index = 1;
  556. while (index < clusters.length) {
  557. addCluster(clusters[index], true, false);
  558. addCluster(clusters[index + 1], false, true);
  559. addCluster(clusters[index + 2], true, true);
  560. index += 3;
  561. // have one cluster (in top left). lay out next three relative
  562. // to it in a grid
  563. returnBounds = getBoundingBox(circles);
  564. }
  565. // convert back to solution form
  566. var ret = {};
  567. for (i = 0; i < circles.length; ++i) {
  568. ret[circles[i].setid] = circles[i];
  569. }
  570. return ret;
  571. }
  572. exports.normalizeSolution = normalizeSolution;
  573. /** Scales a solution from venn.venn or venn.greedyLayout such that it fits in
  574. a rectangle of width/height - with padding around the borders. also
  575. centers the diagram in the available space at the same time */
  576. function scaleSolution(solution, width, height, padding) {
  577. var circles = [], setids = [];
  578. for (var setid in solution) {
  579. // eslint-disable-next-line
  580. if (solution.hasOwnProperty(setid)) {
  581. setids.push(setid);
  582. circles.push(solution[setid]);
  583. }
  584. }
  585. width -= 2 * padding;
  586. height -= 2 * padding;
  587. var bounds = getBoundingBox(circles), xRange = bounds.xRange, yRange = bounds.yRange;
  588. if (xRange.max == xRange.min || yRange.max == yRange.min) {
  589. console.log('not scaling solution: zero size detected');
  590. return solution;
  591. }
  592. var xScaling = width / (xRange.max - xRange.min), yScaling = height / (yRange.max - yRange.min), scaling = Math.min(yScaling, xScaling),
  593. // while we're at it, center the diagram too
  594. xOffset = (width - (xRange.max - xRange.min) * scaling) / 2, yOffset = (height - (yRange.max - yRange.min) * scaling) / 2;
  595. var scaled = {};
  596. for (var i = 0; i < circles.length; ++i) {
  597. var circle = circles[i];
  598. scaled[setids[i]] = {
  599. radius: scaling * circle.radius,
  600. x: padding + xOffset + (circle.x - xRange.min) * scaling,
  601. y: padding + yOffset + (circle.y - yRange.min) * scaling,
  602. };
  603. }
  604. return scaled;
  605. }
  606. exports.scaleSolution = scaleSolution;
  607. //# sourceMappingURL=layout.js.map