Source/Charts/Renderers/LegendRenderer.swift (427 lines of code) (raw):
//
// LegendRenderer.swift
// Charts
//
// Copyright 2015 Daniel Cohen Gindi & Philipp Jahoda
// A port of MPAndroidChart for iOS
// Licensed under Apache License 2.0
//
// https://github.com/danielgindi/Charts
//
import Foundation
import CoreGraphics
@objc(ChartLegendRenderer)
open class LegendRenderer: NSObject, Renderer
{
@objc public let viewPortHandler: ViewPortHandler
/// the legend object this renderer renders
@objc open var legend: Legend?
@objc public init(viewPortHandler: ViewPortHandler, legend: Legend?)
{
self.viewPortHandler = viewPortHandler
self.legend = legend
super.init()
}
/// Prepares the legend and calculates all needed forms, labels and colors.
@objc open func computeLegend(data: ChartData)
{
guard let legend = legend else { return }
if !legend.isLegendCustom
{
var entries: [LegendEntry] = []
// loop for building up the colors and labels used in the legend
for dataSet in data
{
let clrs: [NSUIColor] = dataSet.colors
let entryCount = dataSet.entryCount
// if we have a barchart with stacked bars
if dataSet is BarChartDataSetProtocol &&
(dataSet as! BarChartDataSetProtocol).isStacked
{
let bds = dataSet as! BarChartDataSetProtocol
let sLabels = bds.stackLabels
let minEntries = min(clrs.count, bds.stackSize)
for j in 0..<minEntries
{
let label: String?
if !sLabels.isEmpty && minEntries > 0
{
let labelIndex = j % minEntries
label = sLabels.indices.contains(labelIndex) ? sLabels[labelIndex] : nil
}
else
{
label = nil
}
let entry = LegendEntry(label: label)
entry.form = dataSet.form
entry.formSize = dataSet.formSize
entry.formLineWidth = dataSet.formLineWidth
entry.formLineDashPhase = dataSet.formLineDashPhase
entry.formLineDashLengths = dataSet.formLineDashLengths
entry.formColor = clrs[j]
entries.append(entry)
}
if dataSet.label != nil
{
// add the legend description label
let entry = LegendEntry(label: dataSet.label)
entry.form = .none
entries.append(entry)
}
}
else if dataSet is PieChartDataSetProtocol
{
let pds = dataSet as! PieChartDataSetProtocol
for j in 0..<min(clrs.count, entryCount)
{
let entry = LegendEntry(label: (pds.entryForIndex(j) as? PieChartDataEntry)?.label)
entry.form = dataSet.form
entry.formSize = dataSet.formSize
entry.formLineWidth = dataSet.formLineWidth
entry.formLineDashPhase = dataSet.formLineDashPhase
entry.formLineDashLengths = dataSet.formLineDashLengths
entry.formColor = clrs[j]
entries.append(entry)
}
if dataSet.label != nil
{
// add the legend description label
let entry = LegendEntry(label: dataSet.label)
entry.form = .none
entries.append(entry)
}
}
else if dataSet is CandleChartDataSetProtocol &&
(dataSet as! CandleChartDataSetProtocol).decreasingColor != nil
{
let candleDataSet = dataSet as! CandleChartDataSetProtocol
let decreasingEntry = LegendEntry(label: nil)
decreasingEntry.form = dataSet.form
decreasingEntry.formSize = dataSet.formSize
decreasingEntry.formLineWidth = dataSet.formLineWidth
decreasingEntry.formLineDashPhase = dataSet.formLineDashPhase
decreasingEntry.formLineDashLengths = dataSet.formLineDashLengths
decreasingEntry.formColor = candleDataSet.decreasingColor
entries.append(decreasingEntry)
let increasingEntry = LegendEntry(label: dataSet.label)
increasingEntry.form = dataSet.form
increasingEntry.formSize = dataSet.formSize
increasingEntry.formLineWidth = dataSet.formLineWidth
increasingEntry.formLineDashPhase = dataSet.formLineDashPhase
increasingEntry.formLineDashLengths = dataSet.formLineDashLengths
increasingEntry.formColor = candleDataSet.increasingColor
entries.append(increasingEntry)
}
else
{ // all others
for j in 0..<min(clrs.count, entryCount)
{
let label: String?
// if multiple colors are set for a DataSet, group them
if j < clrs.count - 1 && j < entryCount - 1
{
label = nil
}
else
{ // add label to the last entry
label = dataSet.label
}
let entry = LegendEntry(label: label)
entry.form = dataSet.form
entry.formSize = dataSet.formSize
entry.formLineWidth = dataSet.formLineWidth
entry.formLineDashPhase = dataSet.formLineDashPhase
entry.formLineDashLengths = dataSet.formLineDashLengths
entry.formColor = clrs[j]
entries.append(entry)
}
}
}
legend.entries = entries + legend.extraEntries
}
// calculate all dimensions of the legend
legend.calculateDimensions(labelFont: legend.font, viewPortHandler: viewPortHandler)
}
@objc open func renderLegend(context: CGContext)
{
guard let legend = legend else { return }
if !legend.enabled
{
return
}
let labelFont = legend.font
let labelTextColor = legend.textColor
let labelLineHeight = labelFont.lineHeight
let formYOffset = labelLineHeight / 2.0
let entries = legend.entries
let defaultFormSize = legend.formSize
let formToTextSpace = legend.formToTextSpace
let xEntrySpace = legend.xEntrySpace
let yEntrySpace = legend.yEntrySpace
let orientation = legend.orientation
let horizontalAlignment = legend.horizontalAlignment
let verticalAlignment = legend.verticalAlignment
let direction = legend.direction
// space between the entries
let stackSpace = legend.stackSpace
let yoffset = legend.yOffset
let xoffset = legend.xOffset
var originPosX: CGFloat = 0.0
switch horizontalAlignment
{
case .left:
if orientation == .vertical
{
originPosX = xoffset
}
else
{
originPosX = viewPortHandler.contentLeft + xoffset
}
if direction == .rightToLeft
{
originPosX += legend.neededWidth
}
case .right:
if orientation == .vertical
{
originPosX = viewPortHandler.chartWidth - xoffset
}
else
{
originPosX = viewPortHandler.contentRight - xoffset
}
if direction == .leftToRight
{
originPosX -= legend.neededWidth
}
case .center:
if orientation == .vertical
{
originPosX = viewPortHandler.chartWidth / 2.0
}
else
{
originPosX = viewPortHandler.contentLeft
+ viewPortHandler.contentWidth / 2.0
}
originPosX += (direction == .leftToRight
? +xoffset
: -xoffset)
// Horizontally layed out legends do the center offset on a line basis,
// So here we offset the vertical ones only.
if orientation == .vertical
{
if direction == .leftToRight
{
originPosX -= legend.neededWidth / 2.0 - xoffset
}
else
{
originPosX += legend.neededWidth / 2.0 - xoffset
}
}
}
switch orientation
{
case .horizontal:
let calculatedLineSizes = legend.calculatedLineSizes
let calculatedLabelSizes = legend.calculatedLabelSizes
let calculatedLabelBreakPoints = legend.calculatedLabelBreakPoints
var posX: CGFloat = originPosX
var posY: CGFloat
switch verticalAlignment
{
case .top:
posY = yoffset
case .bottom:
posY = viewPortHandler.chartHeight - yoffset - legend.neededHeight
case .center:
posY = (viewPortHandler.chartHeight - legend.neededHeight) / 2.0 + yoffset
}
var lineIndex: Int = 0
for i in entries.indices
{
let e = entries[i]
let drawingForm = e.form != .none
let formSize = e.formSize.isNaN ? defaultFormSize : e.formSize
if i < calculatedLabelBreakPoints.endIndex &&
calculatedLabelBreakPoints[i]
{
posX = originPosX
posY += labelLineHeight + yEntrySpace
}
if posX == originPosX &&
horizontalAlignment == .center &&
lineIndex < calculatedLineSizes.endIndex
{
posX += (direction == .rightToLeft
? calculatedLineSizes[lineIndex].width
: -calculatedLineSizes[lineIndex].width) / 2.0
lineIndex += 1
}
let isStacked = e.label == nil // grouped forms have null labels
if drawingForm
{
if direction == .rightToLeft
{
posX -= formSize
}
drawForm(
context: context,
x: posX,
y: posY + formYOffset,
entry: e,
legend: legend)
if direction == .leftToRight
{
posX += formSize
}
}
if !isStacked
{
if drawingForm
{
posX += direction == .rightToLeft ? -formToTextSpace : formToTextSpace
}
if direction == .rightToLeft
{
posX -= calculatedLabelSizes[i].width
}
drawLabel(
context: context,
x: posX,
y: posY,
label: e.label!,
font: labelFont,
textColor: e.labelColor ?? labelTextColor)
if direction == .leftToRight
{
posX += calculatedLabelSizes[i].width
}
posX += direction == .rightToLeft ? -xEntrySpace : xEntrySpace
}
else
{
posX += direction == .rightToLeft ? -stackSpace : stackSpace
}
}
case .vertical:
// contains the stacked legend size in pixels
var stack = CGFloat(0.0)
var wasStacked = false
var posY: CGFloat = 0.0
switch verticalAlignment
{
case .top:
posY = (horizontalAlignment == .center
? 0.0
: viewPortHandler.contentTop)
posY += yoffset
case .bottom:
posY = (horizontalAlignment == .center
? viewPortHandler.chartHeight
: viewPortHandler.contentBottom)
posY -= legend.neededHeight + yoffset
case .center:
posY = viewPortHandler.chartHeight / 2.0 - legend.neededHeight / 2.0 + legend.yOffset
}
for i in entries.indices
{
let e = entries[i]
let drawingForm = e.form != .none
let formSize = e.formSize.isNaN ? defaultFormSize : e.formSize
var posX = originPosX
if drawingForm
{
if direction == .leftToRight
{
posX += stack
}
else
{
posX -= formSize - stack
}
drawForm(
context: context,
x: posX,
y: posY + formYOffset,
entry: e,
legend: legend)
if direction == .leftToRight
{
posX += formSize
}
}
if e.label != nil
{
if drawingForm && !wasStacked
{
posX += direction == .leftToRight ? formToTextSpace : -formToTextSpace
}
else if wasStacked
{
posX = originPosX
}
if direction == .rightToLeft
{
posX -= (e.label! as NSString).size(withAttributes: [.font: labelFont]).width
}
if !wasStacked
{
drawLabel(context: context, x: posX, y: posY, label: e.label!, font: labelFont, textColor: e.labelColor ?? labelTextColor)
}
else
{
posY += labelLineHeight + yEntrySpace
drawLabel(context: context, x: posX, y: posY, label: e.label!, font: labelFont, textColor: e.labelColor ?? labelTextColor)
}
// make a step down
posY += labelLineHeight + yEntrySpace
stack = 0.0
}
else
{
stack += formSize + stackSpace
wasStacked = true
}
}
}
}
private var _formLineSegmentsBuffer = [CGPoint](repeating: CGPoint(), count: 2)
/// Draws the Legend-form at the given position with the color at the given index.
@objc open func drawForm(
context: CGContext,
x: CGFloat,
y: CGFloat,
entry: LegendEntry,
legend: Legend)
{
guard
let formColor = entry.formColor,
formColor != NSUIColor.clear
else { return }
var form = entry.form
if form == .default
{
form = legend.form
}
let formSize = entry.formSize.isNaN ? legend.formSize : entry.formSize
context.saveGState()
defer { context.restoreGState() }
switch form
{
case .none:
// Do nothing
break
case .empty:
// Do not draw, but keep space for the form
break
case .default: fallthrough
case .circle:
context.setFillColor(formColor.cgColor)
context.fillEllipse(in: CGRect(x: x, y: y - formSize / 2.0, width: formSize, height: formSize))
case .square:
context.setFillColor(formColor.cgColor)
context.fill(CGRect(x: x, y: y - formSize / 2.0, width: formSize, height: formSize))
case .line:
let formLineWidth = entry.formLineWidth.isNaN ? legend.formLineWidth : entry.formLineWidth
let formLineDashPhase = entry.formLineDashPhase.isNaN ? legend.formLineDashPhase : entry.formLineDashPhase
let formLineDashLengths = entry.formLineDashLengths == nil ? legend.formLineDashLengths : entry.formLineDashLengths
context.setLineWidth(formLineWidth)
if formLineDashLengths != nil && !formLineDashLengths!.isEmpty
{
context.setLineDash(phase: formLineDashPhase, lengths: formLineDashLengths!)
}
else
{
context.setLineDash(phase: 0.0, lengths: [])
}
context.setStrokeColor(formColor.cgColor)
_formLineSegmentsBuffer[0].x = x
_formLineSegmentsBuffer[0].y = y
_formLineSegmentsBuffer[1].x = x + formSize
_formLineSegmentsBuffer[1].y = y
context.strokeLineSegments(between: _formLineSegmentsBuffer)
}
}
/// Draws the provided label at the given position.
@objc open func drawLabel(context: CGContext, x: CGFloat, y: CGFloat, label: String, font: NSUIFont, textColor: NSUIColor)
{
context.drawText(label, at: CGPoint(x: x, y: y), align: .left, attributes: [.font: font, .foregroundColor: textColor])
}
}