123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- //
- // Legend.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
- #if canImport(UIKit)
- import UIKit
- #endif
- #if canImport(AppKit)
- import AppKit
- #endif
- @objc(ChartLegend)
- open class Legend: ComponentBase
- {
- @objc(ChartLegendForm)
- public enum Form: Int
- {
- /// Avoid drawing a form
- case none
-
- /// Do not draw the a form, but leave space for it
- case empty
-
- /// Use default (default dataset's form to the legend's form)
- case `default`
-
- /// Draw a square
- case square
-
- /// Draw a circle
- case circle
-
- /// Draw a horizontal line
- case line
- }
-
- @objc(ChartLegendHorizontalAlignment)
- public enum HorizontalAlignment: Int
- {
- case left
- case center
- case right
- }
-
- @objc(ChartLegendVerticalAlignment)
- public enum VerticalAlignment: Int
- {
- case top
- case center
- case bottom
- }
-
- @objc(ChartLegendOrientation)
- public enum Orientation: Int
- {
- case horizontal
- case vertical
- }
-
- @objc(ChartLegendDirection)
- public enum Direction: Int
- {
- case leftToRight
- case rightToLeft
- }
-
- /// The legend entries array
- @objc open var entries = [LegendEntry]()
-
- /// Entries that will be appended to the end of the auto calculated entries after calculating the legend.
- /// (if the legend has already been calculated, you will need to call notifyDataSetChanged() to let the changes take effect)
- @objc open var extraEntries = [LegendEntry]()
-
- /// Are the legend labels/colors a custom value or auto calculated? If false, then it's auto, if true, then custom.
- ///
- /// **default**: false (automatic legend)
- private var _isLegendCustom = false
- /// The horizontal alignment of the legend
- @objc open var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.left
-
- /// The vertical alignment of the legend
- @objc open var verticalAlignment: VerticalAlignment = VerticalAlignment.bottom
-
- /// The orientation of the legend
- @objc open var orientation: Orientation = Orientation.horizontal
-
- /// Flag indicating whether the legend will draw inside the chart or outside
- @objc open var drawInside: Bool = false
-
- /// Flag indicating whether the legend will draw inside the chart or outside
- @objc open var isDrawInsideEnabled: Bool { return drawInside }
-
- /// The text direction of the legend
- @objc open var direction: Direction = Direction.leftToRight
- @objc open var font: NSUIFont = NSUIFont.systemFont(ofSize: 10.0)
- @objc open var textColor = NSUIColor.labelOrBlack
- /// The form/shape of the legend forms
- @objc open var form = Form.square
-
- /// The size of the legend forms
- @objc open var formSize = CGFloat(8.0)
-
- /// The line width for forms that consist of lines
- @objc open var formLineWidth = CGFloat(3.0)
-
- /// Line dash configuration for shapes that consist of lines.
- ///
- /// This is how much (in pixels) into the dash pattern are we starting from.
- @objc open var formLineDashPhase: CGFloat = 0.0
-
- /// Line dash configuration for shapes that consist of lines.
- ///
- /// This is the actual dash pattern.
- /// I.e. [2, 3] will paint [-- -- ]
- /// [1, 3, 4, 2] will paint [- ---- - ---- ]
- @objc open var formLineDashLengths: [CGFloat]?
-
- @objc open var xEntrySpace = CGFloat(6.0)
- @objc open var yEntrySpace = CGFloat(0.0)
- @objc open var formToTextSpace = CGFloat(5.0)
- @objc open var stackSpace = CGFloat(3.0)
-
- @objc open var calculatedLabelSizes = [CGSize]()
- @objc open var calculatedLabelBreakPoints = [Bool]()
- @objc open var calculatedLineSizes = [CGSize]()
-
- public override init()
- {
- super.init()
-
- self.xOffset = 5.0
- self.yOffset = 3.0
- }
-
- @objc public init(entries: [LegendEntry])
- {
- super.init()
-
- self.entries = entries
- }
-
- @objc open func getMaximumEntrySize(withFont font: NSUIFont) -> CGSize
- {
- var maxW = CGFloat(0.0)
- var maxH = CGFloat(0.0)
-
- var maxFormSize: CGFloat = 0.0
- for entry in entries
- {
- let formSize = entry.formSize.isNaN ? self.formSize : entry.formSize
- if formSize > maxFormSize
- {
- maxFormSize = formSize
- }
-
- guard let label = entry.label
- else { continue }
-
- let size = (label as NSString).size(withAttributes: [.font: font])
-
- if size.width > maxW
- {
- maxW = size.width
- }
- if size.height > maxH
- {
- maxH = size.height
- }
- }
-
- return CGSize(
- width: maxW + maxFormSize + formToTextSpace,
- height: maxH
- )
- }
- @objc open var neededWidth = CGFloat(0.0)
- @objc open var neededHeight = CGFloat(0.0)
- @objc open var textWidthMax = CGFloat(0.0)
- @objc open var textHeightMax = CGFloat(0.0)
-
- /// flag that indicates if word wrapping is enabled
- /// this is currently supported only for `orientation == Horizontal`.
- /// you may want to set maxSizePercent when word wrapping, to set the point where the text wraps.
- ///
- /// **default**: true
- @objc open var wordWrapEnabled = true
-
- /// if this is set, then word wrapping the legend is enabled.
- @objc open var isWordWrapEnabled: Bool { return wordWrapEnabled }
- /// The maximum relative size out of the whole chart view in percent.
- /// If the legend is to the right/left of the chart, then this affects the width of the legend.
- /// If the legend is to the top/bottom of the chart, then this affects the height of the legend.
- ///
- /// **default**: 0.95 (95%)
- @objc open var maxSizePercent: CGFloat = 0.95
-
- @objc open func calculateDimensions(labelFont: NSUIFont, viewPortHandler: ViewPortHandler)
- {
- let maxEntrySize = getMaximumEntrySize(withFont: labelFont)
- let defaultFormSize = self.formSize
- let stackSpace = self.stackSpace
- let formToTextSpace = self.formToTextSpace
- let xEntrySpace = self.xEntrySpace
- let yEntrySpace = self.yEntrySpace
- let wordWrapEnabled = self.wordWrapEnabled
- let entries = self.entries
- let entryCount = entries.count
-
- textWidthMax = maxEntrySize.width
- textHeightMax = maxEntrySize.height
-
- switch orientation
- {
- case .vertical:
-
- var maxWidth = CGFloat(0.0)
- var width = CGFloat(0.0)
- var maxHeight = CGFloat(0.0)
- let labelLineHeight = labelFont.lineHeight
-
- var wasStacked = false
-
- for i in entries.indices
- {
- let e = entries[i]
- let drawingForm = e.form != .none
- let formSize = e.formSize.isNaN ? defaultFormSize : e.formSize
- if !wasStacked
- {
- width = 0.0
- }
-
- if drawingForm
- {
- if wasStacked
- {
- width += stackSpace
- }
- width += formSize
- }
-
- if let label = e.label
- {
- let size = (label as NSString).size(withAttributes: [.font: labelFont])
- if drawingForm && !wasStacked
- {
- width += formToTextSpace
- }
- else if wasStacked
- {
- maxWidth = max(maxWidth, width)
- maxHeight += labelLineHeight + yEntrySpace
- width = 0.0
- wasStacked = false
- }
-
- width += size.width
- maxHeight += labelLineHeight + yEntrySpace
- }
- else
- {
- wasStacked = true
- width += formSize
-
- if i < entryCount - 1
- {
- width += stackSpace
- }
- }
-
- maxWidth = max(maxWidth, width)
- }
-
- neededWidth = maxWidth
- neededHeight = maxHeight
-
- case .horizontal:
-
- let labelLineHeight = labelFont.lineHeight
-
- let contentWidth: CGFloat = viewPortHandler.contentWidth * maxSizePercent
-
- // Prepare arrays for calculated layout
- if calculatedLabelSizes.count != entryCount
- {
- calculatedLabelSizes = [CGSize](repeating: CGSize(), count: entryCount)
- }
-
- if calculatedLabelBreakPoints.count != entryCount
- {
- calculatedLabelBreakPoints = [Bool](repeating: false, count: entryCount)
- }
-
- calculatedLineSizes.removeAll(keepingCapacity: true)
-
- // Start calculating layout
-
- var maxLineWidth: CGFloat = 0.0
- var currentLineWidth: CGFloat = 0.0
- var requiredWidth: CGFloat = 0.0
- var stackedStartIndex: Int = -1
-
- for i in entries.indices
- {
- let e = entries[i]
- let drawingForm = e.form != .none
- let label = e.label
-
- calculatedLabelBreakPoints[i] = false
-
- if stackedStartIndex == -1
- {
- // we are not stacking, so required width is for this label only
- requiredWidth = 0.0
- }
- else
- {
- // add the spacing appropriate for stacked labels/forms
- requiredWidth += stackSpace
- }
-
- // grouped forms have null labels
- if let label = label
- {
- calculatedLabelSizes[i] = (label as NSString).size(withAttributes: [.font: labelFont])
- requiredWidth += drawingForm ? formToTextSpace + formSize : 0.0
- requiredWidth += calculatedLabelSizes[i].width
- }
- else
- {
- calculatedLabelSizes[i] = CGSize()
- requiredWidth += drawingForm ? formSize : 0.0
-
- if stackedStartIndex == -1
- {
- // mark this index as we might want to break here later
- stackedStartIndex = i
- }
- }
-
- if label != nil || i == entryCount - 1
- {
- let requiredSpacing = currentLineWidth == 0.0 ? 0.0 : xEntrySpace
-
- if (!wordWrapEnabled || // No word wrapping, it must fit.
- currentLineWidth == 0.0 || // The line is empty, it must fit.
- (contentWidth - currentLineWidth >= requiredSpacing + requiredWidth)) // It simply fits
- {
- // Expand current line
- currentLineWidth += requiredSpacing + requiredWidth
- }
- else
- { // It doesn't fit, we need to wrap a line
-
- // Add current line size to array
- calculatedLineSizes.append(CGSize(width: currentLineWidth, height: labelLineHeight))
- maxLineWidth = max(maxLineWidth, currentLineWidth)
-
- // Start a new line
- calculatedLabelBreakPoints[stackedStartIndex > -1 ? stackedStartIndex : i] = true
- currentLineWidth = requiredWidth
- }
-
- if i == entryCount - 1
- { // Add last line size to array
- calculatedLineSizes.append(CGSize(width: currentLineWidth, height: labelLineHeight))
- maxLineWidth = max(maxLineWidth, currentLineWidth)
- }
- }
-
- stackedStartIndex = label != nil ? -1 : stackedStartIndex
- }
-
- neededWidth = maxLineWidth
- neededHeight = labelLineHeight * CGFloat(calculatedLineSizes.count) +
- yEntrySpace * CGFloat(calculatedLineSizes.isEmpty ? 0 : (calculatedLineSizes.count - 1))
- }
-
- neededWidth += xOffset
- neededHeight += yOffset
- }
-
- /// MARK: - Custom legend
-
- /// Sets a custom legend's entries array.
- /// * A nil label will start a group.
- /// This will disable the feature that automatically calculates the legend entries from the datasets.
- /// Call `resetCustom(...)` to re-enable automatic calculation (and then `notifyDataSetChanged()` is needed).
- @objc open func setCustom(entries: [LegendEntry])
- {
- self.entries = entries
- _isLegendCustom = true
- }
-
- /// Calling this will disable the custom legend entries (set by `setLegend(...)`). Instead, the entries will again be calculated automatically (after `notifyDataSetChanged()` is called).
- @objc open func resetCustom()
- {
- _isLegendCustom = false
- }
-
- /// **default**: false (automatic legend)
- /// `true` if a custom legend entries has been set
- @objc open var isLegendCustom: Bool
- {
- return _isLegendCustom
- }
- }
|