Library I'm using: charts
I have line chart with values at specific days in year. I don't want to draw each day as label on x axis (there may be more than 300 days range) so I'm looking to draw months only. Right now, my xVals looks like: [nil, nil, nil, "07/2016", nil nil nil (..)]. Unfortunately, no label are displayed.
Is there any other way to achieve desired behaviour?
Code (x axis values):
while date.compare(to) != NSComparisonResult.OrderedDescending { // iterate over all days in range
let components = currentCalendar.components([.Day, .Month , .Year], fromDate: date)
if components.day != 1 {
vals.append(nil)
} else { // first day of the month
vals.append(String(components.month) + " / " + String(components.year))
}
let incrementComponents = NSDateComponents()
incrementComponents.day = 1
date = currentCalendar.dateByAddingComponents(incrementComponents, toDate: date, options: [])!
}
And entries:
for point in base!.points {
if let index = chartValuesFormatter?.indexFromDate(point.date) {
entries.append(ChartDataEntry(value: point.value, xIndex: index))
}
}
Generally it works, when I specify all values some of them are displayed and entries are rendered correctly. My only problem is to display only month labels on the beginning of month periods.
Ok, so I've found solution. It's not as simple as is should (for such basic problem), but is very customizable.
LineChartView (via BarLineChartViewBase)
has property xAxisRenderer
. You can subclass ChartXAxisRenderer
which is default one, and override drawLabels
function. In my case, I've copied original code and modified logic behind calculation what label and where should be drawn.
Edit: My custom renderer with overlap logic. Probably overcomplicated, but it works so far.
class ChartXAxisDateRenderer: ChartXAxisRenderer {
internal static let FDEG2RAD = CGFloat(M_PI / 180.0)
/// draws the x-labels on the specified y-position
override func drawLabels(context context: CGContext, pos: CGFloat, anchor: CGPoint) {
guard let xAxis = xAxis else { return }
let paraStyle = NSParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
paraStyle.alignment = .Center
let labelAttrs = [NSFontAttributeName: xAxis.labelFont,
NSForegroundColorAttributeName: xAxis.labelTextColor,
NSParagraphStyleAttributeName: paraStyle]
let labelRotationAngleRadians = xAxis.labelRotationAngle * ChartXAxisDateRenderer.FDEG2RAD
let valueToPixelMatrix = transformer.valueToPixelMatrix
let minLabelsMargin: CGFloat = 4.0
var position = CGPoint(x: 0.0, y: 0.0)
var labelMaxSize = CGSize()
if (xAxis.isWordWrapEnabled) {
labelMaxSize.width = xAxis.wordWrapWidthPercent * valueToPixelMatrix.a
}
var positions = [CGPoint]()
var widths = [CGFloat]()
var labels = [String]()
var originalIndices = [Int]()
for i in 0...xAxis.values.count-1 {
let label = xAxis.values[i]
if (label == nil || label == "")
{
continue
}
originalIndices.append(i)
labels.append(label!)
position.x = CGFloat(i)
position.y = 0.0
position = CGPointApplyAffineTransform(position, valueToPixelMatrix)
positions.append(position)
let labelns = label! as NSString
let width = labelns.boundingRectWithSize(labelMaxSize, options: .UsesLineFragmentOrigin, attributes: labelAttrs, context: nil).size.width
widths.append(width)
}
let newIndices = findBestPositions(positions, widths: widths, margin: minLabelsMargin)
for index in newIndices {
let label = labels[index]
let position = positions[index]
let i = originalIndices[index]
if (viewPortHandler.isInBoundsX(position.x)) {
drawLabel(context: context, label: label, xIndex: i, x: position.x, y: pos, attributes: labelAttrs, constrainedToSize: labelMaxSize, anchor: anchor, angleRadians: labelRotationAngleRadians)
}
}
}
// Best position indices - minimum "n" without overlapping
private func findBestPositions(positions: [CGPoint], widths: [CGFloat], margin: CGFloat) -> [Int] {
var n = 1
var overlap = true
// finding "n"
while n < widths.count && overlap {
overlap = doesOverlap(n, positions: positions, widths: widths, margin: margin)
if overlap {
n += 1
}
}
var newPositions = [Int]()
var i = 0
// create result indices
while i < positions.count {
newPositions.append(i)
i += n
}
return newPositions
}
// returns whether drawing only n-th labels will casue overlapping
private func doesOverlap(n: Int, positions: [CGPoint], widths: [CGFloat], margin: CGFloat) -> Bool {
var i = 0
var newPositions = [CGPoint]()
var newWidths = [CGFloat]()
// getting only n-th records
while i < positions.count {
newPositions.append(positions[i])
newWidths.append(widths[i])
i += n
}
// overlap with next label checking
for j in 0...newPositions.count - 2 {
if newPositions[j].x + newWidths[j] + margin > newPositions[j+1].x {
return true
}
}
return false
}
}