barGrid.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  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 { each, defaults, hasOwn, assert } from 'zrender/lib/core/util.js';
  41. import { mathAbs, mathMax, mathMin, parsePercent } from '../util/number.js';
  42. import { isDimensionStacked } from '../data/helper/dataStackHelper.js';
  43. import createRenderPlanner from '../chart/helper/createRenderPlanner.js';
  44. import Axis2D from '../coord/cartesian/Axis2D.js';
  45. import { createFloat32Array } from '../util/vendor.js';
  46. import { makeCallOnlyOnce } from '../util/model.js';
  47. import { isOrdinalScale } from '../scale/helper.js';
  48. import { isCartesian2DInjectedAsDataCoordSys } from '../coord/cartesian/cartesianAxisHelper.js';
  49. import { registerAxisContainShapeHandler } from '../coord/scaleRawExtentInfo.js';
  50. import { eachAxisOnKey, eachSeriesOnAxisOnKey } from '../coord/axisStatistics.js';
  51. import { calcBandWidth } from '../coord/axisBand.js';
  52. import { getStartValue, requireAxisStatisticsForBaseBar } from './barCommon.js';
  53. import { COORD_SYS_TYPE_CARTESIAN_2D } from '../coord/cartesian/GridModel.js';
  54. import { createBandWidthBasedAxisContainShapeHandler, makeAxisStatKey2 } from '../chart/helper/axisSnippets.js';
  55. var callOnlyOnce = makeCallOnlyOnce();
  56. var STACK_PREFIX = '__ec_stack_';
  57. function getSeriesStackId(seriesModel) {
  58. return seriesModel.get('stack') || STACK_PREFIX + seriesModel.seriesIndex;
  59. }
  60. /**
  61. * Return null/undefined if not 'category' axis.
  62. *
  63. * PENDING: The layout on non-'category' axis relies on `bandWidth`, which is calculated
  64. * based on the `linearPositiveMinGap` of series data. This strategy is somewhat heuristic
  65. * and will not be public to custom series until required in future. Additionally, more ec
  66. * options may be introduced for that, because it requires `requireAxisStatistics` to be
  67. * called on custom series that requires this feature.
  68. */
  69. export function computeBarLayoutForCustomSeries(opt) {
  70. if (!isOrdinalScale(opt.axis.scale)) {
  71. return;
  72. }
  73. var bandWidthResult = calcBandWidth(opt.axis);
  74. var params = [];
  75. for (var i = 0; i < opt.count || 0; i++) {
  76. params.push(defaults({
  77. stackId: STACK_PREFIX + i
  78. }, opt));
  79. }
  80. var widthAndOffsets = calcBarWidthAndOffset({
  81. bandWidthResult: bandWidthResult,
  82. seriesInfo: params
  83. });
  84. var result = [];
  85. for (var i = 0; i < opt.count; i++) {
  86. var item = widthAndOffsets[STACK_PREFIX + i];
  87. item.offsetCenter = item.offset + item.width / 2;
  88. result.push(item);
  89. }
  90. return result;
  91. }
  92. /**
  93. * NOTICE: This layout is based on axis pixel extent and scale extent.
  94. * It may be used on estimation, where axis pixel extent and scale extent
  95. * are approximately set. But the result should not be cached since the
  96. * axis pixel extent and scale extent may be changed finally.
  97. */
  98. function makeColumnLayoutOnAxisReal(baseAxis, seriesType) {
  99. var seriesInfoListOnAxis = createLayoutInfoListOnAxis(baseAxis, seriesType);
  100. seriesInfoListOnAxis.columnMap = calcBarWidthAndOffset(seriesInfoListOnAxis);
  101. return seriesInfoListOnAxis;
  102. }
  103. function createLayoutInfoListOnAxis(baseAxis, seriesType) {
  104. var axisStatKey = makeAxisStatKey2(seriesType, COORD_SYS_TYPE_CARTESIAN_2D);
  105. var seriesInfoOnAxis = [];
  106. var bandWidthResult = calcBandWidth(baseAxis, {
  107. fromStat: {
  108. key: axisStatKey
  109. },
  110. min: 1
  111. });
  112. eachSeriesOnAxisOnKey(baseAxis, axisStatKey, function (seriesModel) {
  113. seriesInfoOnAxis.push({
  114. barWidth: parsePercent(seriesModel.get('barWidth'), bandWidthResult.w),
  115. barMaxWidth: parsePercent(seriesModel.get('barMaxWidth'), bandWidthResult.w),
  116. barMinWidth: parsePercent(
  117. // barMinWidth by default is 0.5 / 1 in cartesian. Because in value axis,
  118. // the auto-calculated bar width might be less than 0.5 / 1.
  119. seriesModel.get('barMinWidth') || (isInLargeMode(seriesModel) ? 0.5 : 1), bandWidthResult.w),
  120. barGap: seriesModel.get('barGap'),
  121. barCategoryGap: seriesModel.get('barCategoryGap'),
  122. defaultBarGap: seriesModel.get('defaultBarGap'),
  123. stackId: getSeriesStackId(seriesModel)
  124. });
  125. });
  126. return {
  127. bandWidthResult: bandWidthResult,
  128. seriesInfo: seriesInfoOnAxis
  129. };
  130. }
  131. /**
  132. * CAUTION: When multiple series are laid out on one axis, relevant ec options effect all series.
  133. * But for historical reason, these options are configured on each series option, which may
  134. * introduce confliction. The legacy implementation uses some options (e.g., `defaultBarGap`)
  135. * from the first declared series, and other options (e.g., `barGap`, `barCategoryGap`) from the last declared
  136. * series. Nevertheless, We remain this design to avoid breaking change.
  137. */
  138. function calcBarWidthAndOffset(seriesInfoOnAxis) {
  139. var bandWidth = seriesInfoOnAxis.bandWidthResult.w;
  140. var remainedWidth = bandWidth;
  141. var autoWidthCount = 0;
  142. var barCategoryGapOption;
  143. var barGapOption;
  144. var stackIdList = [];
  145. var stackMap = {};
  146. each(seriesInfoOnAxis.seriesInfo, function (seriesInfo, idx) {
  147. if (!idx) {
  148. barGapOption = seriesInfo.defaultBarGap || 0;
  149. }
  150. var stackId = seriesInfo.stackId;
  151. if (!hasOwn(stackMap, stackId)) {
  152. autoWidthCount++;
  153. }
  154. var stackItem = stackMap[stackId];
  155. if (!stackItem) {
  156. stackItem = stackMap[stackId] = {
  157. width: 0,
  158. maxWidth: 0
  159. };
  160. stackIdList.push(stackId);
  161. }
  162. var barWidth = seriesInfo.barWidth;
  163. if (barWidth && !stackItem.width) {
  164. // See #6312, do not restrict width.
  165. stackItem.width = barWidth;
  166. barWidth = mathMin(remainedWidth, barWidth);
  167. remainedWidth -= barWidth;
  168. }
  169. var barMaxWidth = seriesInfo.barMaxWidth;
  170. barMaxWidth && (stackItem.maxWidth = barMaxWidth);
  171. var barMinWidth = seriesInfo.barMinWidth;
  172. barMinWidth && (stackItem.minWidth = barMinWidth);
  173. var barGap = seriesInfo.barGap;
  174. barGap != null && (barGapOption = barGap);
  175. var barCategoryGap = seriesInfo.barCategoryGap;
  176. barCategoryGap != null && (barCategoryGapOption = barCategoryGap);
  177. });
  178. if (barCategoryGapOption == null) {
  179. // More columns in one group
  180. // the spaces between group is smaller. Or the column will be too thin.
  181. barCategoryGapOption = mathMax(35 - stackIdList.length * 4, 15) + '%';
  182. }
  183. var barCategoryGapNum = parsePercent(barCategoryGapOption, bandWidth);
  184. var barGapPercent = parsePercent(barGapOption, 1);
  185. var autoWidth = (remainedWidth - barCategoryGapNum) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
  186. autoWidth = mathMax(autoWidth, 0);
  187. // Find if any auto calculated bar exceeded maxBarWidth
  188. each(stackIdList, function (stackId) {
  189. var column = stackMap[stackId];
  190. var maxWidth = column.maxWidth;
  191. var minWidth = column.minWidth;
  192. if (!column.width) {
  193. var finalWidth = autoWidth;
  194. if (maxWidth && maxWidth < finalWidth) {
  195. finalWidth = mathMin(maxWidth, remainedWidth);
  196. }
  197. // `minWidth` has higher priority. `minWidth` decide that whether the
  198. // bar is able to be visible. So `minWidth` should not be restricted
  199. // by `maxWidth` or `remainedWidth` (which is from `bandWidth`). In
  200. // the extreme cases for `value` axis, bars are allowed to overlap
  201. // with each other if `minWidth` specified.
  202. if (minWidth && minWidth > finalWidth) {
  203. finalWidth = minWidth;
  204. }
  205. if (finalWidth !== autoWidth) {
  206. column.width = finalWidth;
  207. remainedWidth -= finalWidth + barGapPercent * finalWidth;
  208. autoWidthCount--;
  209. }
  210. } else {
  211. // `barMinWidth/barMaxWidth` has higher priority than `barWidth`, as
  212. // CSS does. Because barWidth can be a percent value, where
  213. // `barMaxWidth` can be used to restrict the final width.
  214. var finalWidth = column.width;
  215. if (maxWidth) {
  216. finalWidth = mathMin(finalWidth, maxWidth);
  217. }
  218. // `minWidth` has higher priority, as described above
  219. if (minWidth) {
  220. finalWidth = mathMax(finalWidth, minWidth);
  221. }
  222. column.width = finalWidth;
  223. remainedWidth -= finalWidth + barGapPercent * finalWidth;
  224. autoWidthCount--;
  225. }
  226. });
  227. // Recalculate width again
  228. autoWidth = (remainedWidth - barCategoryGapNum) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
  229. autoWidth = mathMax(autoWidth, 0);
  230. var widthSum = 0;
  231. var lastColumn;
  232. each(stackIdList, function (stackId) {
  233. var column = stackMap[stackId];
  234. if (!column.width) {
  235. column.width = autoWidth;
  236. }
  237. lastColumn = column;
  238. widthSum += column.width * (1 + barGapPercent);
  239. });
  240. if (lastColumn) {
  241. widthSum -= lastColumn.width * barGapPercent;
  242. }
  243. var result = {};
  244. var offset = -widthSum / 2;
  245. each(stackIdList, function (stackId) {
  246. var column = stackMap[stackId];
  247. result[stackId] = result[stackId] || {
  248. bandWidth: bandWidth,
  249. offset: offset,
  250. width: column.width
  251. };
  252. offset += column.width * (1 + barGapPercent);
  253. });
  254. return result;
  255. }
  256. export function createCrossSeriesLayoutHandler(seriesType) {
  257. return {
  258. seriesType: seriesType,
  259. overallReset: function (ecModel) {
  260. var axisStatKey = makeAxisStatKey2(seriesType, COORD_SYS_TYPE_CARTESIAN_2D);
  261. eachAxisOnKey(ecModel, axisStatKey, function (axis) {
  262. if (process.env.NODE_ENV !== 'production') {
  263. assert(axis instanceof Axis2D);
  264. }
  265. var columnLayout = makeColumnLayoutOnAxisReal(axis, seriesType);
  266. eachSeriesOnAxisOnKey(axis, axisStatKey, function (seriesModel) {
  267. var columnLayoutInfo = columnLayout.columnMap[getSeriesStackId(seriesModel)];
  268. seriesModel.getData().setLayout({
  269. bandWidth: columnLayoutInfo.bandWidth,
  270. offset: columnLayoutInfo.offset,
  271. size: columnLayoutInfo.width
  272. });
  273. });
  274. });
  275. }
  276. };
  277. }
  278. ;
  279. // TODO: Do not support stack in large mode yet.
  280. export function createProgressiveLayout(seriesType) {
  281. return {
  282. seriesType: seriesType,
  283. plan: createRenderPlanner(),
  284. reset: function (seriesModel) {
  285. if (!isCartesian2DInjectedAsDataCoordSys(seriesModel)) {
  286. return;
  287. }
  288. var data = seriesModel.getData();
  289. var cartesian = seriesModel.coordinateSystem;
  290. var baseAxis = cartesian.getBaseAxis();
  291. var valueAxis = cartesian.getOtherAxis(baseAxis);
  292. var valueDimIdx = data.getDimensionIndex(data.mapDimension(valueAxis.dim));
  293. var baseDimIdx = data.getDimensionIndex(data.mapDimension(baseAxis.dim));
  294. var drawBackground = seriesModel.get('showBackground', true);
  295. var valueDim = data.mapDimension(valueAxis.dim);
  296. var stackResultDim = data.getCalculationInfo('stackResultDimension');
  297. var stacked = isDimensionStacked(data, valueDim) && !!data.getCalculationInfo('stackedOnSeries');
  298. var isValueAxisH = valueAxis.isHorizontal();
  299. var valueAxisStart = valueAxis.toGlobalCoord(valueAxis.dataToCoord(getStartValue(valueAxis)));
  300. var isLarge = isInLargeMode(seriesModel);
  301. var barMinHeight = seriesModel.get('barMinHeight') || 0;
  302. var stackedDimIdx = stackResultDim && data.getDimensionIndex(stackResultDim);
  303. // Layout info.
  304. var columnWidth = data.getLayout('size');
  305. var columnOffset = data.getLayout('offset');
  306. return {
  307. progress: function (params, data) {
  308. var count = params.count;
  309. var largePoints = isLarge && createFloat32Array(count * 3);
  310. var largeBackgroundPoints = isLarge && drawBackground && createFloat32Array(count * 3);
  311. var largeDataIndices = isLarge && createFloat32Array(count);
  312. var coordLayout = cartesian.master.getRect();
  313. var bgSize = isValueAxisH ? coordLayout.width : coordLayout.height;
  314. var dataIndex;
  315. var store = data.getStore();
  316. var idxOffset = 0;
  317. while ((dataIndex = params.next()) != null) {
  318. var value = store.get(stacked ? stackedDimIdx : valueDimIdx, dataIndex);
  319. var baseValue = store.get(baseDimIdx, dataIndex);
  320. var baseCoord = valueAxisStart;
  321. var stackStartValue = void 0;
  322. // Because of the barMinHeight, we can not use the value in
  323. // stackResultDimension directly.
  324. if (stacked) {
  325. stackStartValue = +value - store.get(valueDimIdx, dataIndex);
  326. }
  327. var x = void 0;
  328. var y = void 0;
  329. var width = void 0;
  330. var height = void 0;
  331. if (isValueAxisH) {
  332. var coord = cartesian.dataToPoint([value, baseValue]);
  333. if (stacked) {
  334. baseCoord = cartesian.dataToPoint([stackStartValue, baseValue])[0];
  335. }
  336. x = baseCoord;
  337. y = coord[1] + columnOffset;
  338. width = coord[0] - baseCoord;
  339. height = columnWidth;
  340. if (mathAbs(width) < barMinHeight) {
  341. width = (width < 0 ? -1 : 1) * barMinHeight;
  342. }
  343. } else {
  344. var coord = cartesian.dataToPoint([baseValue, value]);
  345. if (stacked) {
  346. baseCoord = cartesian.dataToPoint([baseValue, stackStartValue])[1];
  347. }
  348. x = coord[0] + columnOffset;
  349. y = baseCoord;
  350. width = columnWidth;
  351. height = coord[1] - baseCoord;
  352. if (mathAbs(height) < barMinHeight) {
  353. // Include zero to has a positive bar
  354. height = (height <= 0 ? -1 : 1) * barMinHeight;
  355. }
  356. }
  357. if (!isLarge) {
  358. data.setItemLayout(dataIndex, {
  359. x: x,
  360. y: y,
  361. width: width,
  362. height: height
  363. });
  364. } else {
  365. largePoints[idxOffset] = x;
  366. largePoints[idxOffset + 1] = y;
  367. largePoints[idxOffset + 2] = isValueAxisH ? width : height;
  368. if (largeBackgroundPoints) {
  369. largeBackgroundPoints[idxOffset] = isValueAxisH ? coordLayout.x : x;
  370. largeBackgroundPoints[idxOffset + 1] = isValueAxisH ? y : coordLayout.y;
  371. largeBackgroundPoints[idxOffset + 2] = bgSize;
  372. }
  373. largeDataIndices[dataIndex] = dataIndex;
  374. }
  375. idxOffset += 3;
  376. }
  377. if (isLarge) {
  378. data.setLayout({
  379. largePoints: largePoints,
  380. largeDataIndices: largeDataIndices,
  381. largeBackgroundPoints: largeBackgroundPoints,
  382. valueAxisHorizontal: isValueAxisH
  383. });
  384. }
  385. }
  386. };
  387. }
  388. };
  389. }
  390. function isInLargeMode(seriesModel) {
  391. return seriesModel.pipelineContext && seriesModel.pipelineContext.large;
  392. }
  393. function barGridCreateAxisContainShapeHandler(seriesType) {
  394. // See also #6728, #4862, `test/bar-overflow-time-plot.html` `test/bar-overflow-plot2.html`
  395. // NOTE:
  396. // Series shapes may overflow `bandWidth` when ec option is set like:
  397. // - `barWidth` > 100%.
  398. // - `barWidth` is set as absolute pixel values and `dataZoom` is used, since `bandWidth` is
  399. // calculated on data space rather than pixel space.
  400. // We originally used `calcShapeOverflowSupplement` to cover this case, but it still can not
  401. // resolve pixel `barWidth` case perfectly. A thorough solution may introduce considerable complex,
  402. // but may not necessary, since users can avoid it by proper ec option settings.
  403. // Therefore, we use simply `createBandWidthBasedAxisContainShapeHandler` to calculate "containShape"
  404. // only based on `bandWidth`.
  405. return createBandWidthBasedAxisContainShapeHandler(makeAxisStatKey2(seriesType, COORD_SYS_TYPE_CARTESIAN_2D));
  406. // return function (axis, scale, ecModel) {
  407. // if (!countSeriesOnAxisOnKey(axis, makeAxisStatKey2(seriesType, COORD_SYS_TYPE_CARTESIAN_2D))) {
  408. // return; // Quick path - in most cases there is no bar on non-ordinal axis.
  409. // }
  410. // const columnLayout = makeColumnLayoutOnAxisReal(axis, seriesType);
  411. // return calcShapeOverflowSupplement(columnLayout);
  412. // };
  413. }
  414. // /**
  415. // * @see [AXIS_CONTAIN_SHAPE_COMMON_STRATEGY] for more details.
  416. // */
  417. // function calcShapeOverflowSupplement(
  418. // columnLayout: BarGridColumnLayoutOnAxis | NullUndefined
  419. // ): number[] | NullUndefined {
  420. // if (columnLayout == null) {
  421. // return;
  422. // }
  423. // const bandWidthResult = columnLayout.bandWidthResult;
  424. // const invRatio = bandWidthResult.invRatio;
  425. // if (!isNullableNumberFinite(invRatio)) {
  426. // return; // No series data or no more than one distinct valid data values.
  427. // }
  428. // const barsBoundPx = initExtentForUnion();
  429. // const bandWidth = bandWidthResult.w;
  430. // // Union `-bandWidth / 2` and `bandWidth / 2` to provide extra space for visually preferred,
  431. // // Otherwise the bars on the edges may overlap with axis line.
  432. // // And it also includes `0`, which ensures `barsBoundPx[0] <= 0 <= barsBoundPx[1]`.
  433. // unionExtentFromNumber(barsBoundPx, -bandWidth / 2);
  434. // unionExtentFromNumber(barsBoundPx, bandWidth / 2);
  435. // // Shapes may overflow the `bandWidth`. For example, that might happen in `pictorialBar`.
  436. // // Therefore, we also involve shape size (mapped to data scale) in this expansion calculation.
  437. // each(columnLayout.columnMap, function (item) {
  438. // unionExtentFromNumber(barsBoundPx, item.offset);
  439. // unionExtentFromNumber(barsBoundPx, item.offset + item.width);
  440. // });
  441. // if (extentHasValue(barsBoundPx)) {
  442. // // Convert from pixel domain to data domain, since the `barsBoundPx` is calculated based on
  443. // // `minGap` and extent on data domain.
  444. // return [barsBoundPx[0] * invRatio, barsBoundPx[1] * invRatio];
  445. // // If AXIS_BAND_WIDTH_KIND_SINGULAR, extent expansion is not needed.
  446. // }
  447. // }
  448. export function registerBarGridAxisHandlers(registers) {
  449. callOnlyOnce(registers, function () {
  450. function register(seriesType) {
  451. var axisStatKey = makeAxisStatKey2(seriesType, COORD_SYS_TYPE_CARTESIAN_2D);
  452. requireAxisStatisticsForBaseBar(registers, axisStatKey, seriesType, COORD_SYS_TYPE_CARTESIAN_2D);
  453. registerAxisContainShapeHandler(axisStatKey, barGridCreateAxisContainShapeHandler(seriesType));
  454. }
  455. register('bar');
  456. register('pictorialBar');
  457. });
  458. }