]> git.proxmox.com Git - extjs.git/blame - extjs/packages/charts/src/chart/series/sprite/Line.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / charts / src / chart / series / sprite / Line.js
CommitLineData
6527f429
DM
1/**\r
2 * @class Ext.chart.series.sprite.Line\r
3 * @extends Ext.chart.series.sprite.Aggregative\r
4 *\r
5 * Line series sprite.\r
6 */\r
7Ext.define('Ext.chart.series.sprite.Line', {\r
8 alias: 'sprite.lineSeries',\r
9 extend: 'Ext.chart.series.sprite.Aggregative',\r
10\r
11 inheritableStatics: {\r
12 def: {\r
13 processors: {\r
14 /**\r
15 * @cfg {Boolean} [smooth=false]\r
16 * `true` if the sprite uses line smoothing.\r
17 * Don't enable this if your data has gaps: NaN, undefined, etc.\r
18 */\r
19 smooth: 'bool',\r
20 /**\r
21 * @cfg {Boolean} [fillArea=false]\r
22 * `true` if the sprite paints the area underneath the line.\r
23 */\r
24 fillArea: 'bool',\r
25 /**\r
26 * @cfg {Boolean} [step=false]\r
27 * `true` if the line uses steps instead of straight lines to connect the dots.\r
28 * It is ignored if `smooth` is `true`.\r
29 */\r
30 step: 'bool',\r
31 /**\r
32 * @cfg {Boolean} [preciseStroke=true]\r
33 * `true` if the line uses precise stroke.\r
34 */\r
35 preciseStroke: 'bool',\r
36 /**\r
37 * @private\r
38 * The x-axis associated with the Line series.\r
39 * We need to know the position of the x-axis to fill the area underneath\r
40 * the stroke properly.\r
41 */\r
42 xAxis: 'default',\r
43 /**\r
44 * @cfg {Number} [yCap=Math.pow(2, 20)]\r
45 * Absolute maximum y-value.\r
46 * Larger values will be capped to avoid rendering issues.\r
47 */\r
48 yCap: 'default' // The 'default' processor is used here as we don't want this attribute to animate.\r
49 },\r
50\r
51 defaults: {\r
52 smooth: false,\r
53 fillArea: false,\r
54 step: false,\r
55 preciseStroke: true,\r
56 xAxis: null,\r
57 yCap: Math.pow(2, 20),\r
58 yJump: 50\r
59 },\r
60\r
61 triggers: {\r
62 dataX: 'dataX,bbox,smooth',\r
63 dataY: 'dataY,bbox,smooth',\r
64 smooth: 'smooth'\r
65 },\r
66\r
67 updaters: {\r
68 smooth: function (attr) {\r
69 var dataX = attr.dataX,\r
70 dataY = attr.dataY;\r
71 if (attr.smooth && dataX && dataY && dataX.length > 2 && dataY.length > 2) {\r
72 this.smoothX = Ext.draw.Draw.spline(dataX);\r
73 this.smoothY = Ext.draw.Draw.spline(dataY);\r
74 } else {\r
75 delete this.smoothX;\r
76 delete this.smoothY;\r
77 }\r
78 }\r
79 }\r
80 }\r
81 },\r
82\r
83 list: null,\r
84\r
85 updatePlainBBox: function (plain) {\r
86 var attr = this.attr,\r
87 ymin = Math.min(0, attr.dataMinY),\r
88 ymax = Math.max(0, attr.dataMaxY);\r
89 plain.x = attr.dataMinX;\r
90 plain.y = ymin;\r
91 plain.width = attr.dataMaxX - attr.dataMinX;\r
92 plain.height = ymax - ymin;\r
93 },\r
94\r
95 drawStrip: function (ctx, strip) {\r
96 ctx.moveTo(strip[0], strip[1]);\r
97 for (var i = 2, ln = strip.length; i < ln; i += 2) {\r
98 ctx.lineTo(strip[i], strip[i + 1]);\r
99 }\r
100 },\r
101\r
102 drawStraightStroke: function (surface, ctx, start, end, list, xAxis) {\r
103 var me = this,\r
104 attr = me.attr,\r
105 renderer = attr.renderer,\r
106 step = attr.step,\r
107 needMoveTo = true,\r
108 lineConfig = {\r
109 type: 'line',\r
110 smooth: false,\r
111 step: step\r
112 },\r
113 strip = [], // Stores last continuous segment of the stroke.\r
114 lineConfig, changes, params, stripStartX,\r
115 x, y, x0, y0, x1, y1, i;\r
116\r
117 for (i = 3; i < list.length; i += 3) {\r
118 x0 = list[i - 3];\r
119 y0 = list[i - 2];\r
120 x = list[i];\r
121 y = list[i + 1];\r
122 x1 = list[i + 3];\r
123 y1 = list[i + 4];\r
124\r
125 if (renderer) {\r
126 lineConfig.x = x;\r
127 lineConfig.y = y;\r
128 lineConfig.x0 = x0;\r
129 lineConfig.y0 = y0;\r
130 params = [me, lineConfig, me.rendererData, start + i/3];\r
131 changes = Ext.callback(renderer, null, params, 0, me.getSeries());\r
132 }\r
133\r
134 if (Ext.isNumber(x + y + x0 + y0)) {\r
135 if (needMoveTo) {\r
136 ctx.beginPath();\r
137 ctx.moveTo(x0, y0);\r
138 strip.push(x0, y0);\r
139 stripStartX = x0;\r
140 needMoveTo = false;\r
141 }\r
142 } else {\r
143 continue;\r
144 }\r
145\r
146 if (step) {\r
147 ctx.lineTo(x, y0);\r
148 strip.push(x, y0);\r
149 }\r
150 ctx.lineTo(x, y);\r
151 strip.push(x, y);\r
152\r
153 if ( changes || !(Ext.isNumber(x1 + y1)) ) {\r
154 ctx.save();\r
155 Ext.apply(ctx, changes);\r
156\r
157 if (attr.fillArea) {\r
158 ctx.lineTo(x, xAxis);\r
159 ctx.lineTo(stripStartX, xAxis);\r
160 ctx.closePath();\r
161 ctx.fill();\r
162 }\r
163\r
164 // Draw the line on top of the filled area.\r
165 ctx.beginPath();\r
166 me.drawStrip(ctx, strip);\r
167 strip = [];\r
168 ctx.stroke();\r
169 ctx.restore();\r
170\r
171 ctx.beginPath();\r
172 needMoveTo = true;\r
173 }\r
174 }\r
175 },\r
176\r
177 calculateScale: function (count, end) {\r
178 var power = 0,\r
179 n = count;\r
180 while (n < end && count > 0) {\r
181 power++;\r
182 n += count >> power;\r
183 }\r
184 return Math.pow(2, power > 0 ? power - 1 : power);\r
185 },\r
186\r
187 drawSmoothStroke: function (surface, ctx, start, end, list, xAxis) {\r
188 var me = this,\r
189 attr = me.attr,\r
190 step = attr.step,\r
191 matrix = attr.matrix,\r
192 renderer = attr.renderer,\r
193 xx = matrix.getXX(),\r
194 yy = matrix.getYY(),\r
195 dx = matrix.getDX(),\r
196 dy = matrix.getDY(),\r
197 smoothX = me.smoothX,\r
198 smoothY = me.smoothY,\r
199 scale = me.calculateScale(attr.dataX.length, end),\r
200 cx1, cy1, cx2, cy2, x, y, x0, y0,\r
201 i, j, changes, params,\r
202 lineConfig = {\r
203 type: 'line',\r
204 smooth: true,\r
205 step: step\r
206 };\r
207\r
208 ctx.beginPath();\r
209 ctx.moveTo(smoothX[start * 3] * xx + dx, smoothY[start * 3] * yy + dy);\r
210 for (i = 0, j = start * 3 + 1; i < list.length - 3; i += 3, j += 3 * scale) {\r
211 cx1 = smoothX[j] * xx + dx;\r
212 cy1 = smoothY[j] * yy + dy;\r
213 cx2 = smoothX[j + 1] * xx + dx;\r
214 cy2 = smoothY[j + 1] * yy + dy;\r
215 x = surface.roundPixel(list[i + 3]);\r
216 y = list[i + 4];\r
217 x0 = surface.roundPixel(list[i]);\r
218 y0 = list[i + 1];\r
219\r
220 if (renderer) {\r
221 lineConfig.x0 = x0;\r
222 lineConfig.y0 = y0;\r
223 lineConfig.cx1 = cx1;\r
224 lineConfig.cy1 = cy1;\r
225 lineConfig.cx2 = cx2;\r
226 lineConfig.cy2 = cy2;\r
227 lineConfig.x = x;\r
228 lineConfig.y = y;\r
229 params = [me, lineConfig, me.rendererData, start + i/3 + 1];\r
230 changes = Ext.callback(renderer, null, params, 0, me.getSeries());\r
231 ctx.save();\r
232 Ext.apply(ctx, changes);\r
233 }\r
234\r
235 if (attr.fillArea) {\r
236 ctx.moveTo(x0, y0);\r
237 ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);\r
238 ctx.lineTo(x, xAxis);\r
239 ctx.lineTo(x0, xAxis);\r
240 ctx.lineTo(x0, y0);\r
241 ctx.closePath();\r
242 ctx.fill();\r
243 ctx.beginPath();\r
244 }\r
245 // Draw the line on top of the filled area.\r
246 ctx.moveTo(x0, y0);\r
247 ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);\r
248 ctx.stroke();\r
249 ctx.moveTo(x0, y0);\r
250 ctx.closePath();\r
251\r
252 if (renderer) {\r
253 ctx.restore();\r
254 }\r
255\r
256 ctx.beginPath();\r
257 ctx.moveTo(x, y);\r
258 }\r
259 // Prevent the last visible segment from being stroked twice\r
260 // (second time by the ctx.fillStroke inside Path sprite 'render' method)\r
261 ctx.beginPath();\r
262 },\r
263\r
264 drawLabel: function (text, dataX, dataY, labelId, rect) {\r
265 var me = this,\r
266 attr = me.attr,\r
267 label = me.getMarker('labels'),\r
268 labelTpl = label.getTemplate(),\r
269 labelCfg = me.labelCfg || (me.labelCfg = {}),\r
270 surfaceMatrix = me.surfaceMatrix,\r
271 labelX, labelY,\r
272 labelOverflowPadding = attr.labelOverflowPadding,\r
273 halfHeight, labelBBox,\r
274 changes, params, hasPendingChanges;\r
275\r
276 // The coordinates below (data point converted to surface coordinates)\r
277 // are just for the renderer to give it a notion of where the label will be positioned.\r
278 // The actual position of the label will be different\r
279 // (unless the renderer returns x/y coordinates in the changes object)\r
280 // and depend on several things including the size of the text,\r
281 // which has to be measured after the renderer call,\r
282 // since text can be modified by the renderer.\r
283 labelCfg.x = surfaceMatrix.x(dataX, dataY);\r
284 labelCfg.y = surfaceMatrix.y(dataX, dataY);\r
285\r
286 if (attr.flipXY) {\r
287 labelCfg.rotationRads = Math.PI * 0.5;\r
288 } else {\r
289 labelCfg.rotationRads = 0;\r
290 }\r
291\r
292 labelCfg.text = text;\r
293\r
294 if (labelTpl.attr.renderer) {\r
295 params = [text, label, labelCfg, me.rendererData, labelId];\r
296 changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());\r
297 if (typeof changes === 'string') {\r
298 labelCfg.text = changes;\r
299 } else if (typeof changes === 'object') {\r
300 if ('text' in changes) {\r
301 labelCfg.text = changes.text;\r
302 }\r
303 hasPendingChanges = true;\r
304 }\r
305 }\r
306\r
307 labelBBox = me.getMarkerBBox('labels', labelId, true);\r
308 if (!labelBBox) {\r
309 me.putMarker('labels', labelCfg, labelId);\r
310 labelBBox = me.getMarkerBBox('labels', labelId, true);\r
311 }\r
312\r
313 halfHeight = labelBBox.height / 2;\r
314 labelX = dataX;\r
315\r
316 switch (labelTpl.attr.display) {\r
317 case 'under':\r
318 labelY = dataY - halfHeight - labelOverflowPadding;\r
319 break;\r
320 case 'rotate':\r
321 labelX += labelOverflowPadding;\r
322 labelY = dataY - labelOverflowPadding;\r
323 labelCfg.rotationRads = -Math.PI / 4;\r
324 break;\r
325 default: // 'over'\r
326 labelY = dataY + halfHeight + labelOverflowPadding;\r
327 }\r
328\r
329 labelCfg.x = surfaceMatrix.x(labelX, labelY);\r
330 labelCfg.y = surfaceMatrix.y(labelX, labelY);\r
331\r
332 if (hasPendingChanges) {\r
333 Ext.apply(labelCfg, changes);\r
334 }\r
335\r
336 me.putMarker('labels', labelCfg, labelId);\r
337 },\r
338\r
339 drawMarker: function (x, y, index) {\r
340 var me = this,\r
341 attr = me.attr,\r
342 renderer = attr.renderer,\r
343 surfaceMatrix = me.surfaceMatrix,\r
344 markerCfg = {},\r
345 changes, params;\r
346\r
347 if (renderer && me.getMarker('markers')) {\r
348 markerCfg.type = 'marker';\r
349 markerCfg.x = x;\r
350 markerCfg.y = y;\r
351 params = [me, markerCfg, me.rendererData, index];\r
352 changes = Ext.callback(renderer, null, params, 0, me.getSeries());\r
353 if (changes) {\r
354 Ext.apply(markerCfg, changes);\r
355 }\r
356 }\r
357\r
358 markerCfg.translationX = surfaceMatrix.x(x, y);\r
359 markerCfg.translationY = surfaceMatrix.y(x, y);\r
360\r
361 delete markerCfg.x;\r
362 delete markerCfg.y;\r
363\r
364 me.putMarker('markers', markerCfg, index, !renderer);\r
365 },\r
366\r
367 drawStroke: function (surface, ctx, start, end, list, xAxis) {\r
368 var me = this,\r
369 isSmooth = me.attr.smooth && me.smoothX && me.smoothY;\r
370\r
371 if (isSmooth) {\r
372 me.drawSmoothStroke(surface, ctx, start, end, list, xAxis);\r
373 } else {\r
374 me.drawStraightStroke(surface, ctx, start, end, list, xAxis);\r
375 }\r
376 },\r
377\r
378 renderAggregates: function (aggregates, start, end, surface, ctx, clip, rect) {\r
379 var me = this,\r
380 attr = me.attr,\r
381 dataX = attr.dataX,\r
382 dataY = attr.dataY,\r
383 labels = attr.labels,\r
384 xAxis = attr.xAxis,\r
385 yCap = attr.yCap,\r
386 isSmooth = attr.smooth && me.smoothX && me.smoothY,\r
387 isDrawLabels = labels && me.getMarker('labels'),\r
388 isDrawMarkers = me.getMarker('markers'),\r
389 matrix = attr.matrix,\r
390 pixel = surface.devicePixelRatio,\r
391 xx = matrix.getXX(),\r
392 yy = matrix.getYY(),\r
393 dx = matrix.getDX(),\r
394 dy = matrix.getDY(),\r
395 list = me.list || (me.list = []),\r
396 minXs = aggregates.minX,\r
397 maxXs = aggregates.maxX,\r
398 minYs = aggregates.minY,\r
399 maxYs = aggregates.maxY,\r
400 idx = aggregates.startIdx,\r
401 isContinuousLine = true,\r
402 xAxisOrigin, isVerticalX,\r
403 x, y, i, index;\r
404\r
405 me.rendererData = {store: me.getStore()};\r
406 list.length = 0;\r
407\r
408 // Say we have 7 y-items (attr.dataY): [20, 19, 17, 15, 11, 10, 14]\r
409 // and 7 x-items (attr.dataX): [0, 1, 2, 3, 4, 5, 6].\r
410 // Then aggregates.startIdx is an aggregated index,\r
411 // where every other item is skipped on each aggregation level:\r
412 // [0, 1, 2, 3, 4, 5, 6,\r
413 // 0, 2, 4, 6,\r
414 // 0, 4,\r
415 // 0]\r
416 // aggregates.minY\r
417 // [20, 19, 17, 15, 11, 10, 14,\r
418 // 19, 15, 10, 14,\r
419 // 15, 10,\r
420 // 10]\r
421 // aggregates.maxY\r
422 // [20, 19, 17, 15, 11, 10, 14,\r
423 // 20, 17, 11, 14,\r
424 // 20, 14,\r
425 // 20]\r
426 // aggregates.minX is\r
427 // [0, 1, 2, 3, 4, 5, 6,\r
428 // 1, 3, 5, 6, // TODO: why this order for min?\r
429 // 3, 5, // TODO: why this inconsistency?\r
430 // 5]\r
431 // aggregates.maxX is\r
432 // [0, 1, 2, 3, 4, 5, 6,\r
433 // 0, 2, 4, 6,\r
434 // 0, 6,\r
435 // 0]\r
436\r
437 // Create a list of the form [x0, y0, idx0, x1, y1, idx1, ...],\r
438 // where each x,y pair is a coordinate representing original data point\r
439 // at the idx position.\r
440 for (i = start; i < end; i++) {\r
441 var minX = minXs[i],\r
442 maxX = maxXs[i],\r
443 minY = minYs[i],\r
444 maxY = maxYs[i];\r
445\r
446 if (minX < maxX) {\r
447 list.push(minX * xx + dx, minY * yy + dy, idx[i]);\r
448 list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);\r
449 } else if (minX > maxX) {\r
450 list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);\r
451 list.push(minX * xx + dx, minY * yy + dy, idx[i]);\r
452 } else {\r
453 list.push(maxX * xx + dx, maxY * yy + dy, idx[i]);\r
454 }\r
455 }\r
456\r
457 if (list.length) {\r
458 for (i = 0; i < list.length; i += 3) {\r
459 x = list[i];\r
460 y = list[i + 1];\r
461 if (Ext.isNumber(x + y)) {\r
462 if (y > yCap) {\r
463 y = yCap;\r
464 } else if (y < -yCap) {\r
465 y = -yCap;\r
466 }\r
467 list[i + 1] = y;\r
468 } else {\r
469 isContinuousLine = false;\r
470 continue;\r
471 }\r
472 index = list[i + 2];\r
473 if (isDrawMarkers) {\r
474 me.drawMarker(x, y, index);\r
475 }\r
476 if (isDrawLabels && labels[index]) {\r
477 me.drawLabel(labels[index], x, y, index, rect);\r
478 }\r
479 }\r
480\r
481 me.isContinuousLine = isContinuousLine;\r
482 if (isSmooth && !isContinuousLine) {\r
483 Ext.raise("Line smoothing in only supported for gapless data, " +\r
484 "where all data points are finite numbers.");\r
485 }\r
486\r
487 if (xAxis) {\r
488 isVerticalX = xAxis.getAlignment() === 'vertical';\r
489 if (Ext.isNumber(xAxis.floatingAtCoord)) {\r
490 xAxisOrigin = (isVerticalX ? rect[2] : rect[3]) - xAxis.floatingAtCoord;\r
491 } else {\r
492 xAxisOrigin = isVerticalX ? rect[0] : rect[1];\r
493 }\r
494 } else {\r
495 xAxisOrigin = attr.flipXY ? rect[0] : rect[1];\r
496 }\r
497\r
498 if (attr.preciseStroke) {\r
499 if (attr.fillArea) {\r
500 ctx.fill();\r
501 }\r
502 if (attr.transformFillStroke) {\r
503 attr.inverseMatrix.toContext(ctx);\r
504 }\r
505 me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);\r
506 if (attr.transformFillStroke) {\r
507 attr.matrix.toContext(ctx);\r
508 }\r
509 ctx.stroke();\r
510 } else {\r
511 me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);\r
512\r
513 if (isContinuousLine && isSmooth && attr.fillArea && !attr.renderer) {\r
514 var lastPointX = dataX[dataX.length - 1] * xx + dx + pixel,\r
515 lastPointY = dataY[dataY.length - 1] * yy + dy,\r
516 firstPointX = dataX[0] * xx + dx - pixel,\r
517 firstPointY = dataY[0] * yy + dy;\r
518 ctx.lineTo(lastPointX, lastPointY);\r
519 ctx.lineTo(lastPointX, xAxisOrigin - attr.lineWidth);\r
520 ctx.lineTo(firstPointX, xAxisOrigin - attr.lineWidth);\r
521 ctx.lineTo(firstPointX, firstPointY);\r
522 }\r
523\r
524 if (attr.transformFillStroke) {\r
525 attr.matrix.toContext(ctx);\r
526 }\r
527 // Prevent the reverse transform to fix floating point error.\r
528 if (attr.fillArea) {\r
529 ctx.fillStroke(attr, true);\r
530 } else {\r
531 ctx.stroke(true);\r
532 }\r
533 }\r
534 }\r
535 }\r
536});