layout.js 23 KB

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