venn.src.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. /* *
  2. * Experimental Highcharts module which enables visualization of a Venn Diagram.
  3. *
  4. * (c) 2016-2019 Highsoft AS
  5. *
  6. * Authors: Jon Arild Nygard
  7. *
  8. * Layout algorithm by Ben Frederickson:
  9. * https://www.benfrederickson.com/better-venn-diagrams/
  10. *
  11. * License: www.highcharts.com/license
  12. */
  13. 'use strict';
  14. import draw from '../mixins/draw-point.js';
  15. import geometry from '../mixins/geometry.js';
  16. import geometryCircles from '../mixins/geometry-circles.js';
  17. import H from '../parts/Globals.js';
  18. import '../parts/Series.js';
  19. var color = H.Color,
  20. extend = H.extend,
  21. getAreaOfIntersectionBetweenCircles =
  22. geometryCircles.getAreaOfIntersectionBetweenCircles,
  23. getCircleCircleIntersection = geometryCircles.getCircleCircleIntersection,
  24. getCenterOfPoints = geometry.getCenterOfPoints,
  25. getDistanceBetweenPoints = geometry.getDistanceBetweenPoints,
  26. getOverlapBetweenCirclesByDistance =
  27. geometryCircles.getOverlapBetweenCircles,
  28. isArray = H.isArray,
  29. isNumber = H.isNumber,
  30. isObject = H.isObject,
  31. isPointInsideAllCircles = geometryCircles.isPointInsideAllCircles,
  32. isPointOutsideAllCircles = geometryCircles.isPointOutsideAllCircles,
  33. isString = H.isString,
  34. merge = H.merge,
  35. seriesType = H.seriesType;
  36. var objectValues = function objectValues(obj) {
  37. return Object.keys(obj).map(function (x) {
  38. return obj[x];
  39. });
  40. };
  41. /**
  42. * Calculates the area of overlap between a list of circles.
  43. * @private
  44. * @todo add support for calculating overlap between more than 2 circles.
  45. * @param {Array<object>} circles List of circles with their given positions.
  46. * @return {number} Returns the area of overlap between all the circles.
  47. */
  48. var getOverlapBetweenCircles = function getOverlapBetweenCircles(circles) {
  49. var overlap = 0;
  50. // When there is only two circles we can find the overlap by using their
  51. // radiuses and the distance between them.
  52. if (circles.length === 2) {
  53. var circle1 = circles[0];
  54. var circle2 = circles[1];
  55. overlap = getOverlapBetweenCirclesByDistance(
  56. circle1.r,
  57. circle2.r,
  58. getDistanceBetweenPoints(circle1, circle2)
  59. );
  60. }
  61. return overlap;
  62. };
  63. /**
  64. * Calculates the difference between the desired overlap and the actual overlap
  65. * between two circles.
  66. * @private
  67. * @param {object} mapOfIdToCircle Map from id to circle.
  68. * @param {Array<object>} relations List of relations to calculate the loss of.
  69. * @return {number} Returns the loss between positions of the circles for the
  70. * given relations.
  71. */
  72. var loss = function loss(mapOfIdToCircle, relations) {
  73. var precision = 10e10;
  74. // Iterate all the relations and calculate their individual loss.
  75. return relations.reduce(function (totalLoss, relation) {
  76. var loss = 0;
  77. if (relation.sets.length > 1) {
  78. var wantedOverlap = relation.value;
  79. // Calculate the actual overlap between the sets.
  80. var actualOverlap = getOverlapBetweenCircles(
  81. // Get the circles for the given sets.
  82. relation.sets.map(function (set) {
  83. return mapOfIdToCircle[set];
  84. })
  85. );
  86. var diff = wantedOverlap - actualOverlap;
  87. loss = Math.round((diff * diff) * precision) / precision;
  88. }
  89. // Add calculated loss to the sum.
  90. return totalLoss + loss;
  91. }, 0);
  92. };
  93. /**
  94. * Finds the root of a given function. The root is the input value needed for
  95. * a function to return 0.
  96. *
  97. * See https://en.wikipedia.org/wiki/Bisection_method#Algorithm
  98. *
  99. * TODO: Add unit tests.
  100. *
  101. * @param {function} f The function to find the root of.
  102. * @param {number} a The lowest number in the search range.
  103. * @param {number} b The highest number in the search range.
  104. * @param {number} [tolerance=1e-10] The allowed difference between the returned
  105. * value and root.
  106. * @param {number} [maxIterations=100] The maximum iterations allowed.
  107. */
  108. var bisect = function bisect(f, a, b, tolerance, maxIterations) {
  109. var fA = f(a),
  110. fB = f(b),
  111. nMax = maxIterations || 100,
  112. tol = tolerance || 1e-10,
  113. delta = b - a,
  114. n = 1,
  115. x, fX;
  116. if (a >= b) {
  117. throw new Error('a must be smaller than b.');
  118. } else if (fA * fB > 0) {
  119. throw new Error('f(a) and f(b) must have opposite signs.');
  120. }
  121. if (fA === 0) {
  122. x = a;
  123. } else if (fB === 0) {
  124. x = b;
  125. } else {
  126. while (n++ <= nMax && fX !== 0 && delta > tol) {
  127. delta = (b - a) / 2;
  128. x = a + delta;
  129. fX = f(x);
  130. // Update low and high for next search interval.
  131. if (fA * fX > 0) {
  132. a = x;
  133. } else {
  134. b = x;
  135. }
  136. }
  137. }
  138. return x;
  139. };
  140. /**
  141. * Uses the bisection method to make a best guess of the ideal distance between
  142. * two circles too get the desired overlap.
  143. * Currently there is no known formula to calculate the distance from the area
  144. * of overlap, which makes the bisection method preferred.
  145. * @private
  146. * @param {number} r1 Radius of the first circle.
  147. * @param {number} r2 Radiues of the second circle.
  148. * @param {number} overlap The wanted overlap between the two circles.
  149. * @return {number} Returns the distance needed to get the wanted overlap
  150. * between the two circles.
  151. */
  152. var getDistanceBetweenCirclesByOverlap =
  153. function getDistanceBetweenCirclesByOverlap(r1, r2, overlap) {
  154. var maxDistance = r1 + r2,
  155. distance = maxDistance;
  156. if (overlap > 0) {
  157. distance = bisect(function (x) {
  158. var actualOverlap = getOverlapBetweenCirclesByDistance(r1, r2, x);
  159. // Return the differance between wanted and actual overlap.
  160. return overlap - actualOverlap;
  161. }, 0, maxDistance);
  162. }
  163. return distance;
  164. };
  165. var isSet = function (x) {
  166. return isArray(x.sets) && x.sets.length === 1;
  167. };
  168. /**
  169. * Finds an optimal position for a given point.
  170. * @private
  171. * @todo add unit tests.
  172. * @todo add constraints to optimize the algorithm.
  173. * @param {Function} fn The function to test a point.
  174. * @param {Array<*>} initial The initial point to optimize.
  175. * @return {Array<*>} Returns the opimized position of a point.
  176. */
  177. var nelderMead = function nelderMead(fn, initial) {
  178. var maxIterations = 100,
  179. sortByFx = function (a, b) {
  180. return a.fx - b.fx;
  181. },
  182. pRef = 1, // Reflection parameter
  183. pExp = 2, // Expansion parameter
  184. pCon = -0.5, // Contraction parameter
  185. pOCon = pCon * pRef, // Outwards contraction parameter
  186. pShrink = 0.5; // Shrink parameter
  187. var weightedSum = function weightedSum(weight1, v1, weight2, v2) {
  188. return v1.map(function (x, i) {
  189. return weight1 * x + weight2 * v2[i];
  190. });
  191. };
  192. var getSimplex = function getSimplex(initial) {
  193. var n = initial.length,
  194. simplex = new Array(n + 1);
  195. // Initial point to the simplex.
  196. simplex[0] = initial;
  197. simplex[0].fx = fn(initial);
  198. // Create a set of extra points based on the initial.
  199. for (var i = 0; i < n; ++i) {
  200. var point = initial.slice();
  201. point[i] = point[i] ? point[i] * 1.05 : 0.001;
  202. point.fx = fn(point);
  203. simplex[i + 1] = point;
  204. }
  205. return simplex;
  206. };
  207. var updateSimplex = function (simplex, point) {
  208. point.fx = fn(point);
  209. simplex[simplex.length - 1] = point;
  210. return simplex;
  211. };
  212. var shrinkSimplex = function (simplex) {
  213. var best = simplex[0];
  214. return simplex.map(function (point) {
  215. var p = weightedSum(1 - pShrink, best, pShrink, point);
  216. p.fx = fn(p);
  217. return p;
  218. });
  219. };
  220. var getCentroid = function (simplex) {
  221. var arr = simplex.slice(0, -1),
  222. length = arr.length,
  223. result = [],
  224. sum = function (data, point) {
  225. data.sum += point[data.i];
  226. return data;
  227. };
  228. for (var i = 0; i < length; i++) {
  229. result[i] = simplex.reduce(sum, { sum: 0, i: i }).sum / length;
  230. }
  231. return result;
  232. };
  233. var getPoint = function (centroid, worst, a, b) {
  234. var point = weightedSum(a, centroid, b, worst);
  235. point.fx = fn(point);
  236. return point;
  237. };
  238. // Create a simplex
  239. var simplex = getSimplex(initial);
  240. // Iterate from 0 to max iterations
  241. for (var i = 0; i < maxIterations; i++) {
  242. // Sort the simplex
  243. simplex.sort(sortByFx);
  244. // Create a centroid from the simplex
  245. var worst = simplex[simplex.length - 1];
  246. var centroid = getCentroid(simplex);
  247. // Calculate the reflected point.
  248. var reflected = getPoint(centroid, worst, 1 + pRef, -pRef);
  249. if (reflected.fx < simplex[0].fx) {
  250. // If reflected point is the best, then possibly expand.
  251. var expanded = getPoint(centroid, worst, 1 + pExp, -pExp);
  252. simplex = updateSimplex(
  253. simplex,
  254. (expanded.fx < reflected.fx) ? expanded : reflected
  255. );
  256. } else if (reflected.fx >= simplex[simplex.length - 2].fx) {
  257. // If the reflected point is worse than the second worse, then
  258. // contract.
  259. var contracted;
  260. if (reflected.fx > worst.fx) {
  261. // If the reflected is worse than the worst point, do a
  262. // contraction
  263. contracted = getPoint(centroid, worst, 1 + pCon, -pCon);
  264. if (contracted.fx < worst.fx) {
  265. simplex = updateSimplex(simplex, contracted);
  266. } else {
  267. simplex = shrinkSimplex(simplex);
  268. }
  269. } else {
  270. // Otherwise do an outwards contraction
  271. contracted = getPoint(centroid, worst, 1 - pOCon, pOCon);
  272. if (contracted.fx < reflected.fx) {
  273. simplex = updateSimplex(simplex, contracted);
  274. } else {
  275. simplex = shrinkSimplex(simplex);
  276. }
  277. }
  278. } else {
  279. simplex = updateSimplex(simplex, reflected);
  280. }
  281. }
  282. return simplex[0];
  283. };
  284. /**
  285. * Calculates a margin for a point based on the iternal and external circles.
  286. * The margin describes if the point is well placed within the internal circles,
  287. * and away from the external
  288. * @private
  289. * @todo add unit tests.
  290. * @param {object} point The point to evaluate.
  291. * @param {Array<object>} internal The internal circles.
  292. * @param {Array<object>} external The external circles.
  293. * @return {number} Returns the margin.
  294. */
  295. var getMarginFromCircles =
  296. function getMarginFromCircles(point, internal, external) {
  297. var margin = internal.reduce(function (margin, circle) {
  298. var m = circle.r - getDistanceBetweenPoints(point, circle);
  299. return (m <= margin) ? m : margin;
  300. }, Number.MAX_VALUE);
  301. margin = external.reduce(function (margin, circle) {
  302. var m = getDistanceBetweenPoints(point, circle) - circle.r;
  303. return (m <= margin) ? m : margin;
  304. }, margin);
  305. return margin;
  306. };
  307. /**
  308. * Finds the optimal label position by looking for a position that has a low
  309. * distance from the internal circles, and as large possible distane to the
  310. * external circles.
  311. * @private
  312. * @todo Optimize the intial position.
  313. * @todo Add unit tests.
  314. * @param {Array<object>} internal Internal circles.
  315. * @param {Array<object>} external External circles.
  316. * @return {object} Returns the found position.
  317. */
  318. var getLabelPosition = function getLabelPosition(internal, external) {
  319. // Get the best label position within the internal circles.
  320. var best = internal.reduce(function (best, circle) {
  321. var d = circle.r / 2;
  322. // Give a set of points with the circle to evaluate as the best label
  323. // position.
  324. return [
  325. { x: circle.x, y: circle.y },
  326. { x: circle.x + d, y: circle.y },
  327. { x: circle.x - d, y: circle.y },
  328. { x: circle.x, y: circle.y + d },
  329. { x: circle.x, y: circle.y - d }
  330. ]
  331. // Iterate the given points and return the one with the largest margin.
  332. .reduce(function (best, point) {
  333. var margin = getMarginFromCircles(point, internal, external);
  334. // If the margin better than the current best, then update best.
  335. if (best.margin < margin) {
  336. best.point = point;
  337. best.margin = margin;
  338. }
  339. return best;
  340. }, best);
  341. }, {
  342. point: undefined,
  343. margin: -Number.MAX_VALUE
  344. }).point;
  345. // Use nelder mead to optimize the initial label position.
  346. var optimal = nelderMead(
  347. function (p) {
  348. return -(
  349. getMarginFromCircles({ x: p[0], y: p[1] }, internal, external)
  350. );
  351. },
  352. [best.x, best.y]
  353. );
  354. // Update best to be the point which was found to have the best margin.
  355. best = {
  356. x: optimal[0],
  357. y: optimal[1]
  358. };
  359. if (!(
  360. isPointInsideAllCircles(best, internal) &&
  361. isPointOutsideAllCircles(best, external)
  362. )) {
  363. // If point was either outside one of the internal, or inside one of the
  364. // external, then it was invalid and should use a fallback.
  365. best = getCenterOfPoints(internal);
  366. }
  367. // Return the best point.
  368. return best;
  369. };
  370. /**
  371. * Calulates data label positions for a list of relations.
  372. * @private
  373. * @todo add unit tests
  374. * @todo NOTE: may be better suited as a part of the layout function.
  375. * @param {Array<object>} relations The list of relations.
  376. * @return {object} Returns a map from id to the data label position.
  377. */
  378. var getLabelPositions = function getLabelPositions(relations) {
  379. var singleSets = relations.filter(isSet);
  380. return relations.reduce(function (map, relation) {
  381. if (relation.value) {
  382. var sets = relation.sets,
  383. id = sets.join(),
  384. // Create a list of internal and external circles.
  385. data = singleSets.reduce(function (data, set) {
  386. // If the set exists in this relation, then it is internal,
  387. // otherwise it will be external.
  388. var isInternal = sets.indexOf(set.sets[0]) > -1,
  389. property = isInternal ? 'internal' : 'external';
  390. // Add the circle to the list.
  391. data[property].push(set.circle);
  392. return data;
  393. }, {
  394. internal: [],
  395. external: []
  396. });
  397. // Calulate the label position.
  398. map[id] = getLabelPosition(
  399. data.internal,
  400. data.external
  401. );
  402. }
  403. return map;
  404. }, {});
  405. };
  406. /**
  407. * Takes an array of relations and adds the properties `totalOverlap` and
  408. * `overlapping` to each set. The property `totalOverlap` is the sum of value
  409. * for each relation where this set is included. The property `overlapping` is
  410. * a map of how much this set is overlapping another set.
  411. * NOTE: This algorithm ignores relations consisting of more than 2 sets.
  412. * @private
  413. * @param {Array<object>} relations The list of relations that should be sorted.
  414. * @return {Array<object>} Returns the modified input relations with added
  415. * properties `totalOverlap` and `overlapping`.
  416. */
  417. var addOverlapToSets = function addOverlapToSets(relations) {
  418. // Calculate the amount of overlap per set.
  419. var mapOfIdToProps = relations
  420. // Filter out relations consisting of 2 sets.
  421. .filter(function (relation) {
  422. return relation.sets.length === 2;
  423. })
  424. // Sum up the amount of overlap for each set.
  425. .reduce(function (map, relation) {
  426. var sets = relation.sets;
  427. sets.forEach(function (set, i, arr) {
  428. if (!isObject(map[set])) {
  429. map[set] = {
  430. overlapping: {},
  431. totalOverlap: 0
  432. };
  433. }
  434. map[set].totalOverlap += relation.value;
  435. map[set].overlapping[arr[1 - i]] = relation.value;
  436. });
  437. return map;
  438. }, {});
  439. relations
  440. // Filter out single sets
  441. .filter(isSet)
  442. // Extend the set with the calculated properties.
  443. .forEach(function (set) {
  444. var properties = mapOfIdToProps[set.sets[0]];
  445. extend(set, properties);
  446. });
  447. // Returns the modified relations.
  448. return relations;
  449. };
  450. /**
  451. * Takes two sets and finds the one with the largest total overlap.
  452. * @private
  453. * @param {object} a The first set to compare.
  454. * @param {object} b The second set to compare.
  455. * @return {number} Returns 0 if a and b are equal, <0 if a is greater, >0 if b
  456. * is greater.
  457. */
  458. var sortByTotalOverlap = function sortByTotalOverlap(a, b) {
  459. return b.totalOverlap - a.totalOverlap;
  460. };
  461. /**
  462. * Uses a greedy approach to position all the sets. Works well with a small
  463. * number of sets, and are in these cases a good choice aesthetically.
  464. * @private
  465. * @param {Array<object>} relations List of the overlap between two or more
  466. * sets, or the size of a single set.
  467. * @return {Array<object>} List of circles and their calculated positions.
  468. */
  469. var layoutGreedyVenn = function layoutGreedyVenn(relations) {
  470. var positionedSets = [],
  471. mapOfIdToCircles = {};
  472. // Define a circle for each set.
  473. relations
  474. .filter(function (relation) {
  475. return relation.sets.length === 1;
  476. }).forEach(function (relation) {
  477. mapOfIdToCircles[relation.sets[0]] = relation.circle = {
  478. x: Number.MAX_VALUE,
  479. y: Number.MAX_VALUE,
  480. r: Math.sqrt(relation.value / Math.PI)
  481. };
  482. });
  483. /**
  484. * Takes a set and updates the position, and add the set to the list of
  485. * positioned sets.
  486. * @private
  487. * @param {object} set The set to add to its final position.
  488. * @param {object} coordinates The coordinates to position the set at.
  489. */
  490. var positionSet = function positionSet(set, coordinates) {
  491. var circle = set.circle;
  492. circle.x = coordinates.x;
  493. circle.y = coordinates.y;
  494. positionedSets.push(set);
  495. };
  496. // Find overlap between sets. Ignore relations with more then 2 sets.
  497. addOverlapToSets(relations);
  498. // Sort sets by the sum of their size from large to small.
  499. var sortedByOverlap = relations
  500. .filter(isSet)
  501. .sort(sortByTotalOverlap);
  502. // Position the most overlapped set at 0,0.
  503. positionSet(sortedByOverlap.pop(), { x: 0, y: 0 });
  504. var relationsWithTwoSets = relations.filter(function (x) {
  505. return x.sets.length === 2;
  506. });
  507. // Iterate and position the remaining sets.
  508. sortedByOverlap.forEach(function (set) {
  509. var circle = set.circle,
  510. radius = circle.r,
  511. overlapping = set.overlapping;
  512. var bestPosition = positionedSets
  513. .reduce(function (best, positionedSet, i) {
  514. var positionedCircle = positionedSet.circle,
  515. overlap = overlapping[positionedSet.sets[0]];
  516. // Calculate the distance between the sets to get the correct
  517. // overlap
  518. var distance = getDistanceBetweenCirclesByOverlap(
  519. radius,
  520. positionedCircle.r,
  521. overlap
  522. );
  523. // Create a list of possible coordinates calculated from
  524. // distance.
  525. var possibleCoordinates = [
  526. { x: positionedCircle.x + distance, y: positionedCircle.y },
  527. { x: positionedCircle.x - distance, y: positionedCircle.y },
  528. { x: positionedCircle.x, y: positionedCircle.y + distance },
  529. { x: positionedCircle.x, y: positionedCircle.y - distance }
  530. ];
  531. // If there are more circles overlapping, then add the
  532. // intersection points as possible positions.
  533. positionedSets.slice(i + 1).forEach(function (positionedSet2) {
  534. var positionedCircle2 = positionedSet2.circle,
  535. overlap2 = overlapping[positionedSet2.sets[0]],
  536. distance2 = getDistanceBetweenCirclesByOverlap(
  537. radius,
  538. positionedCircle2.r,
  539. overlap2
  540. );
  541. // Add intersections to list of coordinates.
  542. possibleCoordinates = possibleCoordinates.concat(
  543. getCircleCircleIntersection({
  544. x: positionedCircle.x,
  545. y: positionedCircle.y,
  546. r: distance2
  547. }, {
  548. x: positionedCircle2.x,
  549. y: positionedCircle2.y,
  550. r: distance2
  551. })
  552. );
  553. });
  554. // Iterate all suggested coordinates and find the best one.
  555. possibleCoordinates.forEach(function (coordinates) {
  556. circle.x = coordinates.x;
  557. circle.y = coordinates.y;
  558. // Calculate loss for the suggested coordinates.
  559. var currentLoss = loss(
  560. mapOfIdToCircles, relationsWithTwoSets
  561. );
  562. // If the loss is better, then use these new coordinates.
  563. if (currentLoss < best.loss) {
  564. best.loss = currentLoss;
  565. best.coordinates = coordinates;
  566. }
  567. });
  568. // Return resulting coordinates.
  569. return best;
  570. }, {
  571. loss: Number.MAX_VALUE,
  572. coordinates: undefined
  573. });
  574. // Add the set to its final position.
  575. positionSet(set, bestPosition.coordinates);
  576. });
  577. // Return the positions of each set.
  578. return mapOfIdToCircles;
  579. };
  580. /**
  581. * Calculates the positions of all the sets in the venn diagram.
  582. * @private
  583. * @todo Add support for constrained MDS.
  584. * @param {Array<object>} relations List of the overlap between two or more sets, or the
  585. * size of a single set.
  586. * @return {Arrat<object>} List of circles and their calculated positions.
  587. */
  588. var layout = function (relations) {
  589. var mapOfIdToShape = {};
  590. // Calculate best initial positions by using greedy layout.
  591. if (relations.length > 0) {
  592. mapOfIdToShape = layoutGreedyVenn(relations);
  593. relations
  594. .filter(function (x) {
  595. return !isSet(x);
  596. })
  597. .forEach(function (relation) {
  598. var sets = relation.sets,
  599. id = sets.join(),
  600. circles = sets.map(function (set) {
  601. return mapOfIdToShape[set];
  602. });
  603. // Add intersection shape to map
  604. mapOfIdToShape[id] =
  605. getAreaOfIntersectionBetweenCircles(circles);
  606. });
  607. }
  608. return mapOfIdToShape;
  609. };
  610. var isValidRelation = function (x) {
  611. var map = {};
  612. return (
  613. isObject(x) &&
  614. (isNumber(x.value) && x.value > -1) &&
  615. (isArray(x.sets) && x.sets.length > 0) &&
  616. !x.sets.some(function (set) {
  617. var invalid = false;
  618. if (!map[set] && isString(set)) {
  619. map[set] = true;
  620. } else {
  621. invalid = true;
  622. }
  623. return invalid;
  624. })
  625. );
  626. };
  627. var isValidSet = function (x) {
  628. return (isValidRelation(x) && isSet(x) && x.value > 0);
  629. };
  630. /**
  631. * Prepares the venn data so that it is usable for the layout function. Filter
  632. * out sets, or intersections that includes sets, that are missing in the data
  633. * or has (value < 1). Adds missing relations between sets in the data as
  634. * value = 0.
  635. * @private
  636. * @param {Array<object>} data The raw input data.
  637. * @return {Array<object>} Returns an array of valid venn data.
  638. */
  639. var processVennData = function processVennData(data) {
  640. var d = isArray(data) ? data : [];
  641. var validSets = d
  642. .reduce(function (arr, x) {
  643. // Check if x is a valid set, and that it is not an duplicate.
  644. if (isValidSet(x) && arr.indexOf(x.sets[0]) === -1) {
  645. arr.push(x.sets[0]);
  646. }
  647. return arr;
  648. }, [])
  649. .sort();
  650. var mapOfIdToRelation = d.reduce(function (mapOfIdToRelation, relation) {
  651. if (isValidRelation(relation) && !relation.sets.some(function (set) {
  652. return validSets.indexOf(set) === -1;
  653. })) {
  654. mapOfIdToRelation[relation.sets.sort().join()] = relation;
  655. }
  656. return mapOfIdToRelation;
  657. }, {});
  658. validSets.reduce(function (combinations, set, i, arr) {
  659. var remaining = arr.slice(i + 1);
  660. remaining.forEach(function (set2) {
  661. combinations.push(set + ',' + set2);
  662. });
  663. return combinations;
  664. }, []).forEach(function (combination) {
  665. if (!mapOfIdToRelation[combination]) {
  666. var obj = {
  667. sets: combination.split(','),
  668. value: 0
  669. };
  670. mapOfIdToRelation[combination] = obj;
  671. }
  672. });
  673. // Transform map into array.
  674. return objectValues(mapOfIdToRelation);
  675. };
  676. /**
  677. * Calculates the proper scale to fit the cloud inside the plotting area.
  678. * @private
  679. * @todo add unit test
  680. * @param {number} targetWidth Width of target area.
  681. * @param {number} targetHeight Height of target area.
  682. * @param {object} field The playing field.
  683. * @param {Highcharts.Series} series Series object.
  684. * @return {object} Returns the value to scale the playing field up to the size
  685. * of the target area, and center of x and y.
  686. */
  687. var getScale = function getScale(targetWidth, targetHeight, field) {
  688. var height = field.bottom - field.top, // top is smaller than bottom
  689. width = field.right - field.left,
  690. scaleX = width > 0 ? 1 / width * targetWidth : 1,
  691. scaleY = height > 0 ? 1 / height * targetHeight : 1,
  692. adjustX = (field.right + field.left) / 2,
  693. adjustY = (field.top + field.bottom) / 2,
  694. scale = Math.min(scaleX, scaleY);
  695. return {
  696. scale: scale,
  697. centerX: targetWidth / 2 - adjustX * scale,
  698. centerY: targetHeight / 2 - adjustY * scale
  699. };
  700. };
  701. /**
  702. * If a circle is outside a give field, then the boundaries of the field is
  703. * adjusted accordingly. Modifies the field object which is passed as the first
  704. * parameter.
  705. * @private
  706. * @todo NOTE: Copied from wordcloud, can probably be unified.
  707. * @param {object} field The bounding box of a playing field.
  708. * @param {object} placement The bounding box for a placed point.
  709. * @return {object} Returns a modified field object.
  710. */
  711. var updateFieldBoundaries = function updateFieldBoundaries(field, circle) {
  712. var left = circle.x - circle.r,
  713. right = circle.x + circle.r,
  714. bottom = circle.y + circle.r,
  715. top = circle.y - circle.r;
  716. // TODO improve type checking.
  717. if (!isNumber(field.left) || field.left > left) {
  718. field.left = left;
  719. }
  720. if (!isNumber(field.right) || field.right < right) {
  721. field.right = right;
  722. }
  723. if (!isNumber(field.top) || field.top > top) {
  724. field.top = top;
  725. }
  726. if (!isNumber(field.bottom) || field.bottom < bottom) {
  727. field.bottom = bottom;
  728. }
  729. return field;
  730. };
  731. /**
  732. * A Venn diagram displays all possible logical relations between a collection
  733. * of different sets. The sets are represented by circles, and the relation
  734. * between the sets are displayed by the overlap or lack of overlap between
  735. * them. The venn diagram is a special case of Euler diagrams, which can also
  736. * be displayed by this series type.
  737. *
  738. * @sample {highcharts} highcharts/demo/venn-diagram/
  739. * Venn diagram
  740. * @sample {highcharts} highcharts/demo/euler-diagram/
  741. * Euler diagram
  742. *
  743. * @extends plotOptions.scatter
  744. * @excluding connectEnds, connectNulls, cropThreshold, findNearestPointBy,
  745. * getExtremesFromAll, jitter, label, linecap, lineWidth,
  746. * linkedTo, marker, negativeColor, pointInterval,
  747. * pointIntervalUnit, pointPlacement, pointStart, softThreshold,
  748. * stacking, steps, threshold, xAxis, yAxis, zoneAxis, zones
  749. * @product highcharts
  750. * @optionparent plotOptions.venn
  751. */
  752. var vennOptions = {
  753. borderColor: '#cccccc',
  754. borderDashStyle: 'solid',
  755. borderWidth: 1,
  756. brighten: 0,
  757. clip: false,
  758. colorByPoint: true,
  759. dataLabels: {
  760. enabled: true,
  761. formatter: function () {
  762. return this.point.name;
  763. }
  764. },
  765. marker: false,
  766. opacity: 0.75,
  767. showInLegend: false,
  768. states: {
  769. hover: {
  770. opacity: 1,
  771. halo: false,
  772. borderColor: '#333333'
  773. },
  774. select: {
  775. color: '#cccccc',
  776. borderColor: '#000000',
  777. animation: false
  778. }
  779. },
  780. tooltip: {
  781. pointFormat: '{point.name}: {point.value}'
  782. }
  783. };
  784. var vennSeries = {
  785. isCartesian: false,
  786. axisTypes: [],
  787. directTouch: true,
  788. pointArrayMap: ['value'],
  789. translate: function () {
  790. var chart = this.chart;
  791. this.processedXData = this.xData;
  792. this.generatePoints();
  793. // Process the data before passing it into the layout function.
  794. var relations = processVennData(this.options.data);
  795. // Calculate the positions of each circle.
  796. var mapOfIdToShape = layout(relations);
  797. // Calculate positions of each data label
  798. var mapOfIdToLabelPosition = getLabelPositions(relations);
  799. // Calculate the scale, and center of the plot area.
  800. var field = Object.keys(mapOfIdToShape)
  801. .filter(function (key) {
  802. var shape = mapOfIdToShape[key];
  803. return shape && isNumber(shape.r);
  804. })
  805. .reduce(function (field, key) {
  806. return updateFieldBoundaries(field, mapOfIdToShape[key]);
  807. }, { top: 0, bottom: 0, left: 0, right: 0 }),
  808. scaling = getScale(chart.plotWidth, chart.plotHeight, field),
  809. scale = scaling.scale,
  810. centerX = scaling.centerX,
  811. centerY = scaling.centerY;
  812. // Iterate all points and calculate and draw their graphics.
  813. this.points.forEach(function (point) {
  814. var sets = isArray(point.sets) ? point.sets : [],
  815. id = sets.join(),
  816. shape = mapOfIdToShape[id],
  817. shapeArgs,
  818. dataLabelPosition = mapOfIdToLabelPosition[id];
  819. if (shape) {
  820. if (shape.r) {
  821. shapeArgs = {
  822. x: centerX + shape.x * scale,
  823. y: centerY + shape.y * scale,
  824. r: shape.r * scale
  825. };
  826. } else if (shape.d) {
  827. // TODO: find a better way to handle scaling of a path.
  828. var d = shape.d.reduce(function (path, arr) {
  829. if (arr[0] === 'M') {
  830. arr[1] = centerX + arr[1] * scale;
  831. arr[2] = centerY + arr[2] * scale;
  832. } else if (arr[0] === 'A') {
  833. arr[1] = arr[1] * scale;
  834. arr[2] = arr[2] * scale;
  835. arr[6] = centerX + arr[6] * scale;
  836. arr[7] = centerY + arr[7] * scale;
  837. }
  838. return path.concat(arr);
  839. }, [])
  840. .join(' ');
  841. shapeArgs = {
  842. d: d
  843. };
  844. }
  845. // Scale the position for the data label.
  846. if (dataLabelPosition) {
  847. dataLabelPosition.x = centerX + dataLabelPosition.x * scale;
  848. dataLabelPosition.y = centerY + dataLabelPosition.y * scale;
  849. } else {
  850. dataLabelPosition = {};
  851. }
  852. }
  853. point.shapeArgs = shapeArgs;
  854. // Placement for the data labels
  855. if (dataLabelPosition && shapeArgs) {
  856. point.plotX = dataLabelPosition.x;
  857. point.plotY = dataLabelPosition.y;
  858. }
  859. // Set name for usage in tooltip and in data label.
  860. point.name = point.options.name || sets.join('∩');
  861. });
  862. },
  863. /**
  864. * Draw the graphics for each point.
  865. * @private
  866. */
  867. drawPoints: function () {
  868. var series = this,
  869. // Series properties
  870. chart = series.chart,
  871. group = series.group,
  872. points = series.points || [],
  873. // Chart properties
  874. renderer = chart.renderer;
  875. // Iterate all points and calculate and draw their graphics.
  876. points.forEach(function (point) {
  877. var attribs,
  878. shapeArgs = point.shapeArgs;
  879. // Add point attribs
  880. if (!chart.styledMode) {
  881. attribs = series.pointAttribs(point, point.state);
  882. }
  883. // Draw the point graphic.
  884. point.draw({
  885. isNew: !point.graphic,
  886. animatableAttribs: shapeArgs,
  887. attribs: attribs,
  888. group: group,
  889. renderer: renderer,
  890. shapeType: shapeArgs && shapeArgs.d ? 'path' : 'circle'
  891. });
  892. });
  893. },
  894. /**
  895. * Calculates the style attributes for a point. The attributes can vary
  896. * depending on the state of the point.
  897. * @private
  898. * @param {object} point The point which will get the resulting attributes.
  899. * @param {string} state The state of the point.
  900. * @return {object} Returns the calculated attributes.
  901. */
  902. pointAttribs: function (point, state) {
  903. var series = this,
  904. seriesOptions = series.options || {},
  905. pointOptions = point && point.options || {},
  906. stateOptions = (state && seriesOptions.states[state]) || {},
  907. options = merge(
  908. seriesOptions,
  909. { color: point && point.color },
  910. pointOptions,
  911. stateOptions
  912. );
  913. // Return resulting values for the attributes.
  914. return {
  915. 'fill': color(options.color)
  916. .setOpacity(options.opacity)
  917. .brighten(options.brightness)
  918. .get(),
  919. 'stroke': options.borderColor,
  920. 'stroke-width': options.borderWidth,
  921. 'dashstyle': options.borderDashStyle
  922. };
  923. },
  924. animate: function (init) {
  925. if (!init) {
  926. var series = this,
  927. animOptions = H.animObject(series.options.animation);
  928. series.points.forEach(function (point) {
  929. var args = point.shapeArgs;
  930. if (point.graphic && args) {
  931. var attr = {},
  932. animate = {};
  933. if (args.d) {
  934. // If shape is a path, then animate opacity.
  935. attr.opacity = 0.001;
  936. } else {
  937. // If shape is a circle, then animate radius.
  938. attr.r = 0;
  939. animate.r = args.r;
  940. }
  941. point.graphic
  942. .attr(attr)
  943. .animate(animate, animOptions);
  944. // If shape is path, then fade it in after the circles
  945. // animation
  946. if (args.d) {
  947. setTimeout(function () {
  948. if (point && point.graphic) {
  949. point.graphic.animate({
  950. opacity: 1
  951. });
  952. }
  953. }, animOptions.duration);
  954. }
  955. }
  956. }, series);
  957. series.animate = null;
  958. }
  959. },
  960. utils: {
  961. addOverlapToSets: addOverlapToSets,
  962. geometry: geometry,
  963. geometryCircles: geometryCircles,
  964. getDistanceBetweenCirclesByOverlap: getDistanceBetweenCirclesByOverlap,
  965. loss: loss,
  966. processVennData: processVennData,
  967. sortByTotalOverlap: sortByTotalOverlap
  968. }
  969. };
  970. var vennPoint = {
  971. draw: draw,
  972. shouldDraw: function () {
  973. var point = this;
  974. // Only draw points with single sets.
  975. return !!point.shapeArgs;
  976. },
  977. isValid: function () {
  978. return isNumber(this.value);
  979. }
  980. };
  981. /**
  982. * A `venn` series. If the [type](#series.venn.type) option is
  983. * not specified, it is inherited from [chart.type](#chart.type).
  984. *
  985. * @extends series,plotOptions.venn
  986. * @excluding connectEnds, connectNulls, cropThreshold, dataParser, dataURL,
  987. * findNearestPointBy, getExtremesFromAll, label, linecap, lineWidth,
  988. * linkedTo, marker, negativeColor, pointInterval, pointIntervalUnit,
  989. * pointPlacement, pointStart, softThreshold, stack, stacking, steps,
  990. * threshold, xAxis, yAxis, zoneAxis, zones
  991. * @product highcharts
  992. * @apioption series.venn
  993. */
  994. /**
  995. * @type {Array<*>}
  996. * @extends series.scatter.data
  997. * @excluding marker, x, y
  998. * @product highcharts
  999. * @apioption series.venn.data
  1000. */
  1001. /**
  1002. * The name of the point. Used in data labels and tooltip. If name is not
  1003. * defined then it will default to the joined values in
  1004. * [sets](#series.venn.sets).
  1005. *
  1006. * @sample {highcharts} highcharts/demo/venn-diagram/
  1007. * Venn diagram
  1008. * @sample {highcharts} highcharts/demo/euler-diagram/
  1009. * Euler diagram
  1010. *
  1011. * @type {number}
  1012. * @since 7.0.0
  1013. * @product highcharts
  1014. * @apioption series.venn.data.name
  1015. */
  1016. /**
  1017. * The value of the point, resulting in a relative area of the circle, or area
  1018. * of overlap between two sets in the venn or euler diagram.
  1019. *
  1020. * @sample {highcharts} highcharts/demo/venn-diagram/
  1021. * Venn diagram
  1022. * @sample {highcharts} highcharts/demo/euler-diagram/
  1023. * Euler diagram
  1024. *
  1025. * @type {number}
  1026. * @since 7.0.0
  1027. * @product highcharts
  1028. * @apioption series.venn.data.value
  1029. */
  1030. /**
  1031. * The set or sets the options will be applied to. If a single entry is defined,
  1032. * then it will create a new set. If more than one entry is defined, then it
  1033. * will define the overlap between the sets in the array.
  1034. *
  1035. * @sample {highcharts} highcharts/demo/venn-diagram/
  1036. * Venn diagram
  1037. * @sample {highcharts} highcharts/demo/euler-diagram/
  1038. * Euler diagram
  1039. *
  1040. * @type {Array<string>}
  1041. * @since 7.0.0
  1042. * @product highcharts
  1043. * @apioption series.venn.data.sets
  1044. */
  1045. /**
  1046. * @private
  1047. * @class
  1048. * @name Highcharts.seriesTypes.venn
  1049. *
  1050. * @augments Highcharts.Series
  1051. */
  1052. seriesType('venn', 'scatter', vennOptions, vennSeries, vennPoint);