barGrid.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  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. var zrUtil = require("zrender/lib/core/util");
  20. var _number = require("../util/number");
  21. var parsePercent = _number.parsePercent;
  22. var _dataStackHelper = require("../data/helper/dataStackHelper");
  23. var isDimensionStacked = _dataStackHelper.isDimensionStacked;
  24. var createRenderPlanner = require("../chart/helper/createRenderPlanner");
  25. /*
  26. * Licensed to the Apache Software Foundation (ASF) under one
  27. * or more contributor license agreements. See the NOTICE file
  28. * distributed with this work for additional information
  29. * regarding copyright ownership. The ASF licenses this file
  30. * to you under the Apache License, Version 2.0 (the
  31. * "License"); you may not use this file except in compliance
  32. * with the License. You may obtain a copy of the License at
  33. *
  34. * http://www.apache.org/licenses/LICENSE-2.0
  35. *
  36. * Unless required by applicable law or agreed to in writing,
  37. * software distributed under the License is distributed on an
  38. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  39. * KIND, either express or implied. See the License for the
  40. * specific language governing permissions and limitations
  41. * under the License.
  42. */
  43. /* global Float32Array */
  44. var STACK_PREFIX = '__ec_stack_';
  45. var LARGE_BAR_MIN_WIDTH = 0.5;
  46. var LargeArr = typeof Float32Array !== 'undefined' ? Float32Array : Array;
  47. function getSeriesStackId(seriesModel) {
  48. return seriesModel.get('stack') || STACK_PREFIX + seriesModel.seriesIndex;
  49. }
  50. function getAxisKey(axis) {
  51. return axis.dim + axis.index;
  52. }
  53. /**
  54. * @param {Object} opt
  55. * @param {module:echarts/coord/Axis} opt.axis Only support category axis currently.
  56. * @param {number} opt.count Positive interger.
  57. * @param {number} [opt.barWidth]
  58. * @param {number} [opt.barMaxWidth]
  59. * @param {number} [opt.barMinWidth]
  60. * @param {number} [opt.barGap]
  61. * @param {number} [opt.barCategoryGap]
  62. * @return {Object} {width, offset, offsetCenter} If axis.type is not 'category', return undefined.
  63. */
  64. function getLayoutOnAxis(opt) {
  65. var params = [];
  66. var baseAxis = opt.axis;
  67. var axisKey = 'axis0';
  68. if (baseAxis.type !== 'category') {
  69. return;
  70. }
  71. var bandWidth = baseAxis.getBandWidth();
  72. for (var i = 0; i < opt.count || 0; i++) {
  73. params.push(zrUtil.defaults({
  74. bandWidth: bandWidth,
  75. axisKey: axisKey,
  76. stackId: STACK_PREFIX + i
  77. }, opt));
  78. }
  79. var widthAndOffsets = doCalBarWidthAndOffset(params);
  80. var result = [];
  81. for (var i = 0; i < opt.count; i++) {
  82. var item = widthAndOffsets[axisKey][STACK_PREFIX + i];
  83. item.offsetCenter = item.offset + item.width / 2;
  84. result.push(item);
  85. }
  86. return result;
  87. }
  88. function prepareLayoutBarSeries(seriesType, ecModel) {
  89. var seriesModels = [];
  90. ecModel.eachSeriesByType(seriesType, function (seriesModel) {
  91. // Check series coordinate, do layout for cartesian2d only
  92. if (isOnCartesian(seriesModel) && !isInLargeMode(seriesModel)) {
  93. seriesModels.push(seriesModel);
  94. }
  95. });
  96. return seriesModels;
  97. }
  98. /**
  99. * Map from (baseAxis.dim + '_' + baseAxis.index) to min gap of two adjacent
  100. * values.
  101. * This works for time axes, value axes, and log axes.
  102. * For a single time axis, return value is in the form like
  103. * {'x_0': [1000000]}.
  104. * The value of 1000000 is in milliseconds.
  105. */
  106. function getValueAxesMinGaps(barSeries) {
  107. /**
  108. * Map from axis.index to values.
  109. * For a single time axis, axisValues is in the form like
  110. * {'x_0': [1495555200000, 1495641600000, 1495728000000]}.
  111. * Items in axisValues[x], e.g. 1495555200000, are time values of all
  112. * series.
  113. */
  114. var axisValues = {};
  115. zrUtil.each(barSeries, function (seriesModel) {
  116. var cartesian = seriesModel.coordinateSystem;
  117. var baseAxis = cartesian.getBaseAxis();
  118. if (baseAxis.type !== 'time' && baseAxis.type !== 'value') {
  119. return;
  120. }
  121. var data = seriesModel.getData();
  122. var key = baseAxis.dim + '_' + baseAxis.index;
  123. var dim = data.mapDimension(baseAxis.dim);
  124. for (var i = 0, cnt = data.count(); i < cnt; ++i) {
  125. var value = data.get(dim, i);
  126. if (!axisValues[key]) {
  127. // No previous data for the axis
  128. axisValues[key] = [value];
  129. } else {
  130. // No value in previous series
  131. axisValues[key].push(value);
  132. } // Ignore duplicated time values in the same axis
  133. }
  134. });
  135. var axisMinGaps = [];
  136. for (var key in axisValues) {
  137. if (axisValues.hasOwnProperty(key)) {
  138. var valuesInAxis = axisValues[key];
  139. if (valuesInAxis) {
  140. // Sort axis values into ascending order to calculate gaps
  141. valuesInAxis.sort(function (a, b) {
  142. return a - b;
  143. });
  144. var min = null;
  145. for (var j = 1; j < valuesInAxis.length; ++j) {
  146. var delta = valuesInAxis[j] - valuesInAxis[j - 1];
  147. if (delta > 0) {
  148. // Ignore 0 delta because they are of the same axis value
  149. min = min === null ? delta : Math.min(min, delta);
  150. }
  151. } // Set to null if only have one data
  152. axisMinGaps[key] = min;
  153. }
  154. }
  155. }
  156. return axisMinGaps;
  157. }
  158. function makeColumnLayout(barSeries) {
  159. var axisMinGaps = getValueAxesMinGaps(barSeries);
  160. var seriesInfoList = [];
  161. zrUtil.each(barSeries, function (seriesModel) {
  162. var cartesian = seriesModel.coordinateSystem;
  163. var baseAxis = cartesian.getBaseAxis();
  164. var axisExtent = baseAxis.getExtent();
  165. var bandWidth;
  166. if (baseAxis.type === 'category') {
  167. bandWidth = baseAxis.getBandWidth();
  168. } else if (baseAxis.type === 'value' || baseAxis.type === 'time') {
  169. var key = baseAxis.dim + '_' + baseAxis.index;
  170. var minGap = axisMinGaps[key];
  171. var extentSpan = Math.abs(axisExtent[1] - axisExtent[0]);
  172. var scale = baseAxis.scale.getExtent();
  173. var scaleSpan = Math.abs(scale[1] - scale[0]);
  174. bandWidth = minGap ? extentSpan / scaleSpan * minGap : extentSpan; // When there is only one data value
  175. } else {
  176. var data = seriesModel.getData();
  177. bandWidth = Math.abs(axisExtent[1] - axisExtent[0]) / data.count();
  178. }
  179. var barWidth = parsePercent(seriesModel.get('barWidth'), bandWidth);
  180. var barMaxWidth = parsePercent(seriesModel.get('barMaxWidth'), bandWidth);
  181. var barMinWidth = parsePercent( // barMinWidth by default is 1 in cartesian. Because in value axis,
  182. // the auto-calculated bar width might be less than 1.
  183. seriesModel.get('barMinWidth') || 1, bandWidth);
  184. var barGap = seriesModel.get('barGap');
  185. var barCategoryGap = seriesModel.get('barCategoryGap');
  186. seriesInfoList.push({
  187. bandWidth: bandWidth,
  188. barWidth: barWidth,
  189. barMaxWidth: barMaxWidth,
  190. barMinWidth: barMinWidth,
  191. barGap: barGap,
  192. barCategoryGap: barCategoryGap,
  193. axisKey: getAxisKey(baseAxis),
  194. stackId: getSeriesStackId(seriesModel)
  195. });
  196. });
  197. return doCalBarWidthAndOffset(seriesInfoList);
  198. }
  199. function doCalBarWidthAndOffset(seriesInfoList) {
  200. // Columns info on each category axis. Key is cartesian name
  201. var columnsMap = {};
  202. zrUtil.each(seriesInfoList, function (seriesInfo, idx) {
  203. var axisKey = seriesInfo.axisKey;
  204. var bandWidth = seriesInfo.bandWidth;
  205. var columnsOnAxis = columnsMap[axisKey] || {
  206. bandWidth: bandWidth,
  207. remainedWidth: bandWidth,
  208. autoWidthCount: 0,
  209. categoryGap: '20%',
  210. gap: '30%',
  211. stacks: {}
  212. };
  213. var stacks = columnsOnAxis.stacks;
  214. columnsMap[axisKey] = columnsOnAxis;
  215. var stackId = seriesInfo.stackId;
  216. if (!stacks[stackId]) {
  217. columnsOnAxis.autoWidthCount++;
  218. }
  219. stacks[stackId] = stacks[stackId] || {
  220. width: 0,
  221. maxWidth: 0
  222. }; // Caution: In a single coordinate system, these barGrid attributes
  223. // will be shared by series. Consider that they have default values,
  224. // only the attributes set on the last series will work.
  225. // Do not change this fact unless there will be a break change.
  226. var barWidth = seriesInfo.barWidth;
  227. if (barWidth && !stacks[stackId].width) {
  228. // See #6312, do not restrict width.
  229. stacks[stackId].width = barWidth;
  230. barWidth = Math.min(columnsOnAxis.remainedWidth, barWidth);
  231. columnsOnAxis.remainedWidth -= barWidth;
  232. }
  233. var barMaxWidth = seriesInfo.barMaxWidth;
  234. barMaxWidth && (stacks[stackId].maxWidth = barMaxWidth);
  235. var barMinWidth = seriesInfo.barMinWidth;
  236. barMinWidth && (stacks[stackId].minWidth = barMinWidth);
  237. var barGap = seriesInfo.barGap;
  238. barGap != null && (columnsOnAxis.gap = barGap);
  239. var barCategoryGap = seriesInfo.barCategoryGap;
  240. barCategoryGap != null && (columnsOnAxis.categoryGap = barCategoryGap);
  241. });
  242. var result = {};
  243. zrUtil.each(columnsMap, function (columnsOnAxis, coordSysName) {
  244. result[coordSysName] = {};
  245. var stacks = columnsOnAxis.stacks;
  246. var bandWidth = columnsOnAxis.bandWidth;
  247. var categoryGap = parsePercent(columnsOnAxis.categoryGap, bandWidth);
  248. var barGapPercent = parsePercent(columnsOnAxis.gap, 1);
  249. var remainedWidth = columnsOnAxis.remainedWidth;
  250. var autoWidthCount = columnsOnAxis.autoWidthCount;
  251. var autoWidth = (remainedWidth - categoryGap) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
  252. autoWidth = Math.max(autoWidth, 0); // Find if any auto calculated bar exceeded maxBarWidth
  253. zrUtil.each(stacks, function (column) {
  254. var maxWidth = column.maxWidth;
  255. var minWidth = column.minWidth;
  256. if (!column.width) {
  257. var finalWidth = autoWidth;
  258. if (maxWidth && maxWidth < finalWidth) {
  259. finalWidth = Math.min(maxWidth, remainedWidth);
  260. } // `minWidth` has higher priority. `minWidth` decide that wheter the
  261. // bar is able to be visible. So `minWidth` should not be restricted
  262. // by `maxWidth` or `remainedWidth` (which is from `bandWidth`). In
  263. // the extreme cases for `value` axis, bars are allowed to overlap
  264. // with each other if `minWidth` specified.
  265. if (minWidth && minWidth > finalWidth) {
  266. finalWidth = minWidth;
  267. }
  268. if (finalWidth !== autoWidth) {
  269. column.width = finalWidth;
  270. remainedWidth -= finalWidth + barGapPercent * finalWidth;
  271. autoWidthCount--;
  272. }
  273. } else {
  274. // `barMinWidth/barMaxWidth` has higher priority than `barWidth`, as
  275. // CSS does. Becuase barWidth can be a percent value, where
  276. // `barMaxWidth` can be used to restrict the final width.
  277. var finalWidth = column.width;
  278. if (maxWidth) {
  279. finalWidth = Math.min(finalWidth, maxWidth);
  280. } // `minWidth` has higher priority, as described above
  281. if (minWidth) {
  282. finalWidth = Math.max(finalWidth, minWidth);
  283. }
  284. column.width = finalWidth;
  285. remainedWidth -= finalWidth + barGapPercent * finalWidth;
  286. autoWidthCount--;
  287. }
  288. }); // Recalculate width again
  289. autoWidth = (remainedWidth - categoryGap) / (autoWidthCount + (autoWidthCount - 1) * barGapPercent);
  290. autoWidth = Math.max(autoWidth, 0);
  291. var widthSum = 0;
  292. var lastColumn;
  293. zrUtil.each(stacks, function (column, idx) {
  294. if (!column.width) {
  295. column.width = autoWidth;
  296. }
  297. lastColumn = column;
  298. widthSum += column.width * (1 + barGapPercent);
  299. });
  300. if (lastColumn) {
  301. widthSum -= lastColumn.width * barGapPercent;
  302. }
  303. var offset = -widthSum / 2;
  304. zrUtil.each(stacks, function (column, stackId) {
  305. result[coordSysName][stackId] = result[coordSysName][stackId] || {
  306. bandWidth: bandWidth,
  307. offset: offset,
  308. width: column.width
  309. };
  310. offset += column.width * (1 + barGapPercent);
  311. });
  312. });
  313. return result;
  314. }
  315. /**
  316. * @param {Object} barWidthAndOffset The result of makeColumnLayout
  317. * @param {module:echarts/coord/Axis} axis
  318. * @param {module:echarts/model/Series} [seriesModel] If not provided, return all.
  319. * @return {Object} {stackId: {offset, width}} or {offset, width} if seriesModel provided.
  320. */
  321. function retrieveColumnLayout(barWidthAndOffset, axis, seriesModel) {
  322. if (barWidthAndOffset && axis) {
  323. var result = barWidthAndOffset[getAxisKey(axis)];
  324. if (result != null && seriesModel != null) {
  325. result = result[getSeriesStackId(seriesModel)];
  326. }
  327. return result;
  328. }
  329. }
  330. /**
  331. * @param {string} seriesType
  332. * @param {module:echarts/model/Global} ecModel
  333. */
  334. function layout(seriesType, ecModel) {
  335. var seriesModels = prepareLayoutBarSeries(seriesType, ecModel);
  336. var barWidthAndOffset = makeColumnLayout(seriesModels);
  337. var lastStackCoords = {};
  338. var lastStackCoordsOrigin = {};
  339. zrUtil.each(seriesModels, function (seriesModel) {
  340. var data = seriesModel.getData();
  341. var cartesian = seriesModel.coordinateSystem;
  342. var baseAxis = cartesian.getBaseAxis();
  343. var stackId = getSeriesStackId(seriesModel);
  344. var columnLayoutInfo = barWidthAndOffset[getAxisKey(baseAxis)][stackId];
  345. var columnOffset = columnLayoutInfo.offset;
  346. var columnWidth = columnLayoutInfo.width;
  347. var valueAxis = cartesian.getOtherAxis(baseAxis);
  348. var barMinHeight = seriesModel.get('barMinHeight') || 0;
  349. lastStackCoords[stackId] = lastStackCoords[stackId] || [];
  350. lastStackCoordsOrigin[stackId] = lastStackCoordsOrigin[stackId] || []; // Fix #4243
  351. data.setLayout({
  352. bandWidth: columnLayoutInfo.bandWidth,
  353. offset: columnOffset,
  354. size: columnWidth
  355. });
  356. var valueDim = data.mapDimension(valueAxis.dim);
  357. var baseDim = data.mapDimension(baseAxis.dim);
  358. var stacked = isDimensionStacked(data, valueDim
  359. /*, baseDim*/
  360. );
  361. var isValueAxisH = valueAxis.isHorizontal();
  362. var valueAxisStart = getValueAxisStart(baseAxis, valueAxis, stacked);
  363. for (var idx = 0, len = data.count(); idx < len; idx++) {
  364. var value = data.get(valueDim, idx);
  365. var baseValue = data.get(baseDim, idx);
  366. var sign = value >= 0 ? 'p' : 'n';
  367. var baseCoord = valueAxisStart; // Because of the barMinHeight, we can not use the value in
  368. // stackResultDimension directly.
  369. if (stacked) {
  370. // Only ordinal axis can be stacked.
  371. if (!lastStackCoords[stackId][baseValue]) {
  372. lastStackCoords[stackId][baseValue] = {
  373. p: valueAxisStart,
  374. // Positive stack
  375. n: valueAxisStart // Negative stack
  376. };
  377. } // Should also consider #4243
  378. baseCoord = lastStackCoords[stackId][baseValue][sign];
  379. }
  380. var x;
  381. var y;
  382. var width;
  383. var height;
  384. if (isValueAxisH) {
  385. var coord = cartesian.dataToPoint([value, baseValue]);
  386. x = baseCoord;
  387. y = coord[1] + columnOffset;
  388. width = coord[0] - valueAxisStart;
  389. height = columnWidth;
  390. if (Math.abs(width) < barMinHeight) {
  391. width = (width < 0 ? -1 : 1) * barMinHeight;
  392. } // Ignore stack from NaN value
  393. if (!isNaN(width)) {
  394. stacked && (lastStackCoords[stackId][baseValue][sign] += width);
  395. }
  396. } else {
  397. var coord = cartesian.dataToPoint([baseValue, value]);
  398. x = coord[0] + columnOffset;
  399. y = baseCoord;
  400. width = columnWidth;
  401. height = coord[1] - valueAxisStart;
  402. if (Math.abs(height) < barMinHeight) {
  403. // Include zero to has a positive bar
  404. height = (height <= 0 ? -1 : 1) * barMinHeight;
  405. } // Ignore stack from NaN value
  406. if (!isNaN(height)) {
  407. stacked && (lastStackCoords[stackId][baseValue][sign] += height);
  408. }
  409. }
  410. data.setItemLayout(idx, {
  411. x: x,
  412. y: y,
  413. width: width,
  414. height: height
  415. });
  416. }
  417. }, this);
  418. } // TODO: Do not support stack in large mode yet.
  419. var largeLayout = {
  420. seriesType: 'bar',
  421. plan: createRenderPlanner(),
  422. reset: function (seriesModel) {
  423. if (!isOnCartesian(seriesModel) || !isInLargeMode(seriesModel)) {
  424. return;
  425. }
  426. var data = seriesModel.getData();
  427. var cartesian = seriesModel.coordinateSystem;
  428. var coordLayout = cartesian.grid.getRect();
  429. var baseAxis = cartesian.getBaseAxis();
  430. var valueAxis = cartesian.getOtherAxis(baseAxis);
  431. var valueDim = data.mapDimension(valueAxis.dim);
  432. var baseDim = data.mapDimension(baseAxis.dim);
  433. var valueAxisHorizontal = valueAxis.isHorizontal();
  434. var valueDimIdx = valueAxisHorizontal ? 0 : 1;
  435. var barWidth = retrieveColumnLayout(makeColumnLayout([seriesModel]), baseAxis, seriesModel).width;
  436. if (!(barWidth > LARGE_BAR_MIN_WIDTH)) {
  437. // jshint ignore:line
  438. barWidth = LARGE_BAR_MIN_WIDTH;
  439. }
  440. return {
  441. progress: progress
  442. };
  443. function progress(params, data) {
  444. var count = params.count;
  445. var largePoints = new LargeArr(count * 2);
  446. var largeBackgroundPoints = new LargeArr(count * 2);
  447. var largeDataIndices = new LargeArr(count);
  448. var dataIndex;
  449. var coord = [];
  450. var valuePair = [];
  451. var pointsOffset = 0;
  452. var idxOffset = 0;
  453. while ((dataIndex = params.next()) != null) {
  454. valuePair[valueDimIdx] = data.get(valueDim, dataIndex);
  455. valuePair[1 - valueDimIdx] = data.get(baseDim, dataIndex);
  456. coord = cartesian.dataToPoint(valuePair, null, coord); // Data index might not be in order, depends on `progressiveChunkMode`.
  457. largeBackgroundPoints[pointsOffset] = valueAxisHorizontal ? coordLayout.x + coordLayout.width : coord[0];
  458. largePoints[pointsOffset++] = coord[0];
  459. largeBackgroundPoints[pointsOffset] = valueAxisHorizontal ? coord[1] : coordLayout.y + coordLayout.height;
  460. largePoints[pointsOffset++] = coord[1];
  461. largeDataIndices[idxOffset++] = dataIndex;
  462. }
  463. data.setLayout({
  464. largePoints: largePoints,
  465. largeDataIndices: largeDataIndices,
  466. largeBackgroundPoints: largeBackgroundPoints,
  467. barWidth: barWidth,
  468. valueAxisStart: getValueAxisStart(baseAxis, valueAxis, false),
  469. backgroundStart: valueAxisHorizontal ? coordLayout.x : coordLayout.y,
  470. valueAxisHorizontal: valueAxisHorizontal
  471. });
  472. }
  473. }
  474. };
  475. function isOnCartesian(seriesModel) {
  476. return seriesModel.coordinateSystem && seriesModel.coordinateSystem.type === 'cartesian2d';
  477. }
  478. function isInLargeMode(seriesModel) {
  479. return seriesModel.pipelineContext && seriesModel.pipelineContext.large;
  480. } // See cases in `test/bar-start.html` and `#7412`, `#8747`.
  481. function getValueAxisStart(baseAxis, valueAxis, stacked) {
  482. return valueAxis.toGlobalCoord(valueAxis.dataToCoord(valueAxis.type === 'log' ? 1 : 0));
  483. }
  484. exports.getLayoutOnAxis = getLayoutOnAxis;
  485. exports.prepareLayoutBarSeries = prepareLayoutBarSeries;
  486. exports.makeColumnLayout = makeColumnLayout;
  487. exports.retrieveColumnLayout = retrieveColumnLayout;
  488. exports.layout = layout;
  489. exports.largeLayout = largeLayout;