Open woutergoossens opened 8 years ago
I would say yes, but it requires you to do some custom rendering.
e.g. everytime get the four bouding points and fill it. The code can help calculate the point coordinates, but you have to render it.
@danielgindi would you think it's a feature that will be used by many people?
@liuxuan30 How would you recommend getting the four bounding points and filling it? (What part of the API?) Like the above request, I would like to shade a section of the background (e.g., in a line chart that shows the years 1990-2015 in x-axis, I want to be able to background shade 1998-2002 and then 2011-13).
combine yMax, yMin, XMin, XMax, plus each data value, you will know the bounding points in data value coordinate space, and you can easily convert it to pixel coordinate space using chart transformer. ContentRect may also help, but it is in pixel space.
@liuxuan30 Are there any good examples you point out of using ChartTransformer
(ios-chart) or Transformer
(MPAndroidChart)? Especially for drawing on a section of a chart? Or is a matter of digging through the source? Thanks.
Almost every renderer will use chart transformer (aka trans
in the code) to convert data value into pixel value and vice versa. You can look at any renderer or just search 'let trans =' to find an example. It's just a bridge to connect data value space and pixel value space, then you can easily manipulate data value and later transform it into pixel value.
A good example is x axis render's drawGridLines
. If you know how to draw the gird lines, then you will easily know the four bounding points of the rect between two grid lines, and this rect is the rect you want to fill colors in the screenshot.
@woutergoossens Not sure if you implemented anything yet, but based on guidance of @liuxuan30, I came up with the following solution for ChartXAxisRenderer
and its subclasses - to add a renderGridAreas
method that is based on and called after renderGridLines
. Shown below is the implementation for ChartXAxisRendererBarChart
.
public override func renderGridAreas(context context: CGContext)
{
// New isDrawGridAreasEnabled property parallels isDrawGridLinesEnableld
// xAxis.filledAreas is an array of ChartXAxisAreaData instances, a new class
// which has startX and endY properties
if (!_xAxis.isDrawGridAreasEnabled || !_xAxis.isEnabled || _xAxis.filledAreas.count == 0)
{
return
}
let barData = _chart.data as! BarChartData
let step = barData.dataSetCount
CGContextSaveGState(context)
var position = CGPoint(x: 0.0, y: 0.0)
var endPosition = CGPoint(x: 0.0, y: 0.0)
let valueToPixelMatrix = transformer.valueToPixelMatrix
// xAxis.filledAreas is an array of a new class called ChartXAxisAreaData
// which has startX and endY properties
// Iterate through filled areas
let c = _xAxis.filledAreas.count
for (var i=0; i < c; i++) {
let areaData = _xAxis.filledAreas[i];
// Get start position, using the same logic as used in rendering gridlines
let sx = Int(areaData.startX)
position.x = CGFloat(sx * step) + CGFloat(sx) * barData.groupSpace - 0.5
position = CGPointApplyAffineTransform(position, valueToPixelMatrix)
// Get end position
let ex = Int(areaData.endX)
endPosition.x = CGFloat(ex * step) + CGFloat(ex) * barData.groupSpace - 0.5
endPosition = CGPointApplyAffineTransform(endPosition, valueToPixelMatrix)
// Draw rectangle - TODO externalize color
let rectangle = CGRect(x: position.x, y: viewPortHandler.contentTop, width: CGFloat(endPosition.x-position.x), height: viewPortHandler.contentBottom)
let blue = UIColor(red:215/255, green: 231/255, blue: 241/255, alpha: 0.5);
CGContextSetFillColorWithColor(context, blue.CGColor)
CGContextSetStrokeColorWithColor(context, blue.CGColor)
CGContextSetLineWidth(context, 1)
CGContextAddRect(context, rectangle)
CGContextDrawPath(context, .FillStroke)
}
CGContextRestoreGState(context)
}
I then added the following line to the drawRect
method of BarLineChartViewBase
, just after grid lines is rendered:
_xAxisRenderer?.renderGridAreas(context: context)
In my case, I need arbitrary grid areas that is based on the data. However, this could be modified to render a background for every other grid column.
@richwagner good job!
We may add this as a feature - I'm looking into it! Seems like something that could add style (or clarity!) to some charts.
It's nice that we already have some code/logic here :-)
I have just pushed a feature for filling line/radar charts with gradients or images, and I'm asking myself if we should be using the new ChartFill
object to draw the "grid areas".
The new ChartFill
object is something somewhat similar to Android's drawable, in the sense that it can represent an image, a color, a gradient, or a layer etc.
@PhilJay, @richwagner what do you think?
Yes, could definitely be a great new feature I would say.
We need to think this though and come up with a solution that is as easy as possible to "use".
Any propositions?
Well I guess we definitely need an extra object that stores the information. And then I guess to that object it should be possible to add some kind of region + color
@danielgindi I definitely would recommend adding this feature.
Concerning an extra object: In my renderGridAreas
implementation (above), I utilized a ChartXAxisAreaData
object for x-axis-based grid areas (with color defaults for my specific purpose):
public class ChartXAxisAreaData: NSObject {
public var startX = CGFloat(0)
public var endX = CGFloat(0)
public var color = UIColor(red:215/255, green: 231/255, blue: 241/255, alpha: 0.5)
}
I then add an area to a chart like:
ChartXAxisAreaData *areaData2 = [[ChartXAxisAreaData alloc] init];
areaData2.startX = 204.0;
areaData2.endX = 227.0;
[filledAreas addObject:areaData2];
_chartView.xAxis.filledAreas = filledAreas;
That would be an overkill as you want alternation, not to specify each and every stripe of color, am I missing something?
@danielgindi In my case, I needed to define arbitrary data-dependent grid areas, so that level of detail was helpful. But in the above case by @woutergoossens of alternate grid areas, that would be overkill.
Is there any way to add custom drawing to the chart?
eg. I would like similar functionality to this - but a little different, I want different background color for weekends, ie. very custom functionality.
It would be great to add this as a class conforming to some Drawable interface.
Was this ever implemented? Drawing a fill in between values, limit lines, etc...
Here is my implementation if someone wishes to add to the library or do their own. Currently it fills in between 2 fill lines on the Y showing an area that would indicate an ideal range.
Added this code to YAxisRenderer
open func renderLimitFill(context: CGContext) {
guard
let yAxis = self.axis as? YAxis,
let viewPortHandler = self.viewPortHandler,
let transformer = self.transformer
else { return }
var limitLines = yAxis.limitLines
if limitLines.count != 2
{
return
}
var upper = 0.0
var lower = 0.0
context.saveGState()
let trans = transformer.valueToPixelMatrix
var position = CGPoint(x: 0.0, y: 0.0)
for i in 0 ..< limitLines.count
{
let l = limitLines[i]
if !l.isEnabled
{
continue
}
if l.limit > upper {
upper = l.limit
}
else {
lower = l.limit
}
}
if upper == lower {
return
}
var startPosition = CGPoint(x: 0.0, y: 0.0)
startPosition.x = 0.0
startPosition.y = CGFloat(upper)
startPosition = startPosition.applying(trans)
var endPosition = CGPoint(x: 0.0, y: 0.0)
endPosition.y = CGFloat(lower)
endPosition = endPosition.applying(trans)
endPosition.x = viewPortHandler.contentRight
let rect = CGRect(x: min(startPosition.x, endPosition.x),
y: min(startPosition.y, endPosition.y),
width: fabs(startPosition.x - endPosition.x),
height: fabs(startPosition.y - endPosition.y));
context.setFillColor(UIColor.green.withAlphaComponent(0.3).cgColor)
context.setStrokeColor(UIColor.green.cgColor)
context.setLineWidth(0.0)
context.addRect(rect)
context.drawPath(using: .fillStroke)
context.restoreGState()
}
then called it in BarLineChartViewBase
if _leftAxis.isEnabled && _leftAxis.isDrawLimitLinesBehindDataEnabled
{
_leftYAxisRenderer?.renderLimitLines(context: context)
_leftYAxisRenderer?.renderLimitFill(context: context)
}
if _leftAxis.isEnabled && !_leftAxis.isDrawLimitLinesBehindDataEnabled
{
_leftYAxisRenderer?.renderLimitLines(context: context)
_leftYAxisRenderer?.renderLimitFill(context: context)
}
@jtweeks Would you be kind enough to tell me how do you call the function for the graph view ?
what is the variable context: CGContext a reference to? @jtweeks
@richwagner I'm working of your implementation but tried modified it so that we just provide an [Int]
public override func renderGridAreas(context: CGContext, chart: ChartViewBase)
{
// New isDrawGridAreasEnabled property parallels isDrawGridLinesEnableld
// xAxis.filledIndex is an array of Int values correspdoning to the index of entry to be highlighted
guard
let xAxis = self.axis as? XAxis,
let transformer = self.transformer
else { return }
if (!xAxis.drawGridAreasEnabled || !xAxis.isEnabled || xAxis.filledIndexes.count == 0)
{
return
}
let barData = chart.data as! BarChartData
let step = CGFloat(barData.dataSets.first?.entryCount ?? 0)
context.saveGState()
var position = CGPoint(x: 0.0, y: 0.0)
let valueToPixelMatrix = transformer.valueToPixelMatrix
let entries = xAxis.entries
// Iterate through filled areas
for index in xAxis.filledIndexes
{
// Get start position, using the same logic as used in rendering gridlines
let width = (viewPortHandler.contentWidth / step)
position.x = CGFloat(entries[index]) - 0.5
position = position.applying(valueToPixelMatrix)
// Draw rectangle - TODO externalize color
let rectangle = CGRect(x: position.x, y: viewPortHandler.contentTop, width: width, height: viewPortHandler.contentBottom + xAxis.labelHeight)
context.setFillColor(xAxis.gridAreaColor.cgColor)
context.setStrokeColor(xAxis.gridAreaColor.cgColor)
context.setLineWidth(1)
context.addRect(rectangle)
context.drawPath(using: .fillStroke)
}
context.restoreGState()
}
EDIT: I ended up rewriting this to make it part of the BarChartRenderer rather than the axis, and allowing highlighting to be either .bar
or .background
through HighlightStyle
https://github.com/AudaxHealthInc/Charts/tree/feature/missions
Hi @danielgindi
It's a perfect and great library.
Has the Chart library given this feature now?
Added this code to YAxisRenderer
open var fillAreaColor: NSUIColor = NSUIColor.clear
open var fillArea: (Float, Float)?
open func areaFill(context: CGContext) {
guard let transformer = self.transformer else { return }
// 如果颜色是clear,则不作填充处理
guard fillAreaColor != NSUIColor.clear else { return }
// 如果没配范围,则不作填充
guard let area = fillArea else { return }
let lower = area.0
let upper = area.1
if upper == lower {
return
}
context.saveGState()
let trans = transformer.valueToPixelMatrix
var startPosition = CGPoint(x: 0.0, y: 0.0)
startPosition.x = 0.0
startPosition.y = CGFloat(upper)
startPosition = startPosition.applying(trans)
var endPosition = CGPoint(x: 0.0, y: 0.0)
endPosition.y = CGFloat(lower)
endPosition = endPosition.applying(trans)
endPosition.x = viewPortHandler.contentRight
let rect = CGRect(x: min(startPosition.x, endPosition.x),
y: min(startPosition.y, endPosition.y),
width: abs(startPosition.x - endPosition.x),
height: abs(startPosition.y - endPosition.y));
context.setFillColor(fillAreaColor.cgColor)
// context.setStrokeColor(UIColor.green.cgColor)
context.setLineWidth(0.0)
context.addRect(rect)
context.drawPath(using: .fillStroke)
context.restoreGState()
}
called it in BarLineChartViewBase -> func draw(_ rect: CGRect)
leftYAxisRenderer.areaFill(context: context)
add an area to a chart like:
lineChartView.leftYAxisRenderer.fillAreaColor = UIColor.white.withAlphaComponent(0.2) lineChartView.leftYAxisRenderer.fillArea = (50, 60)
Thanks @jtweeks
@danielgindi is this feature available as part of latest library version ?
Hi,
Great library!
I have to create a custom chart like this. Everything is working except for the different grid background colours. Is that possible with this library?
Thanks in advance,
Wouter