mirror of
https://gitee.com/dotnetchina/OpenAuth.Net.git
synced 2026-03-25 10:43:26 +08:00
flow ui
This commit is contained in:
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* This is an experimental Highcharts module that draws long data series on a canvas
|
||||
* in order to increase performance of the initial load time and tooltip responsiveness.
|
||||
*
|
||||
* Compatible with HTML5 canvas compatible browsers (not IE < 9).
|
||||
*
|
||||
* Author: Torstein Honsi
|
||||
*
|
||||
*
|
||||
* Development plan
|
||||
* - Column range.
|
||||
* - Heatmap.
|
||||
* - Treemap.
|
||||
* - Check how it works with Highstock and data grouping.
|
||||
* - Check inverted charts.
|
||||
* - Check reversed axes.
|
||||
* - Chart callback should be async after last series is drawn. (But not necessarily, we don't do
|
||||
that with initial series animation).
|
||||
* - Cache full-size image so we don't have to redraw on hide/show and zoom up. But k-d-tree still
|
||||
* needs to be built.
|
||||
* - Test IE9 and IE10.
|
||||
* - Stacking is not perhaps not correct since it doesn't use the translation given in
|
||||
* the translate method. If this gets to complicated, a possible way out would be to
|
||||
* have a simplified renderCanvas method that simply draws the areaPath on a canvas.
|
||||
*
|
||||
* If this module is taken in as part of the core
|
||||
* - All the loading logic should be merged with core. Update styles in the core.
|
||||
* - Most of the method wraps should probably be added directly in parent methods.
|
||||
*
|
||||
* Notes for boost mode
|
||||
* - Area lines are not drawn
|
||||
* - Point markers are not drawn
|
||||
* - Zones and negativeColor don't work
|
||||
* - Columns are always one pixel wide. Don't set the threshold too low.
|
||||
*
|
||||
* Optimizing tips for users
|
||||
* - For scatter plots, use a marker.radius of 1 or less. It results in a rectangle being drawn, which is
|
||||
* considerably faster than a circle.
|
||||
* - Set extremes (min, max) explicitly on the axes in order for Highcharts to avoid computing extremes.
|
||||
* - Set enableMouseTracking to false on the series to improve total rendering time.
|
||||
* - The default threshold is set based on one series. If you have multiple, dense series, the combined
|
||||
* number of points drawn gets higher, and you may want to set the threshold lower in order to
|
||||
* use optimizations.
|
||||
*/
|
||||
/*global document, Highcharts, HighchartsAdapter, setTimeout */
|
||||
(function (H, HA) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var noop = function () { return undefined; },
|
||||
Color = H.Color,
|
||||
Series = H.Series,
|
||||
seriesTypes = H.seriesTypes,
|
||||
each = H.each,
|
||||
extend = H.extend,
|
||||
addEvent = HA.addEvent,
|
||||
fireEvent = HA.fireEvent,
|
||||
merge = H.merge,
|
||||
pick = H.pick,
|
||||
wrap = H.wrap,
|
||||
plotOptions = H.getOptions().plotOptions,
|
||||
CHUNK_SIZE = 50000;
|
||||
|
||||
function eachAsync(arr, fn, callback, chunkSize, i) {
|
||||
i = i || 0;
|
||||
chunkSize = chunkSize || CHUNK_SIZE;
|
||||
each(arr.slice(i, i + chunkSize), fn);
|
||||
if (i + chunkSize < arr.length) {
|
||||
setTimeout(function () {
|
||||
eachAsync(arr, fn, callback, chunkSize, i + chunkSize);
|
||||
});
|
||||
} else if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Set default options
|
||||
each(['area', 'arearange', 'column', 'line', 'scatter'], function (type) {
|
||||
if (plotOptions[type]) {
|
||||
plotOptions[type].boostThreshold = 5000;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Override a bunch of methods the same way. If the number of points is below the threshold,
|
||||
* run the original method. If not, check for a canvas version or do nothing.
|
||||
*/
|
||||
each(['translate', 'generatePoints', 'drawTracker', 'drawPoints', 'render'], function (method) {
|
||||
function branch(proceed) {
|
||||
var letItPass = this.options.stacking && (method === 'translate' || method === 'generatePoints');
|
||||
if ((this.processedXData || this.options.data).length < (this.options.boostThreshold || Number.MAX_VALUE) ||
|
||||
letItPass) {
|
||||
|
||||
// Clear image
|
||||
if (method === 'render' && this.image) {
|
||||
this.image.attr({ href: '' });
|
||||
this.animate = null; // We're zooming in, don't run animation
|
||||
}
|
||||
|
||||
proceed.call(this);
|
||||
|
||||
// If a canvas version of the method exists, like renderCanvas(), run
|
||||
} else if (this[method + 'Canvas']) {
|
||||
|
||||
this[method + 'Canvas']();
|
||||
}
|
||||
}
|
||||
wrap(Series.prototype, method, branch);
|
||||
|
||||
// A special case for some types - its translate method is already wrapped
|
||||
if (method === 'translate') {
|
||||
if (seriesTypes.column) {
|
||||
wrap(seriesTypes.column.prototype, method, branch);
|
||||
}
|
||||
if (seriesTypes.arearange) {
|
||||
wrap(seriesTypes.arearange.prototype, method, branch);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Do not compute extremes when min and max are set.
|
||||
* If we use this in the core, we can add the hook to hasExtremes to the methods directly.
|
||||
*/
|
||||
wrap(Series.prototype, 'getExtremes', function (proceed) {
|
||||
if (!this.hasExtremes()) {
|
||||
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
});
|
||||
wrap(Series.prototype, 'setData', function (proceed) {
|
||||
if (!this.hasExtremes(true)) {
|
||||
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
});
|
||||
wrap(Series.prototype, 'processData', function (proceed) {
|
||||
if (!this.hasExtremes(true)) {
|
||||
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
H.extend(Series.prototype, {
|
||||
pointRange: 0,
|
||||
|
||||
hasExtremes: function (checkX) {
|
||||
var options = this.options,
|
||||
data = options.data,
|
||||
xAxis = this.xAxis.options,
|
||||
yAxis = this.yAxis.options;
|
||||
return data.length > (options.boostThreshold || Number.MAX_VALUE) && typeof yAxis.min === 'number' && typeof yAxis.max === 'number' &&
|
||||
(!checkX || (typeof xAxis.min === 'number' && typeof xAxis.max === 'number'));
|
||||
},
|
||||
|
||||
/**
|
||||
* If implemented in the core, parts of this can probably be shared with other similar
|
||||
* methods in Highcharts.
|
||||
*/
|
||||
destroyGraphics: function () {
|
||||
var series = this,
|
||||
points = this.points,
|
||||
point,
|
||||
i;
|
||||
|
||||
for (i = 0; i < points.length; i = i + 1) {
|
||||
point = points[i];
|
||||
if (point && point.graphic) {
|
||||
point.graphic = point.graphic.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
each(['graph', 'area'], function (prop) {
|
||||
if (series[prop]) {
|
||||
series[prop] = series[prop].destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a hidden canvas to draw the graph on. The contents is later copied over
|
||||
* to an SVG image element.
|
||||
*/
|
||||
getContext: function () {
|
||||
var width = this.chart.plotWidth,
|
||||
height = this.chart.plotHeight;
|
||||
|
||||
if (!this.canvas) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.image = this.chart.renderer.image('', 0, 0, width, height).add(this.group);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
} else {
|
||||
this.ctx.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
this.canvas.setAttribute('width', width);
|
||||
this.canvas.setAttribute('height', height);
|
||||
this.image.attr({
|
||||
width: width,
|
||||
height: height
|
||||
});
|
||||
|
||||
return this.ctx;
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw the canvas image inside an SVG image
|
||||
*/
|
||||
canvasToSVG: function () {
|
||||
this.image.attr({ href: this.canvas.toDataURL('image/png') });
|
||||
},
|
||||
|
||||
cvsLineTo: function (ctx, clientX, plotY) {
|
||||
ctx.lineTo(clientX, plotY);
|
||||
},
|
||||
|
||||
renderCanvas: function () {
|
||||
var series = this,
|
||||
options = series.options,
|
||||
chart = series.chart,
|
||||
xAxis = this.xAxis,
|
||||
yAxis = this.yAxis,
|
||||
ctx,
|
||||
i,
|
||||
c = 0,
|
||||
xData = series.processedXData,
|
||||
yData = series.processedYData,
|
||||
rawData = options.data,
|
||||
xExtremes = xAxis.getExtremes(),
|
||||
xMin = xExtremes.min,
|
||||
xMax = xExtremes.max,
|
||||
yExtremes = yAxis.getExtremes(),
|
||||
yMin = yExtremes.min,
|
||||
yMax = yExtremes.max,
|
||||
pointTaken = {},
|
||||
lastClientX,
|
||||
sampling = !!series.sampling,
|
||||
points,
|
||||
r = options.marker && options.marker.radius,
|
||||
cvsDrawPoint = this.cvsDrawPoint,
|
||||
cvsLineTo = options.lineWidth ? this.cvsLineTo : false,
|
||||
cvsMarker = r <= 1 ? this.cvsMarkerSquare : this.cvsMarkerCircle,
|
||||
enableMouseTracking = options.enableMouseTracking !== false,
|
||||
lastPoint,
|
||||
threshold = options.threshold,
|
||||
yBottom = yAxis.getThreshold(threshold),
|
||||
hasThreshold = typeof threshold === 'number',
|
||||
translatedThreshold = yBottom,
|
||||
doFill = this.fill,
|
||||
isRange = series.pointArrayMap && series.pointArrayMap.join(',') === 'low,high',
|
||||
isStacked = !!options.stacking,
|
||||
cropStart = series.cropStart || 0,
|
||||
loadingOptions = chart.options.loading,
|
||||
requireSorting = series.requireSorting,
|
||||
wasNull,
|
||||
connectNulls = options.connectNulls,
|
||||
useRaw = !xData,
|
||||
minVal,
|
||||
maxVal,
|
||||
minI,
|
||||
maxI,
|
||||
fillColor = series.fillOpacity ?
|
||||
new Color(series.color).setOpacity(pick(options.fillOpacity, 0.75)).get() :
|
||||
series.color,
|
||||
stroke = function () {
|
||||
if (doFill) {
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fill();
|
||||
} else {
|
||||
ctx.strokeStyle = series.color;
|
||||
ctx.lineWidth = options.lineWidth;
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
drawPoint = function (clientX, plotY, yBottom) {
|
||||
if (c === 0) {
|
||||
ctx.beginPath();
|
||||
}
|
||||
|
||||
if (wasNull) {
|
||||
ctx.moveTo(clientX, plotY);
|
||||
} else {
|
||||
if (cvsDrawPoint) {
|
||||
cvsDrawPoint(ctx, clientX, plotY, yBottom, lastPoint);
|
||||
} else if (cvsLineTo) {
|
||||
cvsLineTo(ctx, clientX, plotY);
|
||||
} else if (cvsMarker) {
|
||||
cvsMarker(ctx, clientX, plotY, r);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to stroke the line for every 1000 pixels. It will crash the browser
|
||||
// memory use if we stroke too infrequently.
|
||||
c = c + 1;
|
||||
if (c === 1000) {
|
||||
stroke();
|
||||
c = 0;
|
||||
}
|
||||
|
||||
// Area charts need to keep track of the last point
|
||||
lastPoint = {
|
||||
clientX: clientX,
|
||||
plotY: plotY,
|
||||
yBottom: yBottom
|
||||
};
|
||||
},
|
||||
|
||||
addKDPoint = function (clientX, plotY, i) {
|
||||
|
||||
// The k-d tree requires series points. Reduce the amount of points, since the time to build the
|
||||
// tree increases exponentially.
|
||||
if (enableMouseTracking && !pointTaken[clientX + ',' + plotY]) {
|
||||
points.push({
|
||||
clientX: clientX,
|
||||
plotX: clientX,
|
||||
plotY: plotY,
|
||||
i: cropStart + i
|
||||
});
|
||||
pointTaken[clientX + ',' + plotY] = true;
|
||||
}
|
||||
};
|
||||
|
||||
// If we are zooming out from SVG mode, destroy the graphics
|
||||
if (this.points) {
|
||||
this.destroyGraphics();
|
||||
}
|
||||
|
||||
// The group
|
||||
series.plotGroup(
|
||||
'group',
|
||||
'series',
|
||||
series.visible ? 'visible' : 'hidden',
|
||||
options.zIndex,
|
||||
chart.seriesGroup
|
||||
);
|
||||
|
||||
series.getAttribs();
|
||||
series.markerGroup = series.group;
|
||||
addEvent(series, 'destroy', function () {
|
||||
series.markerGroup = null;
|
||||
});
|
||||
|
||||
points = this.points = [];
|
||||
ctx = this.getContext();
|
||||
series.buildKDTree = noop; // Do not start building while drawing
|
||||
|
||||
// Display a loading indicator
|
||||
if (rawData.length > 99999) {
|
||||
chart.options.loading = merge(loadingOptions, {
|
||||
labelStyle: {
|
||||
backgroundColor: 'rgba(255,255,255,0.75)',
|
||||
padding: '1em',
|
||||
borderRadius: '0.5em'
|
||||
},
|
||||
style: {
|
||||
backgroundColor: 'none',
|
||||
opacity: 1
|
||||
}
|
||||
});
|
||||
chart.showLoading('Drawing...');
|
||||
chart.options.loading = loadingOptions; // reset
|
||||
if (chart.loadingShown === true) {
|
||||
chart.loadingShown = 1;
|
||||
} else {
|
||||
chart.loadingShown = chart.loadingShown + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over the points
|
||||
i = 0;
|
||||
eachAsync(isStacked ? series.data : (xData || rawData), function (d) {
|
||||
|
||||
var x,
|
||||
y,
|
||||
clientX,
|
||||
plotY,
|
||||
isNull,
|
||||
low,
|
||||
isYInside = true;
|
||||
|
||||
if (useRaw) {
|
||||
x = d[0];
|
||||
y = d[1];
|
||||
} else {
|
||||
x = d;
|
||||
y = yData[i];
|
||||
}
|
||||
|
||||
// Resolve low and high for range series
|
||||
if (isRange) {
|
||||
if (useRaw) {
|
||||
y = d.slice(1, 3);
|
||||
}
|
||||
low = y[0];
|
||||
y = y[1];
|
||||
} else if (isStacked) {
|
||||
x = d.x;
|
||||
y = d.stackY;
|
||||
low = y - d.y;
|
||||
}
|
||||
|
||||
isNull = y === null;
|
||||
|
||||
// Optimize for scatter zooming
|
||||
if (!requireSorting) {
|
||||
isYInside = y >= yMin && y <= yMax;
|
||||
}
|
||||
|
||||
if (!isNull && x >= xMin && x <= xMax && isYInside) {
|
||||
|
||||
clientX = Math.round(xAxis.toPixels(x, true));
|
||||
|
||||
if (sampling) {
|
||||
if (minI === undefined || clientX === lastClientX) {
|
||||
if (!isRange) {
|
||||
low = y;
|
||||
}
|
||||
if (maxI === undefined || y > maxVal) {
|
||||
maxVal = y;
|
||||
maxI = i;
|
||||
}
|
||||
if (minI === undefined || low < minVal) {
|
||||
minVal = low;
|
||||
minI = i;
|
||||
}
|
||||
|
||||
}
|
||||
if (clientX !== lastClientX) { // Add points and reset
|
||||
if (minI !== undefined) { // then maxI is also a number
|
||||
plotY = yAxis.toPixels(maxVal, true);
|
||||
yBottom = yAxis.toPixels(minVal, true);
|
||||
drawPoint(
|
||||
clientX,
|
||||
hasThreshold ? Math.min(plotY, translatedThreshold) : plotY,
|
||||
hasThreshold ? Math.max(yBottom, translatedThreshold) : yBottom
|
||||
);
|
||||
addKDPoint(clientX, plotY, maxI);
|
||||
if (yBottom !== plotY) {
|
||||
addKDPoint(clientX, yBottom, minI);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
minI = maxI = undefined;
|
||||
lastClientX = clientX;
|
||||
}
|
||||
} else {
|
||||
plotY = Math.round(yAxis.toPixels(y, true));
|
||||
drawPoint(clientX, plotY, yBottom);
|
||||
addKDPoint(clientX, plotY, i);
|
||||
}
|
||||
}
|
||||
wasNull = isNull && !connectNulls;
|
||||
|
||||
i = i + 1;
|
||||
|
||||
if (i % CHUNK_SIZE === 0) {
|
||||
series.canvasToSVG();
|
||||
}
|
||||
|
||||
}, function () {
|
||||
|
||||
var loadingDiv = chart.loadingDiv,
|
||||
loadingShown = +chart.loadingShown;
|
||||
|
||||
stroke();
|
||||
series.canvasToSVG();
|
||||
|
||||
fireEvent(series, 'renderedCanvas');
|
||||
|
||||
// Do not use chart.hideLoading, as it runs JS animation and will be blocked by buildKDTree.
|
||||
// CSS animation looks good, but then it must be deleted in timeout. If we add the module to core,
|
||||
// change hideLoading so we can skip this block.
|
||||
if (loadingShown === 1) {
|
||||
extend(loadingDiv.style, {
|
||||
transition: 'opacity 250ms',
|
||||
opacity: 0
|
||||
});
|
||||
|
||||
chart.loadingShown = false;
|
||||
setTimeout(function () {
|
||||
if (loadingDiv.parentNode) { // In exporting it is falsy
|
||||
loadingDiv.parentNode.removeChild(loadingDiv);
|
||||
}
|
||||
chart.loadingDiv = chart.loadingSpan = null;
|
||||
}, 250);
|
||||
}
|
||||
if (loadingShown) {
|
||||
chart.loadingShown = loadingShown - 1;
|
||||
}
|
||||
|
||||
// Pass tests in Pointer.
|
||||
// TODO: Replace this with a single property, and replace when zooming in
|
||||
// below boostThreshold.
|
||||
series.directTouch = false;
|
||||
series.options.stickyTracking = true;
|
||||
|
||||
delete series.buildKDTree; // Go back to prototype, ready to build
|
||||
series.buildKDTree();
|
||||
|
||||
// Don't do async on export, the exportChart, getSVGForExport and getSVG methods are not chained for it.
|
||||
}, chart.renderer.forExport ? Number.MAX_VALUE : undefined);
|
||||
}
|
||||
});
|
||||
|
||||
seriesTypes.scatter.prototype.cvsMarkerCircle = function (ctx, clientX, plotY, r) {
|
||||
ctx.moveTo(clientX, plotY);
|
||||
ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false);
|
||||
};
|
||||
|
||||
// Rect is twice as fast as arc, should be used for small markers
|
||||
seriesTypes.scatter.prototype.cvsMarkerSquare = function (ctx, clientX, plotY, r) {
|
||||
ctx.moveTo(clientX, plotY);
|
||||
ctx.rect(clientX - r, plotY - r, r * 2, r * 2);
|
||||
};
|
||||
seriesTypes.scatter.prototype.fill = true;
|
||||
|
||||
extend(seriesTypes.area.prototype, {
|
||||
cvsDrawPoint: function (ctx, clientX, plotY, yBottom, lastPoint) {
|
||||
if (lastPoint && clientX !== lastPoint.clientX) {
|
||||
ctx.moveTo(lastPoint.clientX, lastPoint.yBottom);
|
||||
ctx.lineTo(lastPoint.clientX, lastPoint.plotY);
|
||||
ctx.lineTo(clientX, plotY);
|
||||
ctx.lineTo(clientX, yBottom);
|
||||
}
|
||||
},
|
||||
fill: true,
|
||||
fillOpacity: true,
|
||||
sampling: true
|
||||
});
|
||||
|
||||
extend(seriesTypes.column.prototype, {
|
||||
cvsDrawPoint: function (ctx, clientX, plotY, yBottom) {
|
||||
ctx.rect(clientX - 1, plotY, 1, yBottom - plotY);
|
||||
},
|
||||
fill: true,
|
||||
sampling: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Return a point instance from the k-d-tree
|
||||
*/
|
||||
wrap(Series.prototype, 'searchPoint', function (proceed, e) {
|
||||
var point = proceed.call(this, e),
|
||||
ret = point;
|
||||
|
||||
if (point && !(point instanceof this.pointClass)) {
|
||||
ret = (new this.pointClass()).init(this, this.options.data[point.i]);
|
||||
ret.dist = point.dist;
|
||||
ret.category = ret.x;
|
||||
ret.plotX = point.plotX;
|
||||
ret.plotY = point.plotY;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
}(Highcharts, HighchartsAdapter));
|
||||
Reference in New Issue
Block a user