axisBreakHelperImpl.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /**
  20. * AUTO-GENERATED FILE. DO NOT MODIFY.
  21. */
  22. /*
  23. * Licensed to the Apache Software Foundation (ASF) under one
  24. * or more contributor license agreements. See the NOTICE file
  25. * distributed with this work for additional information
  26. * regarding copyright ownership. The ASF licenses this file
  27. * to you under the Apache License, Version 2.0 (the
  28. * "License"); you may not use this file except in compliance
  29. * with the License. You may obtain a copy of the License at
  30. *
  31. * http://www.apache.org/licenses/LICENSE-2.0
  32. *
  33. * Unless required by applicable law or agreed to in writing,
  34. * software distributed under the License is distributed on an
  35. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  36. * KIND, either express or implied. See the License for the
  37. * specific language governing permissions and limitations
  38. * under the License.
  39. */
  40. import { makeInner } from '../../util/model.js';
  41. import { assert, each, extend, find, map } from 'zrender/lib/core/util.js';
  42. import { getScaleBreakHelper } from '../../scale/break.js';
  43. import { subPixelOptimizeLine } from 'zrender/lib/graphic/helper/subPixelOptimize.js';
  44. import { applyTransform } from 'zrender/lib/core/vector.js';
  45. import * as matrixUtil from 'zrender/lib/core/matrix.js';
  46. import { AXIS_BREAK_COLLAPSE_ACTION_TYPE, AXIS_BREAK_EXPAND_ACTION_TYPE, AXIS_BREAK_TOGGLE_ACTION_TYPE } from './axisAction.js';
  47. import { labelIntersect, labelLayoutApplyTranslation } from '../../label/labelLayoutHelper.js';
  48. import { registerAxisBreakHelperImpl } from './axisBreakHelper.js';
  49. import { warn } from '../../util/log.js';
  50. import { Group, Line, Point, Polygon, Polyline, WH, XY } from '../../util/graphic.js';
  51. /**
  52. * @caution
  53. * Must not export anything except `installAxisBreakHelper`
  54. */
  55. /**
  56. * The zigzag shapes for axis breaks are generated according to some random
  57. * factors. It should persist as much as possible to avoid constantly
  58. * changing by every user operation.
  59. */
  60. var viewCache = makeInner();
  61. function ensureVisualInCache(visualList, targetBreak) {
  62. var visual = find(visualList, function (item) {
  63. return getScaleBreakHelper().identifyAxisBreak(item.parsedBreak.breakOption, targetBreak.breakOption);
  64. });
  65. if (!visual) {
  66. visualList.push(visual = {
  67. zigzagRandomList: [],
  68. parsedBreak: targetBreak,
  69. shouldRemove: false
  70. });
  71. }
  72. return visual;
  73. }
  74. function resetCacheVisualRemoveFlag(visualList) {
  75. each(visualList, function (item) {
  76. return item.shouldRemove = true;
  77. });
  78. }
  79. function removeUnusedCacheVisual(visualList) {
  80. for (var i = visualList.length - 1; i >= 0; i--) {
  81. if (visualList[i].shouldRemove) {
  82. visualList.splice(i, 1);
  83. }
  84. }
  85. }
  86. function rectCoordBuildBreakAxis(axisGroup, axisView, axisModel, coordSysRect, api) {
  87. var axis = axisModel.axis;
  88. if (axis.scale.isBlank() || !getScaleBreakHelper()) {
  89. return;
  90. }
  91. var breakPairs = getScaleBreakHelper().retrieveAxisBreakPairs(axis.scale.getTicks({
  92. breakTicks: 'only_break'
  93. }), function (tick) {
  94. return tick["break"];
  95. }, false);
  96. if (!breakPairs.length) {
  97. return;
  98. }
  99. var breakAreaModel = axisModel.getModel('breakArea');
  100. var zigzagAmplitude = breakAreaModel.get('zigzagAmplitude');
  101. var zigzagMinSpan = breakAreaModel.get('zigzagMinSpan');
  102. var zigzagMaxSpan = breakAreaModel.get('zigzagMaxSpan');
  103. // Use arbitrary value to avoid dead loop if user gives inappropriate settings.
  104. zigzagMinSpan = Math.max(2, zigzagMinSpan || 0);
  105. zigzagMaxSpan = Math.max(zigzagMinSpan, zigzagMaxSpan || 0);
  106. var expandOnClick = breakAreaModel.get('expandOnClick');
  107. var zigzagZ = breakAreaModel.get('zigzagZ');
  108. var itemStyleModel = breakAreaModel.getModel('itemStyle');
  109. var itemStyle = itemStyleModel.getItemStyle();
  110. var borderColor = itemStyle.stroke;
  111. var borderWidth = itemStyle.lineWidth;
  112. var borderType = itemStyle.lineDash;
  113. var color = itemStyle.fill;
  114. var group = new Group({
  115. ignoreModelZ: true
  116. });
  117. var isAxisHorizontal = axis.isHorizontal();
  118. var cachedVisualList = viewCache(axisView).visualList || (viewCache(axisView).visualList = []);
  119. resetCacheVisualRemoveFlag(cachedVisualList);
  120. var _loop_1 = function (i) {
  121. var parsedBreak = breakPairs[i][0]["break"].parsedBreak;
  122. // Even if brk.gap is 0, we should also draw the breakArea because
  123. // border is sometimes required to be visible (as a line)
  124. var coords = [];
  125. coords[0] = axis.toGlobalCoord(axis.dataToCoord(parsedBreak.vmin, true));
  126. coords[1] = axis.toGlobalCoord(axis.dataToCoord(parsedBreak.vmax, true));
  127. if (coords[1] < coords[0]) {
  128. coords.reverse();
  129. }
  130. var cachedVisual = ensureVisualInCache(cachedVisualList, parsedBreak);
  131. cachedVisual.shouldRemove = false;
  132. var breakGroup = new Group();
  133. addZigzagShapes(cachedVisual.zigzagRandomList, breakGroup, coords[0], coords[1], isAxisHorizontal, parsedBreak);
  134. if (expandOnClick) {
  135. breakGroup.on('click', function () {
  136. var payload = {
  137. type: AXIS_BREAK_EXPAND_ACTION_TYPE,
  138. breaks: [{
  139. start: parsedBreak.breakOption.start,
  140. end: parsedBreak.breakOption.end
  141. }]
  142. };
  143. payload[axis.dim + "AxisIndex"] = axisModel.componentIndex;
  144. api.dispatchAction(payload);
  145. });
  146. }
  147. breakGroup.silent = !expandOnClick;
  148. group.add(breakGroup);
  149. };
  150. for (var i = 0; i < breakPairs.length; i++) {
  151. _loop_1(i);
  152. }
  153. axisGroup.add(group);
  154. removeUnusedCacheVisual(cachedVisualList);
  155. function addZigzagShapes(zigzagRandomList, breakGroup, startCoord, endCoord, isAxisHorizontal, trimmedBreak) {
  156. var polylineStyle = {
  157. stroke: borderColor,
  158. lineWidth: borderWidth,
  159. lineDash: borderType,
  160. fill: 'none'
  161. };
  162. var dimBrk = isAxisHorizontal ? 0 : 1;
  163. var dimZigzag = 1 - dimBrk;
  164. var zigzagCoordMax = coordSysRect[XY[dimZigzag]] + coordSysRect[WH[dimZigzag]];
  165. // Apply `subPixelOptimizeLine` for alignning with break ticks.
  166. function subPixelOpt(brkCoord) {
  167. var pBrk = [];
  168. var dummyP = [];
  169. pBrk[dimBrk] = dummyP[dimBrk] = brkCoord;
  170. pBrk[dimZigzag] = coordSysRect[XY[dimZigzag]];
  171. dummyP[dimZigzag] = zigzagCoordMax;
  172. var dummyShape = {
  173. x1: pBrk[0],
  174. y1: pBrk[1],
  175. x2: dummyP[0],
  176. y2: dummyP[1]
  177. };
  178. subPixelOptimizeLine(dummyShape, dummyShape, {
  179. lineWidth: 1
  180. });
  181. pBrk[0] = dummyShape.x1;
  182. pBrk[1] = dummyShape.y1;
  183. return pBrk[dimBrk];
  184. }
  185. startCoord = subPixelOpt(startCoord);
  186. endCoord = subPixelOpt(endCoord);
  187. var pointsA = [];
  188. var pointsB = [];
  189. var isSwap = true;
  190. var current = coordSysRect[XY[dimZigzag]];
  191. for (var idx = 0;; idx++) {
  192. // Use `isFirstPoint` `isLastPoint` to ensure the intersections between zigzag
  193. // and axis are precise, thus it can join its axis tick correctly.
  194. var isFirstPoint = current === coordSysRect[XY[dimZigzag]];
  195. var isLastPoint = current >= zigzagCoordMax;
  196. if (isLastPoint) {
  197. current = zigzagCoordMax;
  198. }
  199. var pA = [];
  200. var pB = [];
  201. pA[dimBrk] = startCoord;
  202. pB[dimBrk] = endCoord;
  203. if (!isFirstPoint && !isLastPoint) {
  204. pA[dimBrk] += isSwap ? -zigzagAmplitude : zigzagAmplitude;
  205. pB[dimBrk] -= !isSwap ? -zigzagAmplitude : zigzagAmplitude;
  206. }
  207. pA[dimZigzag] = current;
  208. pB[dimZigzag] = current;
  209. pointsA.push(pA);
  210. pointsB.push(pB);
  211. var randomVal = void 0;
  212. if (idx < zigzagRandomList.length) {
  213. randomVal = zigzagRandomList[idx];
  214. } else {
  215. randomVal = Math.random();
  216. zigzagRandomList.push(randomVal);
  217. }
  218. current += randomVal * (zigzagMaxSpan - zigzagMinSpan) + zigzagMinSpan;
  219. isSwap = !isSwap;
  220. if (isLastPoint) {
  221. break;
  222. }
  223. }
  224. var anidSuffix = getScaleBreakHelper().serializeAxisBreakIdentifier(trimmedBreak.breakOption);
  225. // Create two polylines and add them to the breakGroup
  226. breakGroup.add(new Polyline({
  227. anid: "break_a_" + anidSuffix,
  228. shape: {
  229. points: pointsA
  230. },
  231. style: polylineStyle,
  232. z: zigzagZ
  233. }));
  234. /* Add the second polyline and a polygon only if the gap is not zero
  235. * Otherwise if the polyline is with dashed line or being opaque,
  236. * it may not be constant with breaks with non-zero gaps. */
  237. if (trimmedBreak.gapReal !== 0) {
  238. breakGroup.add(new Polyline({
  239. anid: "break_b_" + anidSuffix,
  240. shape: {
  241. // Not reverse to keep the dash stable when dragging resizing.
  242. points: pointsB
  243. },
  244. style: polylineStyle,
  245. z: zigzagZ
  246. }));
  247. // Creating the polygon that fills the area between the polylines
  248. // From end to start for polygon.
  249. var pointsB2 = pointsB.slice();
  250. pointsB2.reverse();
  251. var polygonPoints = pointsA.concat(pointsB2);
  252. breakGroup.add(new Polygon({
  253. anid: "break_c_" + anidSuffix,
  254. shape: {
  255. points: polygonPoints
  256. },
  257. style: {
  258. fill: color,
  259. opacity: itemStyle.opacity
  260. },
  261. z: zigzagZ
  262. }));
  263. }
  264. }
  265. }
  266. function buildAxisBreakLine(axisModel, group, transformGroup, pathBaseProp) {
  267. var axis = axisModel.axis;
  268. var transform = transformGroup.transform;
  269. assert(pathBaseProp.style);
  270. var extent = axis.getExtent();
  271. if (axis.inverse) {
  272. extent = extent.slice();
  273. extent.reverse();
  274. }
  275. var breakPairs = getScaleBreakHelper().retrieveAxisBreakPairs(axis.scale.getTicks({
  276. breakTicks: 'only_break'
  277. }), function (tick) {
  278. return tick["break"];
  279. }, false);
  280. var brkLayoutList = map(breakPairs, function (breakPair) {
  281. var parsedBreak = breakPair[0]["break"].parsedBreak;
  282. var coordPair = [axis.dataToCoord(parsedBreak.vmin, true), axis.dataToCoord(parsedBreak.vmax, true)];
  283. coordPair[0] > coordPair[1] && coordPair.reverse();
  284. return {
  285. coordPair: coordPair,
  286. brkId: getScaleBreakHelper().serializeAxisBreakIdentifier(parsedBreak.breakOption)
  287. };
  288. });
  289. brkLayoutList.sort(function (layout1, layout2) {
  290. return layout1.coordPair[0] - layout2.coordPair[0];
  291. });
  292. var ySegMin = extent[0];
  293. var lastLayout = null;
  294. for (var idx = 0; idx < brkLayoutList.length; idx++) {
  295. var layout = brkLayoutList[idx];
  296. var brkTirmmedMin = Math.max(layout.coordPair[0], extent[0]);
  297. var brkTirmmedMax = Math.min(layout.coordPair[1], extent[1]);
  298. if (ySegMin <= brkTirmmedMin) {
  299. addSeg(ySegMin, brkTirmmedMin, lastLayout, layout);
  300. }
  301. ySegMin = brkTirmmedMax;
  302. lastLayout = layout;
  303. }
  304. if (ySegMin <= extent[1]) {
  305. addSeg(ySegMin, extent[1], lastLayout, null);
  306. }
  307. function addSeg(min, max, layout1, layout2) {
  308. function trans(p1, p2) {
  309. if (transform) {
  310. applyTransform(p1, p1, transform);
  311. applyTransform(p2, p2, transform);
  312. }
  313. }
  314. function subPixelOptimizePP(p1, p2) {
  315. var shape = {
  316. x1: p1[0],
  317. y1: p1[1],
  318. x2: p2[0],
  319. y2: p2[1]
  320. };
  321. subPixelOptimizeLine(shape, shape, pathBaseProp.style);
  322. p1[0] = shape.x1;
  323. p1[1] = shape.y1;
  324. p2[0] = shape.x2;
  325. p2[1] = shape.y2;
  326. }
  327. var lineP1 = [min, 0];
  328. var lineP2 = [max, 0];
  329. // dummy tick is used to align the line segment ends with axis ticks
  330. // after `subPixelOptimizeLine` being applied.
  331. var dummyTickEnd1 = [min, 5];
  332. var dummyTickEnd2 = [max, 5];
  333. trans(lineP1, dummyTickEnd1);
  334. subPixelOptimizePP(lineP1, dummyTickEnd1);
  335. trans(lineP2, dummyTickEnd2);
  336. subPixelOptimizePP(lineP2, dummyTickEnd2);
  337. // Apply it keeping the same as the normal axis line.
  338. subPixelOptimizePP(lineP1, lineP2);
  339. var seg = new Line(extend({
  340. shape: {
  341. x1: lineP1[0],
  342. y1: lineP1[1],
  343. x2: lineP2[0],
  344. y2: lineP2[1]
  345. }
  346. }, pathBaseProp));
  347. group.add(seg);
  348. // Animation should be precise to be consistent with tick and split line animation.
  349. seg.anid = "breakLine_" + (layout1 ? layout1.brkId : '\0') + "_\0_" + (layout2 ? layout2.brkId : '\0');
  350. }
  351. }
  352. /**
  353. * Resolve the overlap of a pair of labels.
  354. *
  355. * [CAUTION] Only label.x/y are allowed to change.
  356. */
  357. function adjustBreakLabelPair(axisInverse, axisRotation, layoutPair) {
  358. if (find(layoutPair, function (item) {
  359. return !item;
  360. })) {
  361. return;
  362. }
  363. var mtv = new Point();
  364. if (!labelIntersect(layoutPair[0], layoutPair[1], mtv, {
  365. // Assert `labelPair` is `[break_min, break_max]`.
  366. // `axis.inverse: true` means a smaller scale value corresponds to a bigger value in axis.extent.
  367. // The axisRotation indicates mtv direction of OBB intersecting.
  368. direction: -(axisInverse ? axisRotation + Math.PI : axisRotation),
  369. touchThreshold: 0,
  370. // If need to resovle intersection align axis by moving labels according to MTV,
  371. // the direction must not be opposite, otherwise cause misleading.
  372. bidirectional: false
  373. })) {
  374. return;
  375. }
  376. // Rotate axis back to (1, 0) direction, to be a standard axis.
  377. var axisStTrans = matrixUtil.create();
  378. matrixUtil.rotate(axisStTrans, axisStTrans, -axisRotation);
  379. var labelPairStTrans = map(layoutPair, function (layout) {
  380. return layout.transform ? matrixUtil.mul(matrixUtil.create(), axisStTrans, layout.transform) : axisStTrans;
  381. });
  382. function isParallelToAxis(whIdx) {
  383. // Assert label[0] and label[1] has the same rotation, so only use [0].
  384. var localRect = layoutPair[0].localRect;
  385. var labelVec0 = new Point(localRect[WH[whIdx]] * labelPairStTrans[0][0], localRect[WH[whIdx]] * labelPairStTrans[0][1]);
  386. return Math.abs(labelVec0.y) < 1e-5;
  387. }
  388. // If overlapping, move pair[0] pair[1] apart a little. We need to calculate a ratio k to
  389. // distribute mtv to pair[0] and pair[1]. This is to place the text gap as close as possible
  390. // to the center of the break ticks, otherwise it might looks weird or misleading.
  391. // - When labels' width/height are not parallel to axis (usually by rotation),
  392. // we can simply treat the k as `0.5`.
  393. var k = 0.5;
  394. // - When labels' width/height are parallel to axis, the width/height need to be considered,
  395. // since they may differ significantly. In this case we keep textAlign as 'center' rather
  396. // than 'left'/'right', due to considerations of space utilization for wide break.gap.
  397. // A sample case: break on xAxis(no inverse) is [200, 300000].
  398. // We calculate k based on the formula below:
  399. // Rotated axis and labels to the direction of (1, 0).
  400. // uval = ( (pair[0].insidePt - mtv*k) + (pair[1].insidePt + mtv*(1-k)) ) / 2 - brkCenter
  401. // 0 <= k <= 1
  402. // |uval| should be as small as possible.
  403. // Derived as follows:
  404. // qval = (pair[0].insidePt + pair[1].insidePt + mtv) / 2 - brkCenter
  405. // k = (qval - uval) / mtv
  406. // min(qval, qval-mtv) <= uval <= max(qval, qval-mtv)
  407. if (isParallelToAxis(0) || isParallelToAxis(1)) {
  408. var rectSt = map(layoutPair, function (layout, idx) {
  409. var rect = layout.localRect.clone();
  410. rect.applyTransform(labelPairStTrans[idx]);
  411. return rect;
  412. });
  413. var brkCenterSt = new Point();
  414. brkCenterSt.copy(layoutPair[0].label).add(layoutPair[1].label).scale(0.5);
  415. brkCenterSt.transform(axisStTrans);
  416. var mtvSt = mtv.clone().transform(axisStTrans);
  417. var insidePtSum = rectSt[0].x + rectSt[1].x + (mtvSt.x >= 0 ? rectSt[0].width : rectSt[1].width);
  418. var qval = (insidePtSum + mtvSt.x) / 2 - brkCenterSt.x;
  419. var uvalMin = Math.min(qval, qval - mtvSt.x);
  420. var uvalMax = Math.max(qval, qval - mtvSt.x);
  421. var uval = uvalMax < 0 ? uvalMax : uvalMin > 0 ? uvalMin : 0;
  422. k = (qval - uval) / mtvSt.x;
  423. }
  424. var delta0 = new Point();
  425. var delta1 = new Point();
  426. Point.scale(delta0, mtv, -k);
  427. Point.scale(delta1, mtv, 1 - k);
  428. labelLayoutApplyTranslation(layoutPair[0], delta0);
  429. labelLayoutApplyTranslation(layoutPair[1], delta1);
  430. }
  431. function updateModelAxisBreak(model, payload) {
  432. var result = {
  433. breaks: []
  434. };
  435. each(payload.breaks, function (inputBrk) {
  436. if (!inputBrk) {
  437. return;
  438. }
  439. var breakOption = find(model.get('breaks', true), function (brkOption) {
  440. return getScaleBreakHelper().identifyAxisBreak(brkOption, inputBrk);
  441. });
  442. if (!breakOption) {
  443. if (process.env.NODE_ENV !== 'production') {
  444. warn("Can not find axis break by start: " + inputBrk.start + ", end: " + inputBrk.end);
  445. }
  446. return;
  447. }
  448. var actionType = payload.type;
  449. var old = {
  450. isExpanded: !!breakOption.isExpanded
  451. };
  452. breakOption.isExpanded = actionType === AXIS_BREAK_EXPAND_ACTION_TYPE ? true : actionType === AXIS_BREAK_COLLAPSE_ACTION_TYPE ? false : actionType === AXIS_BREAK_TOGGLE_ACTION_TYPE ? !breakOption.isExpanded : breakOption.isExpanded;
  453. result.breaks.push({
  454. start: breakOption.start,
  455. end: breakOption.end,
  456. isExpanded: !!breakOption.isExpanded,
  457. old: old
  458. });
  459. });
  460. return result;
  461. }
  462. export function installAxisBreakHelper() {
  463. registerAxisBreakHelperImpl({
  464. adjustBreakLabelPair: adjustBreakLabelPair,
  465. buildAxisBreakLine: buildAxisBreakLine,
  466. rectCoordBuildBreakAxis: rectCoordBuildBreakAxis,
  467. updateModelAxisBreak: updateModelAxisBreak
  468. });
  469. }