diff --git a/AAInfographics.podspec b/AAInfographics.podspec index b25dba67..7c14ccaf 100755 --- a/AAInfographics.podspec +++ b/AAInfographics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'AAInfographics' - s.version = '9.1.0' + s.version = '9.2.0' s.summary = '📈📊📱📺💻An elegant modern declarative data visualization chart framework for iOS, iPadOS and macOS. Extremely powerful, supports line, spline, area, areaspline, column, bar, pie, scatter, angular gauges, arearange, areasplinerange, columnrange, bubble, box plot, error bars, funnel, waterfall and polar chart types. 极其精美而又强大的跨平台数据可视化图表框架,支持柱状图、条形图、折线图、曲线图、折线填充图、曲线填充图、气泡图、扇形图、环形图、散点图、雷达图、混合图等各种类型的多达几十种的信息图图表,完全满足工作所需.' s.homepage = 'https://github.com/AAChartModel/AAChartKit-Swift' s.license = 'MIT' diff --git a/AAInfographics/AAChartCreator/AAChartModel.swift b/AAInfographics/AAChartCreator/AAChartModel.swift index 1d252a83..f361fc4e 100755 --- a/AAInfographics/AAChartCreator/AAChartModel.swift +++ b/AAInfographics/AAChartCreator/AAChartModel.swift @@ -156,6 +156,7 @@ public class AAChartModel: AAObject { public var animationType: AAChartAnimationType? //The type of chart animation public var animationDuration: Int? //The chart rendering animation duration public var title: String? //The chart title + public var titleAlign: AAChartAlignType?//The chart title text align style public var titleStyle: AAStyle? //The chart title style public var subtitle: String? //The chart subtitle public var subtitleAlign: AAChartAlignType?//The chart subtitle text align style @@ -194,6 +195,7 @@ public class AAChartModel: AAObject { public var colorsTheme: [Any]? //An array containing the default colors for the chart's series. When all colors are used, new colors are pulled from the start again. Defaults to: ["#1e90ff", "#ef476f", "#ffd066", "#04d69f", "#25547c",] public var series: [Any]? //An array of all the chart's series public var legendEnabled: Bool? //Enable or disable the legend. Defaults to true + public var legendItemStyle: AAStyle? //The item style of the legend public var backgroundColor: Any? //The background color or gradient for the outer chart area. Defaults to #FFFFFF public var borderRadius: Any? //The corner radius of the outer chart border. Defaults to 0 public var markerRadius: Float? //The radius of the point marker. Defaults to 4 @@ -218,6 +220,12 @@ public class AAChartModel: AAObject { return self } + @discardableResult + public func titleAlign(_ prop: AAChartAlignType) -> AAChartModel { + titleAlign = prop + return self + } + @discardableResult public func titleStyle(_ prop: AAStyle) -> AAChartModel { titleStyle = prop @@ -468,6 +476,12 @@ public class AAChartModel: AAObject { return self } + @discardableResult + public func legendItemStyle(_ prop: AAStyle) -> AAChartModel { + legendItemStyle = prop + return self + } + @discardableResult public func backgroundColor(_ prop: Any) -> AAChartModel { backgroundColor = prop diff --git a/AAInfographics/AAChartCreator/AAChartView.swift b/AAInfographics/AAChartCreator/AAChartView.swift index f828cd02..d7cbe2ba 100644 --- a/AAInfographics/AAChartCreator/AAChartView.swift +++ b/AAInfographics/AAChartCreator/AAChartView.swift @@ -89,6 +89,8 @@ public class AAChartView: WKWebView { private var clickEventEnabled: Bool? private var touchEventEnabled: Bool? + private var beforeDrawChartJavaScript: String? + private var afterDrawChartJavaScript: String? private weak var _delegate: AAChartViewDelegate? public weak var delegate: AAChartViewDelegate? { @@ -174,6 +176,10 @@ public class AAChartView: WKWebView { private var optionsJson: String? + #if DEBUG + public var shouldPrintOptionsJSON: Bool = true + #endif + // MARK: - Initialization override private init(frame: CGRect, configuration: WKWebViewConfiguration) { super.init(frame: frame, configuration: configuration) @@ -194,9 +200,25 @@ public class AAChartView: WKWebView { private func drawChart() { + if beforeDrawChartJavaScript != nil { + #if DEBUG + print("📝 \(beforeDrawChartJavaScript ?? "")") + #endif + safeEvaluateJavaScriptString(beforeDrawChartJavaScript!) + beforeDrawChartJavaScript = nil + } + //Add `frame.size.height` to solve the problem that the height of the new version of Highcharts chart will not adapt to the container - let jsStr = "loadTheHighChartView('\(optionsJson ?? "")','\(contentWidth ?? 0)','\(contentHeight ?? frame.size.height)')" + let jsStr = "loadTheHighChartView('\(optionsJson ?? "")','\(contentWidth ?? 0)','\(contentHeight ?? 0)');" safeEvaluateJavaScriptString(jsStr) + + if afterDrawChartJavaScript != nil { + #if DEBUG + print("📝 \(afterDrawChartJavaScript ?? "")") + #endif + safeEvaluateJavaScriptString(afterDrawChartJavaScript!) + afterDrawChartJavaScript = nil + } } private func safeEvaluateJavaScriptString (_ jsString: String) { @@ -221,9 +243,9 @@ public class AAChartView: WKWebView { code = \(objcError.code); domain = \(objcError.domain); userInfo = { - NSLocalizedDescription = "A JavaScript exception occurred"; + NSLocalizedDescription = "\(errorUserInfo["NSLocalizedDescription"] ?? "")"; WKJavaScriptExceptionColumnNumber = \(errorUserInfo["WKJavaScriptExceptionColumnNumber"] ?? ""); - WKJavaScriptExceptionLineNumber = \(errorUserInfo["WKJavaScriptExceptionLineNumber"] ?? ""); + WKJavaScriptExceptionLineNumber = \(errorUserInfo["WKJavaScriptExceptionLineNumber"] ?? ""); WKJavaScriptExceptionMessage = \(errorUserInfo["WKJavaScriptExceptionMessage"] ?? ""); WKJavaScriptExceptionSourceURL = \(errorUserInfo["WKJavaScriptExceptionSourceURL"] ?? ""); } @@ -252,32 +274,43 @@ public class AAChartView: WKWebView { } private func configureOptionsJsonStringWithAAOptions(_ aaOptions: AAOptions) { + if aaOptions.beforeDrawChartJavaScript != nil { + beforeDrawChartJavaScript = aaOptions.beforeDrawChartJavaScript + aaOptions.beforeDrawChartJavaScript = nil + } + + if aaOptions.afterDrawChartJavaScript != nil { + afterDrawChartJavaScript = aaOptions.afterDrawChartJavaScript + aaOptions.afterDrawChartJavaScript = nil + } + if isClearBackgroundColor == true { aaOptions.chart?.backgroundColor = AAColor.clear } if clickEventEnabled == true { aaOptions.clickEventEnabled = true - configurePlotOptionsSeriesPointEvents(aaOptions) } if touchEventEnabled == true { aaOptions.touchEventEnabled = true - if clickEventEnabled != true { //Avoid duplicate invocation of configuration method - configurePlotOptionsSeriesPointEvents(aaOptions) - } + } + if clickEventEnabled == true || touchEventEnabled == true { + configurePlotOptionsSeriesPointEvents(aaOptions) } #if DEBUG - let modelJsonDic = aaOptions.toDic() - let data = try? JSONSerialization.data(withJSONObject: modelJsonDic, options: .prettyPrinted) - if data != nil { - let prettyPrintedModelJson = String(data: data!, encoding: String.Encoding.utf8) - print(""" + if shouldPrintOptionsJSON { + let modelJsonDic = aaOptions.toDic() + let data = try? JSONSerialization.data(withJSONObject: modelJsonDic, options: .prettyPrinted) + if data != nil { + let prettyPrintedModelJson = String(data: data!, encoding: String.Encoding.utf8) + print(""" -----------🖨🖨🖨 console log AAOptions JSON information of AAChartView 🖨🖨🖨-----------: \(prettyPrintedModelJson!) """) + } } #endif @@ -607,12 +640,12 @@ extension AAChartView { queue: nil) { [weak self] _ in //Delay execution by 0.01 seconds to prevent incorrect screen width and height obtained when the screen is rotated DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - self?.handleDeviceOrientationChangeEventWithAnimation(animation) + self?.aa_resizeChart(animation: animation) } } } - private func handleDeviceOrientationChangeEventWithAnimation(_ animation: AAAnimation) { + public func aa_resizeChart(animation: AAAnimation) { let animationJsonStr = animation.toJSON() let jsFuncStr = "changeChartSize('\(frame.size.width)','\(frame.size.height)','\(animationJsonStr)')" safeEvaluateJavaScriptString(jsFuncStr) @@ -735,43 +768,69 @@ extension AAChartView { extension AAChartView { func getJSONStringFromDictionary(dictionary: [String: Any]) -> String { - if !JSONSerialization.isValidJSONObject(dictionary) { - print("❌ String object is not valid Dictionary JSON String") + guard JSONSerialization.isValidJSONObject(dictionary) else { + print("❌ Dictionary object is not valid JSON") return "" } - let data: Data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) - let JSONString = String(data: data, encoding: .utf8) - return JSONString! as String + do { + let data = try JSONSerialization.data(withJSONObject: dictionary, options: []) + if let jsonString = String(data: data, encoding: .utf8) { + return jsonString + } + } catch { + print("❌ Error serializing dictionary to JSON: \(error.localizedDescription)") + } + return "" } func getJSONStringFromArray(array: [Any]) -> String { - if !JSONSerialization.isValidJSONObject(array) { - print("❌ String object is not valid Array JSON String") + guard JSONSerialization.isValidJSONObject(array) else { + print("❌ Array object is not valid JSON") return "" } - let data: Data = try! JSONSerialization.data(withJSONObject: array, options: []) - let JSONString = String(data: data, encoding: .utf8) - return JSONString! as String + do { + let data = try JSONSerialization.data(withJSONObject: array, options: []) + if let jsonString = String(data: data, encoding: .utf8) { + return jsonString + } + } catch { + print("❌ Error serializing array to JSON: \(error.localizedDescription)") + } + return "" } func getDictionaryFromJSONString(jsonString: String) -> [String: Any] { - let jsonData: Data = jsonString.data(using: .utf8)! - let dict = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) - if dict != nil { - return dict as! [String: Any] + guard let jsonData = jsonString.data(using: .utf8) else { + print("❌ Failed to convert string to data") + return [:] + } + + do { + if let dict = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [String: Any] { + return dict + } + } catch { + print("❌ Error parsing JSON string to dictionary: \(error.localizedDescription)") } - return [String: Any]() + return [:] } func getArrayFromJSONString(jsonString: String) -> [Any] { - let jsonData: Data = jsonString.data(using: .utf8)! - let array = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) - if array != nil { - return array as! [Any] + guard let jsonData = jsonString.data(using: .utf8) else { + print("❌ Failed to convert string to data") + return [] + } + + do { + if let array = try JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) as? [Any] { + return array + } + } catch { + print("❌ Error parsing JSON string to array: \(error.localizedDescription)") } - return [Any]() + return [] } } diff --git a/AAInfographics/AAChartCreator/AAOptions.swift b/AAInfographics/AAChartCreator/AAOptions.swift index 5e4e4c8f..a63cf333 100644 --- a/AAInfographics/AAChartCreator/AAOptions.swift +++ b/AAInfographics/AAChartCreator/AAOptions.swift @@ -52,6 +52,12 @@ public class AAOptions: AAObject { internal var clickEventEnabled: Bool? //Please DO NOT use this property internal var touchEventEnabled: Bool? //Please DO NOT use this property + //beforeDrawChartJavaScript + public var beforeDrawChartJavaScript: String? + //afterDrawChartJavaScript + public var afterDrawChartJavaScript: String? + + @discardableResult public func chart(_ prop: AAChart?) -> AAOptions { chart = prop @@ -142,6 +148,18 @@ public class AAOptions: AAObject { return self } + @discardableResult + public func beforeDrawChartJavaScript(_ prop: String?) -> AAOptions { + beforeDrawChartJavaScript = prop + return self + } + + @discardableResult + public func afterDrawChartJavaScript(_ prop: String?) -> AAOptions { + afterDrawChartJavaScript = prop + return self + } + public override init() { let aaCredits = AACredits() aaCredits.enabled = false @@ -169,7 +187,9 @@ public class AAOptionsConstructor { .text(aaChartModel.title) //Title text content if aaChartModel.title != "" { - aaTitle.style(aaChartModel.titleStyle) + aaTitle + .align(aaChartModel.titleAlign) //Title horizontal alignment + .style(aaChartModel.titleStyle) } var aaSubtitle: AASubtitle? @@ -200,6 +220,7 @@ public class AAOptionsConstructor { let aaLegend = AALegend() .enabled(aaChartModel.legendEnabled) + .itemStyle(aaChartModel.legendItemStyle) let aaOptions = AAOptions() .chart(aaChart) diff --git a/AAInfographics/AAChartCreator/AASerializable.swift b/AAInfographics/AAChartCreator/AASerializable.swift index 0c1c09e9..b8e6777b 100755 --- a/AAInfographics/AAChartCreator/AASerializable.swift +++ b/AAInfographics/AAChartCreator/AASerializable.swift @@ -30,11 +30,11 @@ */ - import Foundation -public class AAObject { } - +open class AAObject { + public init() {} +} @available(iOS 10.0, macCatalyst 13.1, macOS 10.13, *) public extension AAObject { @@ -44,64 +44,70 @@ public extension AAObject { } } +@available(iOS 10.0, macCatalyst 13.1, macOS 10.13, *) +public protocol AASerializableWithComputedProperties { + /// 返回计算属性的键值对 + func computedProperties() -> [String: Any] +} @available(iOS 10.0, macCatalyst 13.1, macOS 10.13, *) public extension AAObject { - fileprivate func loopForMirrorChildren(_ mirrorChildren: Mirror.Children, _ representation: inout [String : Any]) { + fileprivate func loopForMirrorChildren(_ mirrorChildren: Mirror.Children, _ representation: inout [String: Any]) { for case let (label?, value) in mirrorChildren { - switch value { - case let value as AAObject: do { + if let value = value as? AAObject { representation[label] = value.toDic() - } - - case let value as [AAObject]: do { - var aaObjectArr = [Any]() - - let valueCount = value.count - for i in 0 ..< valueCount { - let aaObject = value[i] - let aaObjectDic = aaObject.toDic() - aaObjectArr.append(aaObjectDic as Any) - } - - representation[label] = aaObjectArr - } - - case let value as NSObject: do { + } else if let value = value as? [AAObject] { + // 使用 map 简化数组转换 + representation[label] = value.map { $0.toDic() } + } else if let value = value as? NSObject { representation[label] = value } - - default: - // Ignore any unserializable properties - break - } } } func toDic() -> [String: Any] { - var representation = [String: Any]() + // 创建 Mirror 对象 + let mirror = Mirror(reflecting: self) - let mirrorChildren = Mirror(reflecting: self).children - loopForMirrorChildren(mirrorChildren, &representation) + // 预估容量 + let estimatedCapacity = mirror.children.underestimatedCount + + (mirror.superclassMirror?.children.underestimatedCount ?? 0) + 5 + var representation = [String: Any](minimumCapacity: estimatedCapacity) - let superMirrorChildren = Mirror(reflecting: self).superclassMirror?.children - if superMirrorChildren?.count ?? 0 > 0 { - loopForMirrorChildren(superMirrorChildren!, &representation) + // 遍历当前类和父类的反射子属性 + var currentMirror: Mirror? = mirror + while let current = currentMirror { + loopForMirrorChildren(current.children, &representation) + currentMirror = current.superclassMirror } + // 添加计算属性 + addComputedProperties(to: &representation) + return representation } + private func addComputedProperties(to representation: inout [String: Any]) { + if let selfWithComputed = self as? AASerializableWithComputedProperties { + let computedProps = selfWithComputed.computedProperties() + for (key, value) in computedProps { + representation[key] = value + } + } + } + func toJSON() -> String { do { - let data = try JSONSerialization.data(withJSONObject: toDic() as Any, options: []) - guard let jsonStr = String(data: data, encoding: String.Encoding.utf8) else { return "" } - return jsonStr - } catch { + let data = try JSONSerialization.data(withJSONObject: toDic(), options: [.fragmentsAllowed]) + guard let jsonString = String(data: data, encoding: .utf8) else { + print("JSON encoding error: Unable to convert data to String.") + return "" + } + return jsonString + } catch let error as NSError { + print("JSON serialization error: \(error.localizedDescription)") return "" } } - -} - +} \ No newline at end of file diff --git a/AAInfographics/AAJSFiles.bundle/AAChartView.html b/AAInfographics/AAJSFiles.bundle/AAChartView.html index 756c9582..9d333e77 100755 --- a/AAInfographics/AAJSFiles.bundle/AAChartView.html +++ b/AAInfographics/AAJSFiles.bundle/AAChartView.html @@ -14,11 +14,18 @@
-