-
7 Jan 2012 9:49 PM #1
Consider supporting a 'stepline' chart - 9 lines code change on existing line chart
Consider supporting a 'stepline' chart - 9 lines code change on existing line chart
For discontinuous data, please consider supporting a step line chart. It is a very small increment over the line chart and useful in many situations. It only requires a few small changes in the Ext.chart.series.Line class 'drawSeries' method.
I chose to use the 'smooth' parameter to pass the configuration but it could just as easily be another configuration parameter
Jason
The changes are below:
Code:drawSeries: function() { var me = this, chart = me.chart, chartAxes = chart.axes, store = chart.getChartStore(), storeCount = store.getCount(), surface = me.chart.surface, bbox = {}, group = me.group, showMarkers = me.showMarkers, markerGroup = me.markerGroup, enableShadows = chart.shadow, shadowGroups = me.shadowGroups, shadowAttributes = me.shadowAttributes, smooth = me.smooth, stepped = (me.smooth == 'stepped'), lnsh = shadowGroups.length, dummyPath = ["M"], path = ["M"], renderPath = ["M"], smoothPath = ["M"], markerIndex = chart.markerIndex, axes = [].concat(me.axis), shadowBarAttr, xValues = [], xValueMap = {}, yValues = [], yValueMap = {}, onbreak = false, storeIndices = [], markerStyle = me.markerStyle, seriesStyle = me.seriesStyle, colorArrayStyle = me.colorArrayStyle, colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0, isNumber = Ext.isNumber, seriesIdx = me.seriesIdx, boundAxes = me.getAxesForXAndYFields(), boundXAxis = boundAxes.xAxis, boundYAxis = boundAxes.yAxis, shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes, x, y, prevX, prevY, firstX, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue, yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle, endLineStyle, type, count; if (me.fireEvent('beforedraw', me) === false) { return; } if (!storeCount || me.seriesIsHidden) { me.hide(); me.items = []; if (me.line) { me.line.hide(true); if (me.line.shadows) { shadows = me.line.shadows; for (j = 0, lnsh = shadows.length; j < lnsh; j++) { shadow = shadows[j]; shadow.hide(true); } } if (me.fillPath) { me.fillPath.hide(true); } } me.line = null; me.fillPath = null; return; } endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig, { fill: me.seriesStyle.fill || colorArrayStyle[seriesIdx % colorArrayStyle.length] }); type = endMarkerStyle.type; delete endMarkerStyle.type; endLineStyle = seriesStyle; if (!endLineStyle['stroke-width']) { endLineStyle['stroke-width'] = 0.5; } if (markerIndex && markerGroup && markerGroup.getCount()) { for (i = 0; i < markerIndex; i++) { marker = markerGroup.getAt(i); markerGroup.remove(marker); markerGroup.add(marker); markerAux = markerGroup.getAt(markerGroup.getCount() - 2); marker.setAttributes({ x: 0, y: 0, translate: { x: markerAux.attr.translation.x, y: markerAux.attr.translation.y } }, true); } } me.unHighlightItem(); me.cleanHighlights(); me.setBBox(); bbox = me.bbox; me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height]; for (i = 0, ln = axes.length; i < ln; i++) { axis = chartAxes.get(axes[i]); if (axis) { ends = axis.calcEnds(); if (axis.position == 'top' || axis.position == 'bottom') { minX = ends.from; maxX = ends.to; } else { minY = ends.from; maxY = ends.to; } } } if (me.xField && !isNumber(minX) && (boundXAxis == 'bottom' || boundXAxis == 'top') && !chartAxes.get(boundXAxis)) { axis = Ext.create('Ext.chart.axis.Axis', { chart: chart, fields: [].concat(me.xField) }).calcEnds(); minX = axis.from; maxX = axis.to; } if (me.yField && !isNumber(minY) && (boundYAxis == 'right' || boundYAxis == 'left') && !chartAxes.get(boundYAxis)) { axis = Ext.create('Ext.chart.axis.Axis', { chart: chart, fields: [].concat(me.yField) }).calcEnds(); minY = axis.from; maxY = axis.to; } if (isNaN(minX)) { minX = 0; xScale = bbox.width / ((storeCount - 1) || 1); } else { xScale = bbox.width / ((maxX - minX) || (storeCount -1) || 1); } if (isNaN(minY)) { minY = 0; yScale = bbox.height / ((storeCount - 1) || 1); } else { yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1); } me.eachRecord(function(record, i) { xValue = record.get(me.xField); if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue) || boundXAxis && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).type == 'Category') { if (xValue in xValueMap) { xValue = xValueMap[xValue]; } else { xValue = xValueMap[xValue] = i; } } yValue = record.get(me.yField); if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) { return; } if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue) || boundYAxis && chartAxes.get(boundYAxis) && chartAxes.get(boundYAxis).type == 'Category') { yValue = i; } storeIndices.push(i); xValues.push(xValue); yValues.push(yValue); }); ln = xValues.length; if (ln > bbox.width) { coords = me.shrink(xValues, yValues, bbox.width); xValues = coords.x; yValues = coords.y; } me.items = []; count = 0; ln = xValues.length; for (i = 0; i < ln; i++) { xValue = xValues[i]; yValue = yValues[i]; if (yValue === false) { if (path.length == 1) { path = []; } onbreak = true; me.items.push(false); continue; } else { x = (bbox.x + (xValue - minX) * xScale).toFixed(2); y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2); if ((stepped) && (prevX !== undefined)) { path = path.concat([x, prevY]); } if (onbreak) { onbreak = false; path.push('M'); } path = path.concat([x, y]); } if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) { firstY = y; firstX = x; } if (!me.line || chart.resizing) { dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]); } if (chart.animate && chart.resizing && me.line) { me.line.setAttributes({ path: dummyPath }, true); if (me.fillPath) { me.fillPath.setAttributes({ path: dummyPath, opacity: 0.2 }, true); } if (me.line.shadows) { shadows = me.line.shadows; for (j = 0, lnsh = shadows.length; j < lnsh; j++) { shadow = shadows[j]; shadow.setAttributes({ path: dummyPath }, true); } } } if (showMarkers) { marker = markerGroup.getAt(count++); if (!marker) { marker = Ext.chart.Shape[type](surface, Ext.apply({ group: [group, markerGroup], x: 0, y: 0, translate: { x: +(prevX || x), y: prevY || (bbox.y + bbox.height / 2) }, value: '"' + xValue + ', ' + yValue + '"', zIndex: 4000 }, endMarkerStyle)); marker._to = { translate: { x: +x, y: +y } }; } else { marker.setAttributes({ value: '"' + xValue + ', ' + yValue + '"', x: 0, y: 0, hidden: false }, true); marker._to = { translate: { x: +x, y: +y } }; } } me.items.push({ series: me, value: [xValue, yValue], point: [x, y], sprite: marker, storeItem: store.getAt(storeIndices[i]) }); prevX = x; prevY = y; } if (path.length <= 1) { return; } if ((me.smooth) && (!stepped)) { smoothPath = Ext.draw.Draw.smooth(path, isNumber(smooth) ? smooth : me.defaultSmoothness); } renderPath = (smooth && !stepped) ? smoothPath : path; if (chart.markerIndex && me.previousPath) { fromPath = me.previousPath; if (!smooth || stepped) { Ext.Array.erase(fromPath, 1, 2); } } else { fromPath = path; } if (!me.line) { me.line = surface.add(Ext.apply({ type: 'path', group: group, path: dummyPath, stroke: endLineStyle.stroke || endLineStyle.fill }, endLineStyle || {})); if (enableShadows) { me.line.setAttributes(Ext.apply({}, me.shadowOptions), true); } me.line.setAttributes({ fill: 'none', zIndex: 3000 }); if (!endLineStyle.stroke && colorArrayLength) { me.line.setAttributes({ stroke: colorArrayStyle[seriesIdx % colorArrayLength] }, true); } if (enableShadows) { shadows = me.line.shadows = []; for (shindex = 0; shindex < lnsh; shindex++) { shadowBarAttr = shadowAttributes[shindex]; shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath }); shadow = surface.add(Ext.apply({}, { type: 'path', group: shadowGroups[shindex] }, shadowBarAttr)); shadows.push(shadow); } } } if (me.fill) { fillPath = renderPath.concat([ ["L", x, bbox.y + bbox.height], ["L", firstX, bbox.y + bbox.height], ["L", firstX, firstY] ]); if (!me.fillPath) { me.fillPath = surface.add({ group: group, type: 'path', opacity: endLineStyle.opacity || 0.3, fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength], path: dummyPath }); } } markerCount = showMarkers && markerGroup.getCount(); if (chart.animate) { fill = me.fill; line = me.line; rendererAttributes = me.renderer(line, false, { path: renderPath }, i, store); Ext.apply(rendererAttributes, endLineStyle || {}, { stroke: endLineStyle.stroke || endLineStyle.fill }); delete rendererAttributes.fill; line.show(true); if (chart.markerIndex && me.previousPath) { me.animation = animation = me.onAnimate(line, { to: rendererAttributes, from: { path: fromPath } }); } else { me.animation = animation = me.onAnimate(line, { to: rendererAttributes }); } if (enableShadows) { shadows = line.shadows; for(j = 0; j < lnsh; j++) { shadows[j].show(true); if (chart.markerIndex && me.previousPath) { me.onAnimate(shadows[j], { to: { path: renderPath }, from: { path: fromPath } }); } else { me.onAnimate(shadows[j], { to: { path: renderPath } }); } } } if (fill) { me.fillPath.show(true); me.onAnimate(me.fillPath, { to: Ext.apply({}, { path: fillPath, fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength], 'stroke-width': 0 }, endLineStyle || {}) }); } if (showMarkers) { count = 0; for(i = 0; i < ln; i++) { if (me.items[i]) { item = markerGroup.getAt(count++); if (item) { rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); me.onAnimate(item, { to: Ext.apply(rendererAttributes, endMarkerStyle || {}) }); item.show(true); } } } for(; count < markerCount; count++) { item = markerGroup.getAt(count); item.hide(true); } } } else { rendererAttributes = me.renderer(me.line, false, { path: renderPath, hidden: false }, i, store); Ext.apply(rendererAttributes, endLineStyle || {}, { stroke: endLineStyle.stroke || endLineStyle.fill }); delete rendererAttributes.fill; me.line.setAttributes(rendererAttributes, true); if (enableShadows) { shadows = me.line.shadows; for(j = 0; j < lnsh; j++) { shadows[j].setAttributes({ path: renderPath, hidden: false }, true); } } if (me.fill) { me.fillPath.setAttributes({ path: fillPath, hidden: false }, true); } if (showMarkers) { count = 0; for(i = 0; i < ln; i++) { if (me.items[i]) { item = markerGroup.getAt(count++); if (item) { rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store); item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true); item.show(true); } } } for(; count < markerCount; count++) { item = markerGroup.getAt(count); item.hide(true); } } } if (chart.markerIndex) { if ((me.smooth) && (!stepped)) { Ext.Array.erase(path, 1, 2); } else { Ext.Array.splice(path, 1, 0, path[1], path[2]); } me.previousPath = path; } me.renderLabels(); me.renderCallouts(); me.fireEvent('draw', me); }
-
9 Jan 2012 11:05 AM #2Sencha - Senior Forum Manager
- Join Date
- Mar 2007
- Location
- St. Louis, MO
- Posts
- 34,107
- Vote Rating
- 453
Thank you for the report.
Mitchell Simoens @SenchaMitch
Sencha Inc, Senior Forum Manager
________________
http://www.JSONPLint.com - Source to lint your JSONP!
Check out my GitHub, lots of nice things for Ext JS 4 and Sencha Touch 2
https://github.com/mitchellsimoens
Think my support is good? Get more personalized support via a support subscription. https://www.sencha.com/store/
Need more help with your app? Hire Sencha Services services@sencha.com
Want to learn Sencha Touch 2? Check out Sencha Touch in Action that is almost in print!
When posting code, please use BBCode's CODE tags.
-
9 Jan 2012 5:12 PM #3
What is needed to get it into the product?
What is needed to get it into the product?
Hello,
Is there any way that I could help to get this into the product code? Let me know if I can be of assistance.
Jason
-
3 Jan 2013 7:56 AM #4
A "stepped" area chart also, please
A "stepped" area chart also, please
I would like to see a similar modification to the area chart. For example, I made the following changes to yield a "stepped" version of the area chart. I have likely missed something to make this a robust solution, but it appeared to work for my chart. Changes in red below.
Code:Ext.define('Ext.chart.series.Area', { /* Begin Definitions */ extend: 'Ext.chart.series.Cartesian', alias: 'series.area', requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'], /* End Definitions */ type: 'area', // @private Area charts are alyways stacked stacked: true, // set to 'stepped' for a stepped area chart smooth: false, ... // @private Build an array of paths for the chart getPaths: function() { var me = this, chart = me.chart, store = chart.getChartStore(), first = true, bounds = me.getBounds(), bbox = bounds.bbox, items = me.items = [], componentPaths = [], componentPath, count = 0, paths = [], i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path; //added to support steps var prevY = [], stepped = (me.smooth == 'stepped'); ln = bounds.xValues.length; // Start the path for (i = 0; i < ln; i++) { xValue = bounds.xValues[i]; yValue = bounds.yValues[i]; x = bbox.x + (xValue - bounds.minX) * bounds.xScale; acumY = 0; count = 0; for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { // Excluded series if (me.__excludes[areaIndex]) { continue; } if (!componentPaths[areaIndex]) { componentPaths[areaIndex] = []; } areaElem = yValue[count]; acumY += areaElem; y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale; if (!paths[areaIndex]) { paths[areaIndex] = ['M', x, y]; componentPaths[areaIndex].push(['L', x, y]); } else { // Steps if (stepped) { paths[areaIndex].push('L', x, prevY[count]); componentPaths[areaIndex].push(['L', x, prevY[count]]); } paths[areaIndex].push('L', x, y); componentPaths[areaIndex].push(['L', x, y]); } if (!items[areaIndex]) { items[areaIndex] = { pointsUp: [], pointsDown: [], series: me }; } //Steps if (stepped) { items[areaIndex].pointsUp.push([x, prevY[count]]); } items[areaIndex].pointsUp.push([x, y]); prevY[count] = y; count++; } } // Close the paths for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) { // Excluded series if (me.__excludes[areaIndex]) { continue; } path = paths[areaIndex]; // Close bottom path to the axis if (areaIndex == 0 || first) { first = false; path.push('L', x, bbox.y + bbox.height, 'L', bbox.x, bbox.y + bbox.height, 'Z'); } // Close other paths to the one before them else { componentPath = componentPaths[prevAreaIndex]; componentPath.reverse(); path.push('L', x, componentPath[0][2]); for (i = 0; i < ln; i++) { path.push(componentPath[i][0], componentPath[i][1], componentPath[i][2]); items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]]; } path.push('L', bbox.x, path[2], 'Z'); } prevAreaIndex = areaIndex; } return { paths: paths, areasLen: bounds.areasLen }; }
You found a bug! We've classified it as
EXTJSIV-5030
.
We encourage you to continue the discussion and to find an acceptable workaround while we work on a permanent fix.


Reply With Quote