LargeSymbolDraw.js 7.91 KB

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

var graphic = require("../../util/graphic");

var _symbol = require("../../util/symbol");

var createSymbol = _symbol.createSymbol;

var IncrementalDisplayable = require("zrender/lib/graphic/IncrementalDisplayable");

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/* global Float32Array */
// TODO Batch by color
var BOOST_SIZE_THRESHOLD = 4;
var LargeSymbolPath = graphic.extendShape({
  shape: {
    points: null
  },
  symbolProxy: null,
  buildPath: function (path, shape) {
    var points = shape.points;
    var size = shape.size;
    var symbolProxy = this.symbolProxy;
    var symbolProxyShape = symbolProxy.shape;
    var ctx = path.getContext ? path.getContext() : path;
    var canBoost = ctx && size[0] < BOOST_SIZE_THRESHOLD; // Do draw in afterBrush.

    if (canBoost) {
      return;
    }

    for (var i = 0; i < points.length;) {
      var x = points[i++];
      var y = points[i++];

      if (isNaN(x) || isNaN(y)) {
        continue;
      }

      symbolProxyShape.x = x - size[0] / 2;
      symbolProxyShape.y = y - size[1] / 2;
      symbolProxyShape.width = size[0];
      symbolProxyShape.height = size[1];
      symbolProxy.buildPath(path, symbolProxyShape, true);
    }
  },
  afterBrush: function (ctx) {
    var shape = this.shape;
    var points = shape.points;
    var size = shape.size;
    var canBoost = size[0] < BOOST_SIZE_THRESHOLD;

    if (!canBoost) {
      return;
    }

    this.setTransform(ctx); // PENDING If style or other canvas status changed?

    for (var i = 0; i < points.length;) {
      var x = points[i++];
      var y = points[i++];

      if (isNaN(x) || isNaN(y)) {
        continue;
      } // fillRect is faster than building a rect path and draw.
      // And it support light globalCompositeOperation.


      ctx.fillRect(x - size[0] / 2, y - size[1] / 2, size[0], size[1]);
    }

    this.restoreTransform(ctx);
  },
  findDataIndex: function (x, y) {
    // TODO ???
    // Consider transform
    var shape = this.shape;
    var points = shape.points;
    var size = shape.size;
    var w = Math.max(size[0], 4);
    var h = Math.max(size[1], 4); // Not consider transform
    // Treat each element as a rect
    // top down traverse

    for (var idx = points.length / 2 - 1; idx >= 0; idx--) {
      var i = idx * 2;
      var x0 = points[i] - w / 2;
      var y0 = points[i + 1] - h / 2;

      if (x >= x0 && y >= y0 && x <= x0 + w && y <= y0 + h) {
        return idx;
      }
    }

    return -1;
  }
});

function LargeSymbolDraw() {
  this.group = new graphic.Group();
}

var largeSymbolProto = LargeSymbolDraw.prototype;

largeSymbolProto.isPersistent = function () {
  return !this._incremental;
};
/**
 * Update symbols draw by new data
 * @param {module:echarts/data/List} data
 */


largeSymbolProto.updateData = function (data) {
  this.group.removeAll();
  var symbolEl = new LargeSymbolPath({
    rectHover: true,
    cursor: 'default'
  });
  symbolEl.setShape({
    points: data.getLayout('symbolPoints')
  });

  this._setCommon(symbolEl, data);

  this.group.add(symbolEl);
  this._incremental = null;
};

largeSymbolProto.updateLayout = function (data) {
  if (this._incremental) {
    return;
  }

  var points = data.getLayout('symbolPoints');
  this.group.eachChild(function (child) {
    if (child.startIndex != null) {
      var len = (child.endIndex - child.startIndex) * 2;
      var byteOffset = child.startIndex * 4 * 2;
      points = new Float32Array(points.buffer, byteOffset, len);
    }

    child.setShape('points', points);
  });
};

largeSymbolProto.incrementalPrepareUpdate = function (data) {
  this.group.removeAll();

  this._clearIncremental(); // Only use incremental displayables when data amount is larger than 2 million.
  // PENDING Incremental data?


  if (data.count() > 2e6) {
    if (!this._incremental) {
      this._incremental = new IncrementalDisplayable({
        silent: true
      });
    }

    this.group.add(this._incremental);
  } else {
    this._incremental = null;
  }
};

largeSymbolProto.incrementalUpdate = function (taskParams, data) {
  var symbolEl;

  if (this._incremental) {
    symbolEl = new LargeSymbolPath();

    this._incremental.addDisplayable(symbolEl, true);
  } else {
    symbolEl = new LargeSymbolPath({
      rectHover: true,
      cursor: 'default',
      startIndex: taskParams.start,
      endIndex: taskParams.end
    });
    symbolEl.incremental = true;
    this.group.add(symbolEl);
  }

  symbolEl.setShape({
    points: data.getLayout('symbolPoints')
  });

  this._setCommon(symbolEl, data, !!this._incremental);
};

largeSymbolProto._setCommon = function (symbolEl, data, isIncremental) {
  var hostModel = data.hostModel; // TODO
  // if (data.hasItemVisual.symbolSize) {
  //     // TODO typed array?
  //     symbolEl.setShape('sizes', data.mapArray(
  //         function (idx) {
  //             var size = data.getItemVisual(idx, 'symbolSize');
  //             return (size instanceof Array) ? size : [size, size];
  //         }
  //     ));
  // }
  // else {

  var size = data.getVisual('symbolSize');
  symbolEl.setShape('size', size instanceof Array ? size : [size, size]); // }
  // Create symbolProxy to build path for each data

  symbolEl.symbolProxy = createSymbol(data.getVisual('symbol'), 0, 0, 0, 0); // Use symbolProxy setColor method

  symbolEl.setColor = symbolEl.symbolProxy.setColor;
  var extrudeShadow = symbolEl.shape.size[0] < BOOST_SIZE_THRESHOLD;
  symbolEl.useStyle( // Draw shadow when doing fillRect is extremely slow.
  hostModel.getModel('itemStyle').getItemStyle(extrudeShadow ? ['color', 'shadowBlur', 'shadowColor'] : ['color']));
  var visualColor = data.getVisual('color');

  if (visualColor) {
    symbolEl.setColor(visualColor);
  }

  if (!isIncremental) {
    // Enable tooltip
    // PENDING May have performance issue when path is extremely large
    symbolEl.seriesIndex = hostModel.seriesIndex;
    symbolEl.on('mousemove', function (e) {
      symbolEl.dataIndex = null;
      var dataIndex = symbolEl.findDataIndex(e.offsetX, e.offsetY);

      if (dataIndex >= 0) {
        // Provide dataIndex for tooltip
        symbolEl.dataIndex = dataIndex + (symbolEl.startIndex || 0);
      }
    });
  }
};

largeSymbolProto.remove = function () {
  this._clearIncremental();

  this._incremental = null;
  this.group.removeAll();
};

largeSymbolProto._clearIncremental = function () {
  var incremental = this._incremental;

  if (incremental) {
    incremental.clearDisplaybles();
  }
};

var _default = LargeSymbolDraw;
module.exports = _default;