venn.src.js 72 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642
  1. /**
  2. * @license Highcharts JS v8.1.2 (2020-06-16)
  3. *
  4. * (c) 2017-2019 Highsoft AS
  5. * Authors: Jon Arild Nygard
  6. *
  7. * License: www.highcharts.com/license
  8. */
  9. 'use strict';
  10. (function (factory) {
  11. if (typeof module === 'object' && module.exports) {
  12. factory['default'] = factory;
  13. module.exports = factory;
  14. } else if (typeof define === 'function' && define.amd) {
  15. define('highcharts/modules/venn', ['highcharts'], function (Highcharts) {
  16. factory(Highcharts);
  17. factory.Highcharts = Highcharts;
  18. return factory;
  19. });
  20. } else {
  21. factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
  22. }
  23. }(function (Highcharts) {
  24. var _modules = Highcharts ? Highcharts._modules : {};
  25. function _registerModule(obj, path, args, fn) {
  26. if (!obj.hasOwnProperty(path)) {
  27. obj[path] = fn.apply(null, args);
  28. }
  29. }
  30. _registerModule(_modules, 'mixins/draw-point.js', [], function () {
  31. /* *
  32. *
  33. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  34. *
  35. * */
  36. var isFn = function (x) {
  37. return typeof x === 'function';
  38. };
  39. /* eslint-disable no-invalid-this, valid-jsdoc */
  40. /**
  41. * Handles the drawing of a component.
  42. * Can be used for any type of component that reserves the graphic property, and
  43. * provides a shouldDraw on its context.
  44. *
  45. * @private
  46. * @function draw
  47. * @param {DrawPointParams} params
  48. * Parameters.
  49. *
  50. * @todo add type checking.
  51. * @todo export this function to enable usage
  52. */
  53. var draw = function draw(params) {
  54. var _a;
  55. var component = this, graphic = component.graphic, animatableAttribs = params.animatableAttribs, onComplete = params.onComplete, css = params.css, renderer = params.renderer, animation = (_a = component.series) === null || _a === void 0 ? void 0 : _a.options.animation;
  56. if (component.shouldDraw()) {
  57. if (!graphic) {
  58. component.graphic = graphic =
  59. renderer[params.shapeType](params.shapeArgs)
  60. .add(params.group);
  61. }
  62. graphic
  63. .css(css)
  64. .attr(params.attribs)
  65. .animate(animatableAttribs, params.isNew ? false : animation, onComplete);
  66. }
  67. else if (graphic) {
  68. var destroy = function () {
  69. component.graphic = graphic = graphic.destroy();
  70. if (isFn(onComplete)) {
  71. onComplete();
  72. }
  73. };
  74. // animate only runs complete callback if something was animated.
  75. if (Object.keys(animatableAttribs).length) {
  76. graphic.animate(animatableAttribs, void 0, function () {
  77. destroy();
  78. });
  79. }
  80. else {
  81. destroy();
  82. }
  83. }
  84. };
  85. /**
  86. * An extended version of draw customized for points.
  87. * It calls additional methods that is expected when rendering a point.
  88. * @private
  89. * @param {Highcharts.Dictionary<any>} params Parameters
  90. */
  91. var drawPoint = function drawPoint(params) {
  92. var point = this, attribs = params.attribs = params.attribs || {};
  93. // Assigning class in dot notation does go well in IE8
  94. // eslint-disable-next-line dot-notation
  95. attribs['class'] = point.getClassName();
  96. // Call draw to render component
  97. draw.call(point, params);
  98. };
  99. return drawPoint;
  100. });
  101. _registerModule(_modules, 'mixins/geometry.js', [], function () {
  102. /* *
  103. *
  104. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  105. *
  106. * */
  107. /**
  108. * Calculates the center between a list of points.
  109. * @private
  110. * @param {Array<Highcharts.PositionObject>} points
  111. * A list of points to calculate the center of.
  112. * @return {Highcharts.PositionObject}
  113. * Calculated center
  114. */
  115. var getCenterOfPoints = function getCenterOfPoints(points) {
  116. var sum = points.reduce(function (sum, point) {
  117. sum.x += point.x;
  118. sum.y += point.y;
  119. return sum;
  120. }, { x: 0, y: 0 });
  121. return {
  122. x: sum.x / points.length,
  123. y: sum.y / points.length
  124. };
  125. };
  126. /**
  127. * Calculates the distance between two points based on their x and y
  128. * coordinates.
  129. * @private
  130. * @param {Highcharts.PositionObject} p1
  131. * The x and y coordinates of the first point.
  132. * @param {Highcharts.PositionObject} p2
  133. * The x and y coordinates of the second point.
  134. * @return {number}
  135. * Returns the distance between the points.
  136. */
  137. var getDistanceBetweenPoints = function getDistanceBetweenPoints(p1, p2) {
  138. return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
  139. };
  140. /**
  141. * Calculates the angle between two points.
  142. * @todo add unit tests.
  143. * @private
  144. * @param {Highcharts.PositionObject} p1 The first point.
  145. * @param {Highcharts.PositionObject} p2 The second point.
  146. * @return {number} Returns the angle in radians.
  147. */
  148. var getAngleBetweenPoints = function getAngleBetweenPoints(p1, p2) {
  149. return Math.atan2(p2.x - p1.x, p2.y - p1.y);
  150. };
  151. var geometry = {
  152. getAngleBetweenPoints: getAngleBetweenPoints,
  153. getCenterOfPoints: getCenterOfPoints,
  154. getDistanceBetweenPoints: getDistanceBetweenPoints
  155. };
  156. return geometry;
  157. });
  158. _registerModule(_modules, 'mixins/geometry-circles.js', [_modules['mixins/geometry.js']], function (geometry) {
  159. /* *
  160. *
  161. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  162. *
  163. * */
  164. var getAngleBetweenPoints = geometry.getAngleBetweenPoints, getCenterOfPoints = geometry.getCenterOfPoints, getDistanceBetweenPoints = geometry.getDistanceBetweenPoints;
  165. /**
  166. * @private
  167. * @param {number} x
  168. * Number to round
  169. * @param {number} decimals
  170. * Number of decimals to round to
  171. * @return {number}
  172. * Rounded number
  173. */
  174. function round(x, decimals) {
  175. var a = Math.pow(10, decimals);
  176. return Math.round(x * a) / a;
  177. }
  178. /**
  179. * Calculates the area of a circle based on its radius.
  180. * @private
  181. * @param {number} r
  182. * The radius of the circle.
  183. * @return {number}
  184. * Returns the area of the circle.
  185. */
  186. function getAreaOfCircle(r) {
  187. if (r <= 0) {
  188. throw new Error('radius of circle must be a positive number.');
  189. }
  190. return Math.PI * r * r;
  191. }
  192. /**
  193. * Calculates the area of a circular segment based on the radius of the circle
  194. * and the height of the segment.
  195. * See http://mathworld.wolfram.com/CircularSegment.html
  196. * @private
  197. * @param {number} r
  198. * The radius of the circle.
  199. * @param {number} h
  200. * The height of the circular segment.
  201. * @return {number}
  202. * Returns the area of the circular segment.
  203. */
  204. function getCircularSegmentArea(r, h) {
  205. return r * r * Math.acos(1 - h / r) - (r - h) * Math.sqrt(h * (2 * r - h));
  206. }
  207. /**
  208. * Calculates the area of overlap between two circles based on their radiuses
  209. * and the distance between them.
  210. * See http://mathworld.wolfram.com/Circle-CircleIntersection.html
  211. * @private
  212. * @param {number} r1
  213. * Radius of the first circle.
  214. * @param {number} r2
  215. * Radius of the second circle.
  216. * @param {number} d
  217. * The distance between the two circles.
  218. * @return {number}
  219. * Returns the area of overlap between the two circles.
  220. */
  221. function getOverlapBetweenCircles(r1, r2, d) {
  222. var overlap = 0;
  223. // If the distance is larger than the sum of the radiuses then the circles
  224. // does not overlap.
  225. if (d < r1 + r2) {
  226. if (d <= Math.abs(r2 - r1)) {
  227. // If the circles are completely overlapping, then the overlap
  228. // equals the area of the smallest circle.
  229. overlap = getAreaOfCircle(r1 < r2 ? r1 : r2);
  230. }
  231. else {
  232. // Height of first triangle segment.
  233. var d1 = (r1 * r1 - r2 * r2 + d * d) / (2 * d),
  234. // Height of second triangle segment.
  235. d2 = d - d1;
  236. overlap = (getCircularSegmentArea(r1, r1 - d1) +
  237. getCircularSegmentArea(r2, r2 - d2));
  238. }
  239. // Round the result to two decimals.
  240. overlap = round(overlap, 14);
  241. }
  242. return overlap;
  243. }
  244. /**
  245. * Calculates the intersection points of two circles.
  246. *
  247. * NOTE: does not handle floating errors well.
  248. * @private
  249. * @param {Highcharts.CircleObject} c1
  250. * The first circle.
  251. * @param {Highcharts.CircleObject} c2
  252. * The second sircle.
  253. * @return {Array<Highcharts.PositionObject>}
  254. * Returns the resulting intersection points.
  255. */
  256. function getCircleCircleIntersection(c1, c2) {
  257. var d = getDistanceBetweenPoints(c1, c2), r1 = c1.r, r2 = c2.r;
  258. var points = [];
  259. if (d < r1 + r2 && d > Math.abs(r1 - r2)) {
  260. // If the circles are overlapping, but not completely overlapping, then
  261. // it exists intersecting points.
  262. var r1Square = r1 * r1, r2Square = r2 * r2,
  263. // d^2 - r^2 + R^2 / 2d
  264. x = (r1Square - r2Square + d * d) / (2 * d),
  265. // y^2 = R^2 - x^2
  266. y = Math.sqrt(r1Square - x * x), x1 = c1.x, x2 = c2.x, y1 = c1.y, y2 = c2.y, x0 = x1 + x * (x2 - x1) / d, y0 = y1 + x * (y2 - y1) / d, rx = -(y2 - y1) * (y / d), ry = -(x2 - x1) * (y / d);
  267. points = [
  268. { x: round(x0 + rx, 14), y: round(y0 - ry, 14) },
  269. { x: round(x0 - rx, 14), y: round(y0 + ry, 14) }
  270. ];
  271. }
  272. return points;
  273. }
  274. /**
  275. * Calculates all the intersection points for between a list of circles.
  276. * @private
  277. * @param {Array<Highcharts.CircleObject>} circles
  278. * The circles to calculate the points from.
  279. * @return {Array<Highcharts.GeometryObject>}
  280. * Returns a list of intersection points.
  281. */
  282. function getCirclesIntersectionPoints(circles) {
  283. return circles.reduce(function (points, c1, i, arr) {
  284. var additional = arr.slice(i + 1)
  285. .reduce(function (points, c2, j) {
  286. var indexes = [i, j + i + 1];
  287. return points.concat(getCircleCircleIntersection(c1, c2)
  288. .map(function (p) {
  289. p.indexes = indexes;
  290. return p;
  291. }));
  292. }, []);
  293. return points.concat(additional);
  294. }, []);
  295. }
  296. /**
  297. * Tests wether the first circle is completely overlapping the second circle.
  298. *
  299. * @private
  300. * @param {Highcharts.CircleObject} circle1 The first circle.
  301. * @param {Highcharts.CircleObject} circle2 The The second circle.
  302. * @return {boolean} Returns true if circle1 is completely overlapping circle2,
  303. * false if not.
  304. */
  305. function isCircle1CompletelyOverlappingCircle2(circle1, circle2) {
  306. return getDistanceBetweenPoints(circle1, circle2) + circle2.r <
  307. circle1.r + 1e-10;
  308. }
  309. /**
  310. * Tests wether a point lies within a given circle.
  311. * @private
  312. * @param {Highcharts.PositionObject} point
  313. * The point to test for.
  314. * @param {Highcharts.CircleObject} circle
  315. * The circle to test if the point is within.
  316. * @return {boolean}
  317. * Returns true if the point is inside, false if outside.
  318. */
  319. function isPointInsideCircle(point, circle) {
  320. return getDistanceBetweenPoints(point, circle) <= circle.r + 1e-10;
  321. }
  322. /**
  323. * Tests wether a point lies within a set of circles.
  324. * @private
  325. * @param {Highcharts.PositionObject} point
  326. * The point to test.
  327. * @param {Array<Highcharts.CircleObject>} circles
  328. * The list of circles to test against.
  329. * @return {boolean}
  330. * Returns true if the point is inside all the circles, false if not.
  331. */
  332. function isPointInsideAllCircles(point, circles) {
  333. return !circles.some(function (circle) {
  334. return !isPointInsideCircle(point, circle);
  335. });
  336. }
  337. /**
  338. * Tests wether a point lies outside a set of circles.
  339. *
  340. * TODO: add unit tests.
  341. * @private
  342. * @param {Highcharts.PositionObject} point
  343. * The point to test.
  344. * @param {Array<Highcharts.CircleObject>} circles
  345. * The list of circles to test against.
  346. * @return {boolean}
  347. * Returns true if the point is outside all the circles, false if not.
  348. */
  349. function isPointOutsideAllCircles(point, circles) {
  350. return !circles.some(function (circle) {
  351. return isPointInsideCircle(point, circle);
  352. });
  353. }
  354. /**
  355. * Calculates the points for the polygon of the intersection area between a set
  356. * of circles.
  357. *
  358. * @private
  359. * @param {Array<Highcharts.CircleObject>} circles
  360. * List of circles to calculate polygon of.
  361. * @return {Array<Highcharts.GeometryObject>} Return list of points in the
  362. * intersection polygon.
  363. */
  364. function getCirclesIntersectionPolygon(circles) {
  365. return getCirclesIntersectionPoints(circles)
  366. .filter(function (p) {
  367. return isPointInsideAllCircles(p, circles);
  368. });
  369. }
  370. /**
  371. * Calculate the path for the area of overlap between a set of circles.
  372. * @todo handle cases with only 1 or 0 arcs.
  373. * @private
  374. * @param {Array<Highcharts.CircleObject>} circles
  375. * List of circles to calculate area of.
  376. * @return {Highcharts.GeometryIntersectionObject|undefined}
  377. * Returns the path for the area of overlap. Returns an empty string if
  378. * there are no intersection between all the circles.
  379. */
  380. function getAreaOfIntersectionBetweenCircles(circles) {
  381. var intersectionPoints = getCirclesIntersectionPolygon(circles), result;
  382. if (intersectionPoints.length > 1) {
  383. // Calculate the center of the intersection points.
  384. var center_1 = getCenterOfPoints(intersectionPoints);
  385. intersectionPoints = intersectionPoints
  386. // Calculate the angle between the center and the points.
  387. .map(function (p) {
  388. p.angle = getAngleBetweenPoints(center_1, p);
  389. return p;
  390. })
  391. // Sort the points by the angle to the center.
  392. .sort(function (a, b) {
  393. return b.angle - a.angle;
  394. });
  395. var startPoint = intersectionPoints[intersectionPoints.length - 1];
  396. var arcs = intersectionPoints
  397. .reduce(function (data, p1) {
  398. var startPoint = data.startPoint, midPoint = getCenterOfPoints([startPoint, p1]);
  399. // Calculate the arc from the intersection points and their
  400. // circles.
  401. var arc = p1.indexes
  402. // Filter out circles that are not included in both
  403. // intersection points.
  404. .filter(function (index) {
  405. return startPoint.indexes.indexOf(index) > -1;
  406. })
  407. // Iterate the circles of the intersection points and
  408. // calculate arcs.
  409. .reduce(function (arc, index) {
  410. var circle = circles[index], angle1 = getAngleBetweenPoints(circle, p1), angle2 = getAngleBetweenPoints(circle, startPoint), angleDiff = angle2 - angle1 +
  411. (angle2 < angle1 ? 2 * Math.PI : 0), angle = angle2 - angleDiff / 2;
  412. var width = getDistanceBetweenPoints(midPoint, {
  413. x: circle.x + circle.r * Math.sin(angle),
  414. y: circle.y + circle.r * Math.cos(angle)
  415. });
  416. var r = circle.r;
  417. // Width can sometimes become to large due to floating
  418. // point errors
  419. if (width > r * 2) {
  420. width = r * 2;
  421. }
  422. // Get the arc with the smallest width.
  423. if (!arc || arc.width > width) {
  424. arc = {
  425. r: r,
  426. largeArc: width > r ? 1 : 0,
  427. width: width,
  428. x: p1.x,
  429. y: p1.y
  430. };
  431. }
  432. // Return the chosen arc.
  433. return arc;
  434. }, null);
  435. // If we find an arc then add it to the list and update p2.
  436. if (arc) {
  437. var r = arc.r;
  438. data.arcs.push(['A', r, r, 0, arc.largeArc, 1, arc.x, arc.y]);
  439. data.startPoint = p1;
  440. }
  441. return data;
  442. }, {
  443. startPoint: startPoint,
  444. arcs: []
  445. }).arcs;
  446. if (arcs.length === 0) {
  447. // empty
  448. }
  449. else if (arcs.length === 1) {
  450. // empty
  451. }
  452. else {
  453. arcs.unshift(['M', startPoint.x, startPoint.y]);
  454. result = {
  455. center: center_1,
  456. d: arcs
  457. };
  458. }
  459. }
  460. return result;
  461. }
  462. var geometryCircles = {
  463. getAreaOfCircle: getAreaOfCircle,
  464. getAreaOfIntersectionBetweenCircles: getAreaOfIntersectionBetweenCircles,
  465. getCircleCircleIntersection: getCircleCircleIntersection,
  466. getCirclesIntersectionPoints: getCirclesIntersectionPoints,
  467. getCirclesIntersectionPolygon: getCirclesIntersectionPolygon,
  468. getCircularSegmentArea: getCircularSegmentArea,
  469. getOverlapBetweenCircles: getOverlapBetweenCircles,
  470. isCircle1CompletelyOverlappingCircle2: isCircle1CompletelyOverlappingCircle2,
  471. isPointInsideCircle: isPointInsideCircle,
  472. isPointInsideAllCircles: isPointInsideAllCircles,
  473. isPointOutsideAllCircles: isPointOutsideAllCircles,
  474. round: round
  475. };
  476. return geometryCircles;
  477. });
  478. _registerModule(_modules, 'mixins/nelder-mead.js', [], function () {
  479. /* *
  480. *
  481. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  482. *
  483. * */
  484. /* eslint-disable valid-jsdoc */
  485. var getCentroid = function (simplex) {
  486. var arr = simplex.slice(0, -1), length = arr.length, result = [], sum = function (data, point) {
  487. data.sum += point[data.i];
  488. return data;
  489. };
  490. for (var i = 0; i < length; i++) {
  491. result[i] = arr.reduce(sum, { sum: 0, i: i }).sum / length;
  492. }
  493. return result;
  494. };
  495. /**
  496. * Finds an optimal position for a given point.
  497. * @todo add unit tests.
  498. * @todo add constraints to optimize the algorithm.
  499. * @private
  500. * @param {Highcharts.NelderMeadTestFunction} fn
  501. * The function to test a point.
  502. * @param {Highcharts.NelderMeadPointArray} initial
  503. * The initial point to optimize.
  504. * @return {Highcharts.NelderMeadPointArray}
  505. * Returns the opimized position of a point.
  506. */
  507. var nelderMead = function nelderMead(fn, initial) {
  508. var maxIterations = 100, sortByFx = function (a, b) {
  509. return a.fx - b.fx;
  510. }, pRef = 1, // Reflection parameter
  511. pExp = 2, // Expansion parameter
  512. pCon = -0.5, // Contraction parameter
  513. pOCon = pCon * pRef, // Outwards contraction parameter
  514. pShrink = 0.5; // Shrink parameter
  515. /**
  516. * @private
  517. */
  518. var weightedSum = function weightedSum(weight1, v1, weight2, v2) {
  519. return v1.map(function (x, i) {
  520. return weight1 * x + weight2 * v2[i];
  521. });
  522. };
  523. /**
  524. * @private
  525. */
  526. var getSimplex = function getSimplex(initial) {
  527. var n = initial.length, simplex = new Array(n + 1);
  528. // Initial point to the simplex.
  529. simplex[0] = initial;
  530. simplex[0].fx = fn(initial);
  531. // Create a set of extra points based on the initial.
  532. for (var i = 0; i < n; ++i) {
  533. var point = initial.slice();
  534. point[i] = point[i] ? point[i] * 1.05 : 0.001;
  535. point.fx = fn(point);
  536. simplex[i + 1] = point;
  537. }
  538. return simplex;
  539. };
  540. var updateSimplex = function (simplex, point) {
  541. point.fx = fn(point);
  542. simplex[simplex.length - 1] = point;
  543. return simplex;
  544. };
  545. var shrinkSimplex = function (simplex) {
  546. var best = simplex[0];
  547. return simplex.map(function (point) {
  548. var p = weightedSum(1 - pShrink, best, pShrink, point);
  549. p.fx = fn(p);
  550. return p;
  551. });
  552. };
  553. var getPoint = function (centroid, worst, a, b) {
  554. var point = weightedSum(a, centroid, b, worst);
  555. point.fx = fn(point);
  556. return point;
  557. };
  558. // Create a simplex
  559. var simplex = getSimplex(initial);
  560. // Iterate from 0 to max iterations
  561. for (var i = 0; i < maxIterations; i++) {
  562. // Sort the simplex
  563. simplex.sort(sortByFx);
  564. // Create a centroid from the simplex
  565. var worst = simplex[simplex.length - 1];
  566. var centroid = getCentroid(simplex);
  567. // Calculate the reflected point.
  568. var reflected = getPoint(centroid, worst, 1 + pRef, -pRef);
  569. if (reflected.fx < simplex[0].fx) {
  570. // If reflected point is the best, then possibly expand.
  571. var expanded = getPoint(centroid, worst, 1 + pExp, -pExp);
  572. simplex = updateSimplex(simplex, (expanded.fx < reflected.fx) ? expanded : reflected);
  573. }
  574. else if (reflected.fx >= simplex[simplex.length - 2].fx) {
  575. // If the reflected point is worse than the second worse, then
  576. // contract.
  577. var contracted;
  578. if (reflected.fx > worst.fx) {
  579. // If the reflected is worse than the worst point, do a
  580. // contraction
  581. contracted = getPoint(centroid, worst, 1 + pCon, -pCon);
  582. if (contracted.fx < worst.fx) {
  583. simplex = updateSimplex(simplex, contracted);
  584. }
  585. else {
  586. simplex = shrinkSimplex(simplex);
  587. }
  588. }
  589. else {
  590. // Otherwise do an outwards contraction
  591. contracted = getPoint(centroid, worst, 1 - pOCon, pOCon);
  592. if (contracted.fx < reflected.fx) {
  593. simplex = updateSimplex(simplex, contracted);
  594. }
  595. else {
  596. simplex = shrinkSimplex(simplex);
  597. }
  598. }
  599. }
  600. else {
  601. simplex = updateSimplex(simplex, reflected);
  602. }
  603. }
  604. return simplex[0];
  605. };
  606. var content = {
  607. getCentroid: getCentroid,
  608. nelderMead: nelderMead
  609. };
  610. return content;
  611. });
  612. _registerModule(_modules, 'modules/venn.src.js', [_modules['parts/Color.js'], _modules['parts/Globals.js'], _modules['parts/Utilities.js'], _modules['mixins/draw-point.js'], _modules['mixins/geometry.js'], _modules['mixins/geometry-circles.js'], _modules['mixins/nelder-mead.js']], function (Color, H, U, draw, geometry, geometryCirclesModule, nelderMeadModule) {
  613. /* *
  614. *
  615. * Experimental Highcharts module which enables visualization of a Venn
  616. * diagram.
  617. *
  618. * (c) 2016-2020 Highsoft AS
  619. * Authors: Jon Arild Nygard
  620. *
  621. * Layout algorithm by Ben Frederickson:
  622. * https://www.benfrederickson.com/better-venn-diagrams/
  623. *
  624. * License: www.highcharts.com/license
  625. *
  626. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  627. *
  628. * */
  629. var color = Color.parse;
  630. var addEvent = U.addEvent, animObject = U.animObject, extend = U.extend, isArray = U.isArray, isNumber = U.isNumber, isObject = U.isObject, isString = U.isString, merge = U.merge, seriesType = U.seriesType;
  631. var getAreaOfCircle = geometryCirclesModule.getAreaOfCircle, getAreaOfIntersectionBetweenCircles = geometryCirclesModule.getAreaOfIntersectionBetweenCircles, getCircleCircleIntersection = geometryCirclesModule.getCircleCircleIntersection, getCirclesIntersectionPolygon = geometryCirclesModule.getCirclesIntersectionPolygon, getOverlapBetweenCirclesByDistance = geometryCirclesModule.getOverlapBetweenCircles, isCircle1CompletelyOverlappingCircle2 = geometryCirclesModule.isCircle1CompletelyOverlappingCircle2, isPointInsideAllCircles = geometryCirclesModule.isPointInsideAllCircles, isPointInsideCircle = geometryCirclesModule.isPointInsideCircle, isPointOutsideAllCircles = geometryCirclesModule.isPointOutsideAllCircles;
  632. // TODO: replace with individual imports
  633. var nelderMead = nelderMeadModule.nelderMead;
  634. var getCenterOfPoints = geometry.getCenterOfPoints, getDistanceBetweenPoints = geometry.getDistanceBetweenPoints, seriesTypes = H.seriesTypes;
  635. var objectValues = function objectValues(obj) {
  636. return Object.keys(obj).map(function (x) {
  637. return obj[x];
  638. });
  639. };
  640. /**
  641. * Calculates the area of overlap between a list of circles.
  642. * @private
  643. * @todo add support for calculating overlap between more than 2 circles.
  644. * @param {Array<Highcharts.CircleObject>} circles
  645. * List of circles with their given positions.
  646. * @return {number}
  647. * Returns the area of overlap between all the circles.
  648. */
  649. var getOverlapBetweenCircles = function getOverlapBetweenCircles(circles) {
  650. var overlap = 0;
  651. // When there is only two circles we can find the overlap by using their
  652. // radiuses and the distance between them.
  653. if (circles.length === 2) {
  654. var circle1 = circles[0];
  655. var circle2 = circles[1];
  656. overlap = getOverlapBetweenCirclesByDistance(circle1.r, circle2.r, getDistanceBetweenPoints(circle1, circle2));
  657. }
  658. return overlap;
  659. };
  660. /**
  661. * Calculates the difference between the desired overlap and the actual overlap
  662. * between two circles.
  663. * @private
  664. * @param {Dictionary<Highcharts.CircleObject>} mapOfIdToCircle
  665. * Map from id to circle.
  666. * @param {Array<Highcharts.VennRelationObject>} relations
  667. * List of relations to calculate the loss of.
  668. * @return {number}
  669. * Returns the loss between positions of the circles for the given relations.
  670. */
  671. var loss = function loss(mapOfIdToCircle, relations) {
  672. var precision = 10e10;
  673. // Iterate all the relations and calculate their individual loss.
  674. return relations.reduce(function (totalLoss, relation) {
  675. var loss = 0;
  676. if (relation.sets.length > 1) {
  677. var wantedOverlap = relation.value;
  678. // Calculate the actual overlap between the sets.
  679. var actualOverlap = getOverlapBetweenCircles(
  680. // Get the circles for the given sets.
  681. relation.sets.map(function (set) {
  682. return mapOfIdToCircle[set];
  683. }));
  684. var diff = wantedOverlap - actualOverlap;
  685. loss = Math.round((diff * diff) * precision) / precision;
  686. }
  687. // Add calculated loss to the sum.
  688. return totalLoss + loss;
  689. }, 0);
  690. };
  691. /**
  692. * Finds the root of a given function. The root is the input value needed for
  693. * a function to return 0.
  694. *
  695. * See https://en.wikipedia.org/wiki/Bisection_method#Algorithm
  696. *
  697. * TODO: Add unit tests.
  698. *
  699. * @param {Function} f
  700. * The function to find the root of.
  701. * @param {number} a
  702. * The lowest number in the search range.
  703. * @param {number} b
  704. * The highest number in the search range.
  705. * @param {number} [tolerance=1e-10]
  706. * The allowed difference between the returned value and root.
  707. * @param {number} [maxIterations=100]
  708. * The maximum iterations allowed.
  709. * @return {number}
  710. * Root number.
  711. */
  712. var bisect = function bisect(f, a, b, tolerance, maxIterations) {
  713. var fA = f(a), fB = f(b), nMax = maxIterations || 100, tol = tolerance || 1e-10, delta = b - a, n = 1, x, fX;
  714. if (a >= b) {
  715. throw new Error('a must be smaller than b.');
  716. }
  717. else if (fA * fB > 0) {
  718. throw new Error('f(a) and f(b) must have opposite signs.');
  719. }
  720. if (fA === 0) {
  721. x = a;
  722. }
  723. else if (fB === 0) {
  724. x = b;
  725. }
  726. else {
  727. while (n++ <= nMax && fX !== 0 && delta > tol) {
  728. delta = (b - a) / 2;
  729. x = a + delta;
  730. fX = f(x);
  731. // Update low and high for next search interval.
  732. if (fA * fX > 0) {
  733. a = x;
  734. }
  735. else {
  736. b = x;
  737. }
  738. }
  739. }
  740. return x;
  741. };
  742. /**
  743. * Uses the bisection method to make a best guess of the ideal distance between
  744. * two circles too get the desired overlap.
  745. * Currently there is no known formula to calculate the distance from the area
  746. * of overlap, which makes the bisection method preferred.
  747. * @private
  748. * @param {number} r1
  749. * Radius of the first circle.
  750. * @param {number} r2
  751. * Radiues of the second circle.
  752. * @param {number} overlap
  753. * The wanted overlap between the two circles.
  754. * @return {number}
  755. * Returns the distance needed to get the wanted overlap between the two
  756. * circles.
  757. */
  758. var getDistanceBetweenCirclesByOverlap = function getDistanceBetweenCirclesByOverlap(r1, r2, overlap) {
  759. var maxDistance = r1 + r2, distance;
  760. if (overlap <= 0) {
  761. // If overlap is below or equal to zero, then there is no overlap.
  762. distance = maxDistance;
  763. }
  764. else if (getAreaOfCircle(r1 < r2 ? r1 : r2) <= overlap) {
  765. // When area of overlap is larger than the area of the smallest circle,
  766. // then it is completely overlapping.
  767. distance = 0;
  768. }
  769. else {
  770. distance = bisect(function (x) {
  771. var actualOverlap = getOverlapBetweenCirclesByDistance(r1, r2, x);
  772. // Return the differance between wanted and actual overlap.
  773. return overlap - actualOverlap;
  774. }, 0, maxDistance);
  775. }
  776. return distance;
  777. };
  778. var isSet = function (x) {
  779. return isArray(x.sets) && x.sets.length === 1;
  780. };
  781. /**
  782. * Calculates a margin for a point based on the iternal and external circles.
  783. * The margin describes if the point is well placed within the internal circles,
  784. * and away from the external
  785. * @private
  786. * @todo add unit tests.
  787. * @param {Highcharts.PositionObject} point
  788. * The point to evaluate.
  789. * @param {Array<Highcharts.CircleObject>} internal
  790. * The internal circles.
  791. * @param {Array<Highcharts.CircleObject>} external
  792. * The external circles.
  793. * @return {number}
  794. * Returns the margin.
  795. */
  796. var getMarginFromCircles = function getMarginFromCircles(point, internal, external) {
  797. var margin = internal.reduce(function (margin, circle) {
  798. var m = circle.r - getDistanceBetweenPoints(point, circle);
  799. return (m <= margin) ? m : margin;
  800. }, Number.MAX_VALUE);
  801. margin = external.reduce(function (margin, circle) {
  802. var m = getDistanceBetweenPoints(point, circle) - circle.r;
  803. return (m <= margin) ? m : margin;
  804. }, margin);
  805. return margin;
  806. };
  807. /**
  808. * Finds the optimal label position by looking for a position that has a low
  809. * distance from the internal circles, and as large possible distane to the
  810. * external circles.
  811. * @private
  812. * @todo Optimize the intial position.
  813. * @todo Add unit tests.
  814. * @param {Array<Highcharts.CircleObject>} internal
  815. * Internal circles.
  816. * @param {Array<Highcharts.CircleObject>} external
  817. * External circles.
  818. * @return {Highcharts.PositionObject}
  819. * Returns the found position.
  820. */
  821. var getLabelPosition = function getLabelPosition(internal, external) {
  822. // Get the best label position within the internal circles.
  823. var best = internal.reduce(function (best, circle) {
  824. var d = circle.r / 2;
  825. // Give a set of points with the circle to evaluate as the best label
  826. // position.
  827. return [
  828. { x: circle.x, y: circle.y },
  829. { x: circle.x + d, y: circle.y },
  830. { x: circle.x - d, y: circle.y },
  831. { x: circle.x, y: circle.y + d },
  832. { x: circle.x, y: circle.y - d }
  833. ]
  834. // Iterate the given points and return the one with the largest
  835. // margin.
  836. .reduce(function (best, point) {
  837. var margin = getMarginFromCircles(point, internal, external);
  838. // If the margin better than the current best, then update best.
  839. if (best.margin < margin) {
  840. best.point = point;
  841. best.margin = margin;
  842. }
  843. return best;
  844. }, best);
  845. }, {
  846. point: void 0,
  847. margin: -Number.MAX_VALUE
  848. }).point;
  849. // Use nelder mead to optimize the initial label position.
  850. var optimal = nelderMead(function (p) {
  851. return -(getMarginFromCircles({ x: p[0], y: p[1] }, internal, external));
  852. }, [best.x, best.y]);
  853. // Update best to be the point which was found to have the best margin.
  854. best = {
  855. x: optimal[0],
  856. y: optimal[1]
  857. };
  858. if (!(isPointInsideAllCircles(best, internal) &&
  859. isPointOutsideAllCircles(best, external))) {
  860. // If point was either outside one of the internal, or inside one of the
  861. // external, then it was invalid and should use a fallback.
  862. if (internal.length > 1) {
  863. best = getCenterOfPoints(getCirclesIntersectionPolygon(internal));
  864. }
  865. else {
  866. best = {
  867. x: internal[0].x,
  868. y: internal[0].y
  869. };
  870. }
  871. }
  872. // Return the best point.
  873. return best;
  874. };
  875. /**
  876. * Finds the available width for a label, by taking the label position and
  877. * finding the largest distance, which is inside all internal circles, and
  878. * outside all external circles.
  879. *
  880. * @private
  881. * @param {Highcharts.PositionObject} pos
  882. * The x and y coordinate of the label.
  883. * @param {Array<Highcharts.CircleObject>} internal
  884. * Internal circles.
  885. * @param {Array<Highcharts.CircleObject>} external
  886. * External circles.
  887. * @return {number}
  888. * Returns available width for the label.
  889. */
  890. var getLabelWidth = function getLabelWidth(pos, internal, external) {
  891. var radius = internal.reduce(function (min, circle) {
  892. return Math.min(circle.r, min);
  893. }, Infinity),
  894. // Filter out external circles that are completely overlapping.
  895. filteredExternals = external.filter(function (circle) {
  896. return !isPointInsideCircle(pos, circle);
  897. });
  898. var findDistance = function (maxDistance, direction) {
  899. return bisect(function (x) {
  900. var testPos = {
  901. x: pos.x + (direction * x),
  902. y: pos.y
  903. }, isValid = (isPointInsideAllCircles(testPos, internal) &&
  904. isPointOutsideAllCircles(testPos, filteredExternals));
  905. // If the position is valid, then we want to move towards the max
  906. // distance. If not, then we want to away from the max distance.
  907. return -(maxDistance - x) + (isValid ? 0 : Number.MAX_VALUE);
  908. }, 0, maxDistance);
  909. };
  910. // Find the smallest distance of left and right.
  911. return Math.min(findDistance(radius, -1), findDistance(radius, 1)) * 2;
  912. };
  913. /**
  914. * Calulates data label values for a given relations object.
  915. *
  916. * @private
  917. * @todo add unit tests
  918. * @param {Highcharts.VennRelationObject} relation A relations object.
  919. * @param {Array<Highcharts.VennRelationObject>} setRelations The list of
  920. * relations that is a set.
  921. * @return {Highcharts.VennLabelValuesObject}
  922. * Returns an object containing position and width of the label.
  923. */
  924. function getLabelValues(relation, setRelations) {
  925. var sets = relation.sets;
  926. // Create a list of internal and external circles.
  927. var data = setRelations.reduce(function (data, set) {
  928. // If the set exists in this relation, then it is internal,
  929. // otherwise it will be external.
  930. var isInternal = sets.indexOf(set.sets[0]) > -1;
  931. var property = isInternal ? 'internal' : 'external';
  932. // Add the circle to the list.
  933. data[property].push(set.circle);
  934. return data;
  935. }, {
  936. internal: [],
  937. external: []
  938. });
  939. // Filter out external circles that are completely overlapping all internal
  940. data.external = data.external.filter(function (externalCircle) {
  941. return data.internal.some(function (internalCircle) {
  942. return !isCircle1CompletelyOverlappingCircle2(externalCircle, internalCircle);
  943. });
  944. });
  945. // Calulate the label position.
  946. var position = getLabelPosition(data.internal, data.external);
  947. // Calculate the label width
  948. var width = getLabelWidth(position, data.internal, data.external);
  949. return {
  950. position: position,
  951. width: width
  952. };
  953. }
  954. /**
  955. * Takes an array of relations and adds the properties `totalOverlap` and
  956. * `overlapping` to each set. The property `totalOverlap` is the sum of value
  957. * for each relation where this set is included. The property `overlapping` is
  958. * a map of how much this set is overlapping another set.
  959. * NOTE: This algorithm ignores relations consisting of more than 2 sets.
  960. * @private
  961. * @param {Array<Highcharts.VennRelationObject>} relations
  962. * The list of relations that should be sorted.
  963. * @return {Array<Highcharts.VennRelationObject>}
  964. * Returns the modified input relations with added properties `totalOverlap` and
  965. * `overlapping`.
  966. */
  967. var addOverlapToSets = function addOverlapToSets(relations) {
  968. // Calculate the amount of overlap per set.
  969. var mapOfIdToProps = relations
  970. // Filter out relations consisting of 2 sets.
  971. .filter(function (relation) {
  972. return relation.sets.length === 2;
  973. })
  974. // Sum up the amount of overlap for each set.
  975. .reduce(function (map, relation) {
  976. var sets = relation.sets;
  977. sets.forEach(function (set, i, arr) {
  978. if (!isObject(map[set])) {
  979. map[set] = {
  980. overlapping: {},
  981. totalOverlap: 0
  982. };
  983. }
  984. map[set].totalOverlap += relation.value;
  985. map[set].overlapping[arr[1 - i]] = relation.value;
  986. });
  987. return map;
  988. }, {});
  989. relations
  990. // Filter out single sets
  991. .filter(isSet)
  992. // Extend the set with the calculated properties.
  993. .forEach(function (set) {
  994. var properties = mapOfIdToProps[set.sets[0]];
  995. extend(set, properties);
  996. });
  997. // Returns the modified relations.
  998. return relations;
  999. };
  1000. /**
  1001. * Takes two sets and finds the one with the largest total overlap.
  1002. * @private
  1003. * @param {object} a The first set to compare.
  1004. * @param {object} b The second set to compare.
  1005. * @return {number} Returns 0 if a and b are equal, <0 if a is greater, >0 if b
  1006. * is greater.
  1007. */
  1008. var sortByTotalOverlap = function sortByTotalOverlap(a, b) {
  1009. return b.totalOverlap - a.totalOverlap;
  1010. };
  1011. /**
  1012. * Uses a greedy approach to position all the sets. Works well with a small
  1013. * number of sets, and are in these cases a good choice aesthetically.
  1014. * @private
  1015. * @param {Array<object>} relations List of the overlap between two or more
  1016. * sets, or the size of a single set.
  1017. * @return {Array<object>} List of circles and their calculated positions.
  1018. */
  1019. var layoutGreedyVenn = function layoutGreedyVenn(relations) {
  1020. var positionedSets = [], mapOfIdToCircles = {};
  1021. // Define a circle for each set.
  1022. relations
  1023. .filter(function (relation) {
  1024. return relation.sets.length === 1;
  1025. }).forEach(function (relation) {
  1026. mapOfIdToCircles[relation.sets[0]] = relation.circle = {
  1027. x: Number.MAX_VALUE,
  1028. y: Number.MAX_VALUE,
  1029. r: Math.sqrt(relation.value / Math.PI)
  1030. };
  1031. });
  1032. /**
  1033. * Takes a set and updates the position, and add the set to the list of
  1034. * positioned sets.
  1035. * @private
  1036. * @param {object} set
  1037. * The set to add to its final position.
  1038. * @param {object} coordinates
  1039. * The coordinates to position the set at.
  1040. * @return {void}
  1041. */
  1042. var positionSet = function positionSet(set, coordinates) {
  1043. var circle = set.circle;
  1044. circle.x = coordinates.x;
  1045. circle.y = coordinates.y;
  1046. positionedSets.push(set);
  1047. };
  1048. // Find overlap between sets. Ignore relations with more then 2 sets.
  1049. addOverlapToSets(relations);
  1050. // Sort sets by the sum of their size from large to small.
  1051. var sortedByOverlap = relations
  1052. .filter(isSet)
  1053. .sort(sortByTotalOverlap);
  1054. // Position the most overlapped set at 0,0.
  1055. positionSet(sortedByOverlap.shift(), { x: 0, y: 0 });
  1056. var relationsWithTwoSets = relations.filter(function (x) {
  1057. return x.sets.length === 2;
  1058. });
  1059. // Iterate and position the remaining sets.
  1060. sortedByOverlap.forEach(function (set) {
  1061. var circle = set.circle, radius = circle.r, overlapping = set.overlapping;
  1062. var bestPosition = positionedSets
  1063. .reduce(function (best, positionedSet, i) {
  1064. var positionedCircle = positionedSet.circle, overlap = overlapping[positionedSet.sets[0]];
  1065. // Calculate the distance between the sets to get the correct
  1066. // overlap
  1067. var distance = getDistanceBetweenCirclesByOverlap(radius, positionedCircle.r, overlap);
  1068. // Create a list of possible coordinates calculated from
  1069. // distance.
  1070. var possibleCoordinates = [
  1071. { x: positionedCircle.x + distance, y: positionedCircle.y },
  1072. { x: positionedCircle.x - distance, y: positionedCircle.y },
  1073. { x: positionedCircle.x, y: positionedCircle.y + distance },
  1074. { x: positionedCircle.x, y: positionedCircle.y - distance }
  1075. ];
  1076. // If there are more circles overlapping, then add the
  1077. // intersection points as possible positions.
  1078. positionedSets.slice(i + 1).forEach(function (positionedSet2) {
  1079. var positionedCircle2 = positionedSet2.circle, overlap2 = overlapping[positionedSet2.sets[0]], distance2 = getDistanceBetweenCirclesByOverlap(radius, positionedCircle2.r, overlap2);
  1080. // Add intersections to list of coordinates.
  1081. possibleCoordinates = possibleCoordinates.concat(getCircleCircleIntersection({
  1082. x: positionedCircle.x,
  1083. y: positionedCircle.y,
  1084. r: distance
  1085. }, {
  1086. x: positionedCircle2.x,
  1087. y: positionedCircle2.y,
  1088. r: distance2
  1089. }));
  1090. });
  1091. // Iterate all suggested coordinates and find the best one.
  1092. possibleCoordinates.forEach(function (coordinates) {
  1093. circle.x = coordinates.x;
  1094. circle.y = coordinates.y;
  1095. // Calculate loss for the suggested coordinates.
  1096. var currentLoss = loss(mapOfIdToCircles, relationsWithTwoSets);
  1097. // If the loss is better, then use these new coordinates.
  1098. if (currentLoss < best.loss) {
  1099. best.loss = currentLoss;
  1100. best.coordinates = coordinates;
  1101. }
  1102. });
  1103. // Return resulting coordinates.
  1104. return best;
  1105. }, {
  1106. loss: Number.MAX_VALUE,
  1107. coordinates: void 0
  1108. });
  1109. // Add the set to its final position.
  1110. positionSet(set, bestPosition.coordinates);
  1111. });
  1112. // Return the positions of each set.
  1113. return mapOfIdToCircles;
  1114. };
  1115. /**
  1116. * Calculates the positions, and the label values of all the sets in the venn
  1117. * diagram.
  1118. *
  1119. * @private
  1120. * @todo Add support for constrained MDS.
  1121. * @param {Array<Highchats.VennRelationObject>} relations
  1122. * List of the overlap between two or more sets, or the size of a single set.
  1123. * @return {Highcharts.Dictionary<*>}
  1124. * List of circles and their calculated positions.
  1125. */
  1126. function layout(relations) {
  1127. var mapOfIdToShape = {};
  1128. var mapOfIdToLabelValues = {};
  1129. // Calculate best initial positions by using greedy layout.
  1130. if (relations.length > 0) {
  1131. var mapOfIdToCircles_1 = layoutGreedyVenn(relations);
  1132. var setRelations_1 = relations.filter(isSet);
  1133. relations
  1134. .forEach(function (relation) {
  1135. var sets = relation.sets;
  1136. var id = sets.join();
  1137. // Get shape from map of circles, or calculate intersection.
  1138. var shape = isSet(relation) ?
  1139. mapOfIdToCircles_1[id] :
  1140. getAreaOfIntersectionBetweenCircles(sets.map(function (set) {
  1141. return mapOfIdToCircles_1[set];
  1142. }));
  1143. // Calculate label values if the set has a shape
  1144. if (shape) {
  1145. mapOfIdToShape[id] = shape;
  1146. mapOfIdToLabelValues[id] = getLabelValues(relation, setRelations_1);
  1147. }
  1148. });
  1149. }
  1150. return { mapOfIdToShape: mapOfIdToShape, mapOfIdToLabelValues: mapOfIdToLabelValues };
  1151. }
  1152. var isValidRelation = function (x) {
  1153. var map = {};
  1154. return (isObject(x) &&
  1155. (isNumber(x.value) && x.value > -1) &&
  1156. (isArray(x.sets) && x.sets.length > 0) &&
  1157. !x.sets.some(function (set) {
  1158. var invalid = false;
  1159. if (!map[set] && isString(set)) {
  1160. map[set] = true;
  1161. }
  1162. else {
  1163. invalid = true;
  1164. }
  1165. return invalid;
  1166. }));
  1167. };
  1168. var isValidSet = function (x) {
  1169. return (isValidRelation(x) && isSet(x) && x.value > 0);
  1170. };
  1171. /**
  1172. * Prepares the venn data so that it is usable for the layout function. Filter
  1173. * out sets, or intersections that includes sets, that are missing in the data
  1174. * or has (value < 1). Adds missing relations between sets in the data as
  1175. * value = 0.
  1176. * @private
  1177. * @param {Array<object>} data The raw input data.
  1178. * @return {Array<object>} Returns an array of valid venn data.
  1179. */
  1180. var processVennData = function processVennData(data) {
  1181. var d = isArray(data) ? data : [];
  1182. var validSets = d
  1183. .reduce(function (arr, x) {
  1184. // Check if x is a valid set, and that it is not an duplicate.
  1185. if (isValidSet(x) && arr.indexOf(x.sets[0]) === -1) {
  1186. arr.push(x.sets[0]);
  1187. }
  1188. return arr;
  1189. }, [])
  1190. .sort();
  1191. var mapOfIdToRelation = d.reduce(function (mapOfIdToRelation, relation) {
  1192. if (isValidRelation(relation) &&
  1193. !relation.sets.some(function (set) {
  1194. return validSets.indexOf(set) === -1;
  1195. })) {
  1196. mapOfIdToRelation[relation.sets.sort().join()] =
  1197. relation;
  1198. }
  1199. return mapOfIdToRelation;
  1200. }, {});
  1201. validSets.reduce(function (combinations, set, i, arr) {
  1202. var remaining = arr.slice(i + 1);
  1203. remaining.forEach(function (set2) {
  1204. combinations.push(set + ',' + set2);
  1205. });
  1206. return combinations;
  1207. }, []).forEach(function (combination) {
  1208. if (!mapOfIdToRelation[combination]) {
  1209. var obj = {
  1210. sets: combination.split(','),
  1211. value: 0
  1212. };
  1213. mapOfIdToRelation[combination] = obj;
  1214. }
  1215. });
  1216. // Transform map into array.
  1217. return objectValues(mapOfIdToRelation);
  1218. };
  1219. /**
  1220. * Calculates the proper scale to fit the cloud inside the plotting area.
  1221. * @private
  1222. * @todo add unit test
  1223. * @param {number} targetWidth
  1224. * Width of target area.
  1225. * @param {number} targetHeight
  1226. * Height of target area.
  1227. * @param {Highcharts.PolygonBoxObject} field
  1228. * The playing field.
  1229. * @return {Highcharts.Dictionary<number>}
  1230. * Returns the value to scale the playing field up to the size of the target
  1231. * area, and center of x and y.
  1232. */
  1233. var getScale = function getScale(targetWidth, targetHeight, field) {
  1234. var height = field.bottom - field.top, // top is smaller than bottom
  1235. width = field.right - field.left, scaleX = width > 0 ? 1 / width * targetWidth : 1, scaleY = height > 0 ? 1 / height * targetHeight : 1, adjustX = (field.right + field.left) / 2, adjustY = (field.top + field.bottom) / 2, scale = Math.min(scaleX, scaleY);
  1236. return {
  1237. scale: scale,
  1238. centerX: targetWidth / 2 - adjustX * scale,
  1239. centerY: targetHeight / 2 - adjustY * scale
  1240. };
  1241. };
  1242. /**
  1243. * If a circle is outside a give field, then the boundaries of the field is
  1244. * adjusted accordingly. Modifies the field object which is passed as the first
  1245. * parameter.
  1246. * @private
  1247. * @todo NOTE: Copied from wordcloud, can probably be unified.
  1248. * @param {Highcharts.PolygonBoxObject} field
  1249. * The bounding box of a playing field.
  1250. * @param {Highcharts.CircleObject} circle
  1251. * The bounding box for a placed point.
  1252. * @return {Highcharts.PolygonBoxObject}
  1253. * Returns a modified field object.
  1254. */
  1255. var updateFieldBoundaries = function updateFieldBoundaries(field, circle) {
  1256. var left = circle.x - circle.r, right = circle.x + circle.r, bottom = circle.y + circle.r, top = circle.y - circle.r;
  1257. // TODO improve type checking.
  1258. if (!isNumber(field.left) || field.left > left) {
  1259. field.left = left;
  1260. }
  1261. if (!isNumber(field.right) || field.right < right) {
  1262. field.right = right;
  1263. }
  1264. if (!isNumber(field.top) || field.top > top) {
  1265. field.top = top;
  1266. }
  1267. if (!isNumber(field.bottom) || field.bottom < bottom) {
  1268. field.bottom = bottom;
  1269. }
  1270. return field;
  1271. };
  1272. /**
  1273. * A Venn diagram displays all possible logical relations between a collection
  1274. * of different sets. The sets are represented by circles, and the relation
  1275. * between the sets are displayed by the overlap or lack of overlap between
  1276. * them. The venn diagram is a special case of Euler diagrams, which can also
  1277. * be displayed by this series type.
  1278. *
  1279. * @sample {highcharts} highcharts/demo/venn-diagram/
  1280. * Venn diagram
  1281. * @sample {highcharts} highcharts/demo/euler-diagram/
  1282. * Euler diagram
  1283. *
  1284. * @extends plotOptions.scatter
  1285. * @excluding connectEnds, connectNulls, cropThreshold, dragDrop,
  1286. * findNearestPointBy, getExtremesFromAll, jitter, label, linecap,
  1287. * lineWidth, linkedTo, marker, negativeColor, pointInterval,
  1288. * pointIntervalUnit, pointPlacement, pointStart, softThreshold,
  1289. * stacking, steps, threshold, xAxis, yAxis, zoneAxis, zones,
  1290. * dataSorting
  1291. * @product highcharts
  1292. * @requires modules/venn
  1293. * @optionparent plotOptions.venn
  1294. */
  1295. var vennOptions = {
  1296. borderColor: '#cccccc',
  1297. borderDashStyle: 'solid',
  1298. borderWidth: 1,
  1299. brighten: 0,
  1300. clip: false,
  1301. colorByPoint: true,
  1302. dataLabels: {
  1303. enabled: true,
  1304. verticalAlign: 'middle',
  1305. formatter: function () {
  1306. return this.point.name;
  1307. }
  1308. },
  1309. /**
  1310. * @ignore-option
  1311. * @private
  1312. */
  1313. inactiveOtherPoints: true,
  1314. marker: false,
  1315. opacity: 0.75,
  1316. showInLegend: false,
  1317. states: {
  1318. /**
  1319. * @excluding halo
  1320. */
  1321. hover: {
  1322. opacity: 1,
  1323. borderColor: '#333333'
  1324. },
  1325. /**
  1326. * @excluding halo
  1327. */
  1328. select: {
  1329. color: '#cccccc',
  1330. borderColor: '#000000',
  1331. animation: false
  1332. },
  1333. inactive: {
  1334. opacity: 0.075
  1335. }
  1336. },
  1337. tooltip: {
  1338. pointFormat: '{point.name}: {point.value}'
  1339. }
  1340. };
  1341. var vennSeries = {
  1342. isCartesian: false,
  1343. axisTypes: [],
  1344. directTouch: true,
  1345. pointArrayMap: ['value'],
  1346. init: function () {
  1347. seriesTypes.scatter.prototype.init.apply(this, arguments);
  1348. // Venn's opacity is a different option from other series
  1349. delete this.opacity;
  1350. },
  1351. translate: function () {
  1352. var chart = this.chart;
  1353. this.processedXData = this.xData;
  1354. this.generatePoints();
  1355. // Process the data before passing it into the layout function.
  1356. var relations = processVennData(this.options.data);
  1357. // Calculate the positions of each circle.
  1358. var _a = layout(relations), mapOfIdToShape = _a.mapOfIdToShape, mapOfIdToLabelValues = _a.mapOfIdToLabelValues;
  1359. // Calculate the scale, and center of the plot area.
  1360. var field = Object.keys(mapOfIdToShape)
  1361. .filter(function (key) {
  1362. var shape = mapOfIdToShape[key];
  1363. return shape && isNumber(shape.r);
  1364. })
  1365. .reduce(function (field, key) {
  1366. return updateFieldBoundaries(field, mapOfIdToShape[key]);
  1367. }, { top: 0, bottom: 0, left: 0, right: 0 }), scaling = getScale(chart.plotWidth, chart.plotHeight, field), scale = scaling.scale, centerX = scaling.centerX, centerY = scaling.centerY;
  1368. // Iterate all points and calculate and draw their graphics.
  1369. this.points.forEach(function (point) {
  1370. var sets = isArray(point.sets) ? point.sets : [], id = sets.join(), shape = mapOfIdToShape[id], shapeArgs, dataLabelValues = mapOfIdToLabelValues[id] || {}, dataLabelWidth = dataLabelValues.width, dataLabelPosition = dataLabelValues.position, dlOptions = point.options && point.options.dataLabels;
  1371. if (shape) {
  1372. if (shape.r) {
  1373. shapeArgs = {
  1374. x: centerX + shape.x * scale,
  1375. y: centerY + shape.y * scale,
  1376. r: shape.r * scale
  1377. };
  1378. }
  1379. else if (shape.d) {
  1380. var d = shape.d;
  1381. d.forEach(function (seg) {
  1382. if (seg[0] === 'M') {
  1383. seg[1] = centerX + seg[1] * scale;
  1384. seg[2] = centerY + seg[2] * scale;
  1385. }
  1386. else if (seg[0] === 'A') {
  1387. seg[1] = seg[1] * scale;
  1388. seg[2] = seg[2] * scale;
  1389. seg[6] = centerX + seg[6] * scale;
  1390. seg[7] = centerY + seg[7] * scale;
  1391. }
  1392. });
  1393. shapeArgs = { d: d };
  1394. }
  1395. // Scale the position for the data label.
  1396. if (dataLabelPosition) {
  1397. dataLabelPosition.x = centerX + dataLabelPosition.x * scale;
  1398. dataLabelPosition.y = centerY + dataLabelPosition.y * scale;
  1399. }
  1400. else {
  1401. dataLabelPosition = {};
  1402. }
  1403. if (isNumber(dataLabelWidth)) {
  1404. dataLabelWidth = Math.round(dataLabelWidth * scale);
  1405. }
  1406. }
  1407. point.shapeArgs = shapeArgs;
  1408. // Placement for the data labels
  1409. if (dataLabelPosition && shapeArgs) {
  1410. point.plotX = dataLabelPosition.x;
  1411. point.plotY = dataLabelPosition.y;
  1412. }
  1413. // Add width for the data label
  1414. if (dataLabelWidth && shapeArgs) {
  1415. point.dlOptions = merge(true, {
  1416. style: {
  1417. width: dataLabelWidth
  1418. }
  1419. }, isObject(dlOptions) && dlOptions);
  1420. }
  1421. // Set name for usage in tooltip and in data label.
  1422. point.name = point.options.name || sets.join('∩');
  1423. });
  1424. },
  1425. /* eslint-disable valid-jsdoc */
  1426. /**
  1427. * Draw the graphics for each point.
  1428. * @private
  1429. */
  1430. drawPoints: function () {
  1431. var series = this,
  1432. // Series properties
  1433. chart = series.chart, group = series.group, points = series.points || [],
  1434. // Chart properties
  1435. renderer = chart.renderer;
  1436. // Iterate all points and calculate and draw their graphics.
  1437. points.forEach(function (point) {
  1438. var attribs = {
  1439. zIndex: isArray(point.sets) ? point.sets.length : 0
  1440. }, shapeArgs = point.shapeArgs;
  1441. // Add point attribs
  1442. if (!chart.styledMode) {
  1443. extend(attribs, series.pointAttribs(point, point.state));
  1444. }
  1445. // Draw the point graphic.
  1446. point.draw({
  1447. isNew: !point.graphic,
  1448. animatableAttribs: shapeArgs,
  1449. attribs: attribs,
  1450. group: group,
  1451. renderer: renderer,
  1452. shapeType: shapeArgs && shapeArgs.d ? 'path' : 'circle'
  1453. });
  1454. });
  1455. },
  1456. /**
  1457. * Calculates the style attributes for a point. The attributes can vary
  1458. * depending on the state of the point.
  1459. * @private
  1460. * @param {Highcharts.Point} point
  1461. * The point which will get the resulting attributes.
  1462. * @param {string} [state]
  1463. * The state of the point.
  1464. * @return {Highcharts.SVGAttributes}
  1465. * Returns the calculated attributes.
  1466. */
  1467. pointAttribs: function (point, state) {
  1468. var series = this, seriesOptions = series.options || {}, pointOptions = point && point.options || {}, stateOptions = (state && seriesOptions.states[state]) || {}, options = merge(seriesOptions, { color: point && point.color }, pointOptions, stateOptions);
  1469. // Return resulting values for the attributes.
  1470. return {
  1471. 'fill': color(options.color)
  1472. .setOpacity(options.opacity)
  1473. .brighten(options.brightness)
  1474. .get(),
  1475. 'stroke': options.borderColor,
  1476. 'stroke-width': options.borderWidth,
  1477. 'dashstyle': options.borderDashStyle
  1478. };
  1479. },
  1480. /* eslint-enable valid-jsdoc */
  1481. animate: function (init) {
  1482. if (!init) {
  1483. var series = this, animOptions = animObject(series.options.animation);
  1484. series.points.forEach(function (point) {
  1485. var args = point.shapeArgs;
  1486. if (point.graphic && args) {
  1487. var attr = {}, animate = {};
  1488. if (args.d) {
  1489. // If shape is a path, then animate opacity.
  1490. attr.opacity = 0.001;
  1491. }
  1492. else {
  1493. // If shape is a circle, then animate radius.
  1494. attr.r = 0;
  1495. animate.r = args.r;
  1496. }
  1497. point.graphic
  1498. .attr(attr)
  1499. .animate(animate, animOptions);
  1500. // If shape is path, then fade it in after the circles
  1501. // animation
  1502. if (args.d) {
  1503. setTimeout(function () {
  1504. if (point && point.graphic) {
  1505. point.graphic.animate({
  1506. opacity: 1
  1507. });
  1508. }
  1509. }, animOptions.duration);
  1510. }
  1511. }
  1512. }, series);
  1513. }
  1514. },
  1515. utils: {
  1516. addOverlapToSets: addOverlapToSets,
  1517. geometry: geometry,
  1518. geometryCircles: geometryCirclesModule,
  1519. getLabelWidth: getLabelWidth,
  1520. getMarginFromCircles: getMarginFromCircles,
  1521. getDistanceBetweenCirclesByOverlap: getDistanceBetweenCirclesByOverlap,
  1522. layoutGreedyVenn: layoutGreedyVenn,
  1523. loss: loss,
  1524. nelderMead: nelderMeadModule,
  1525. processVennData: processVennData,
  1526. sortByTotalOverlap: sortByTotalOverlap
  1527. }
  1528. };
  1529. var vennPoint = {
  1530. draw: draw,
  1531. shouldDraw: function () {
  1532. var point = this;
  1533. // Only draw points with single sets.
  1534. return !!point.shapeArgs;
  1535. },
  1536. isValid: function () {
  1537. return isNumber(this.value);
  1538. }
  1539. };
  1540. /**
  1541. * A `venn` series. If the [type](#series.venn.type) option is
  1542. * not specified, it is inherited from [chart.type](#chart.type).
  1543. *
  1544. * @extends series,plotOptions.venn
  1545. * @excluding connectEnds, connectNulls, cropThreshold, dataParser, dataURL,
  1546. * findNearestPointBy, getExtremesFromAll, label, linecap, lineWidth,
  1547. * linkedTo, marker, negativeColor, pointInterval, pointIntervalUnit,
  1548. * pointPlacement, pointStart, softThreshold, stack, stacking, steps,
  1549. * threshold, xAxis, yAxis, zoneAxis, zones, dataSorting
  1550. * @product highcharts
  1551. * @requires modules/venn
  1552. * @apioption series.venn
  1553. */
  1554. /**
  1555. * @type {Array<*>}
  1556. * @extends series.scatter.data
  1557. * @excluding marker, x, y
  1558. * @product highcharts
  1559. * @apioption series.venn.data
  1560. */
  1561. /**
  1562. * The name of the point. Used in data labels and tooltip. If name is not
  1563. * defined then it will default to the joined values in
  1564. * [sets](#series.venn.sets).
  1565. *
  1566. * @sample {highcharts} highcharts/demo/venn-diagram/
  1567. * Venn diagram
  1568. * @sample {highcharts} highcharts/demo/euler-diagram/
  1569. * Euler diagram
  1570. *
  1571. * @type {number}
  1572. * @since 7.0.0
  1573. * @product highcharts
  1574. * @apioption series.venn.data.name
  1575. */
  1576. /**
  1577. * The value of the point, resulting in a relative area of the circle, or area
  1578. * of overlap between two sets in the venn or euler diagram.
  1579. *
  1580. * @sample {highcharts} highcharts/demo/venn-diagram/
  1581. * Venn diagram
  1582. * @sample {highcharts} highcharts/demo/euler-diagram/
  1583. * Euler diagram
  1584. *
  1585. * @type {number}
  1586. * @since 7.0.0
  1587. * @product highcharts
  1588. * @apioption series.venn.data.value
  1589. */
  1590. /**
  1591. * The set or sets the options will be applied to. If a single entry is defined,
  1592. * then it will create a new set. If more than one entry is defined, then it
  1593. * will define the overlap between the sets in the array.
  1594. *
  1595. * @sample {highcharts} highcharts/demo/venn-diagram/
  1596. * Venn diagram
  1597. * @sample {highcharts} highcharts/demo/euler-diagram/
  1598. * Euler diagram
  1599. *
  1600. * @type {Array<string>}
  1601. * @since 7.0.0
  1602. * @product highcharts
  1603. * @apioption series.venn.data.sets
  1604. */
  1605. /**
  1606. * @excluding halo
  1607. * @apioption series.venn.states.hover
  1608. */
  1609. /**
  1610. * @excluding halo
  1611. * @apioption series.venn.states.select
  1612. */
  1613. /**
  1614. * @private
  1615. * @class
  1616. * @name Highcharts.seriesTypes.venn
  1617. *
  1618. * @augments Highcharts.Series
  1619. */
  1620. seriesType('venn', 'scatter', vennOptions, vennSeries, vennPoint);
  1621. /* eslint-disable no-invalid-this */
  1622. // Modify final series options.
  1623. addEvent(seriesTypes.venn, 'afterSetOptions', function (e) {
  1624. var options = e.options, states = options.states;
  1625. if (this.is('venn')) {
  1626. // Explicitly disable all halo options.
  1627. Object.keys(states).forEach(function (state) {
  1628. states[state].halo = false;
  1629. });
  1630. }
  1631. });
  1632. });
  1633. _registerModule(_modules, 'masters/modules/venn.src.js', [], function () {
  1634. });
  1635. }));