layout.js 23 KB

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