layout.js 24 KB

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