]>
Commit | Line | Data |
---|---|---|
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 | |
7 | Ext.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 | }); |