在这里插入图片描述

项目概述

图表生成是现代应用开发中的重要需求。无论是在数据分析、业务报告、性能监控还是数据展示中,都需要进行各种图表生成和可视化操作。然而,不同的编程语言和平台对图表生成的实现方式各不相同,这导致开发者需要在不同平台上重复编写类似的逻辑。

本文介绍一个基于 Kotlin Multiplatform (KMP) 和 OpenHarmony 平台的图表生成高级工具库。这个工具库提供了一套完整的图表生成能力,包括柱状图、折线图、饼图、散点图等多种图表类型。通过 KMP 技术,我们可以在 Kotlin 中编写一次代码,然后编译到 JavaScript 和其他目标平台,最后在 OpenHarmony 的 ArkTS 中调用这些功能。

技术架构

多平台支持

  • Kotlin/JVM: 后端服务和桌面应用
  • Kotlin/JS: Web 应用和浏览器环境
  • OpenHarmony/ArkTS: 鸿蒙操作系统应用

核心功能模块

  1. 柱状图生成: 生成柱状图表
  2. 折线图生成: 生成折线图表
  3. 饼图生成: 生成饼图表
  4. 散点图生成: 生成散点图表
  5. 数据处理: 处理和转换图表数据
  6. 样式定制: 自定义图表样式
  7. 导出功能: 导出图表数据
  8. 统计分析: 进行数据统计分析

Kotlin 实现

核心图表生成类

// 文件: src/commonMain/kotlin/ChartGenerator.kt

/**
 * 图表生成工具类
 * 提供多种图表生成和数据可视化功能
 */
class ChartGenerator {
    
    data class ChartData(
        val labels: List<String>,
        val values: List<Double>,
        val title: String = "",
        val colors: List<String> = emptyList()
    )
    
    data class ChartConfig(
        val width: Int = 800,
        val height: Int = 600,
        val showLegend: Boolean = true,
        val showGrid: Boolean = true,
        val backgroundColor: String = "#FFFFFF"
    )
    
    /**
     * 生成柱状图
     * @param data 图表数据
     * @param config 图表配置
     * @return SVG 字符串
     */
    fun generateBarChart(data: ChartData, config: ChartConfig = ChartConfig()): String {
        val svg = StringBuilder()
        svg.append("<svg width='${config.width}' height='${config.height}' xmlns='http://www.w3.org/2000/svg'>\n")
        svg.append("<rect width='${config.width}' height='${config.height}' fill='${config.backgroundColor}'/>\n")
        
        if (data.values.isNotEmpty()) {
            val maxValue = data.values.maxOrNull() ?: 1.0
            val barWidth = (config.width * 0.8) / data.values.size
            val barHeight = config.height * 0.7
            
            data.values.forEachIndexed { index, value ->
                val x = (config.width * 0.1) + (index * barWidth)
                val height = (value / maxValue) * barHeight
                val y = config.height - height - 50
                val color = if (data.colors.isNotEmpty()) data.colors[index % data.colors.size] else "#4DB6AC"
                
                svg.append("<rect x='$x' y='$y' width='${barWidth * 0.8}' height='$height' fill='$color'/>\n")
                svg.append("<text x='${x + barWidth * 0.4}' y='${config.height - 20}' text-anchor='middle'>${data.labels.getOrNull(index) ?: ""}</text>\n")
            }
        }
        
        svg.append("</svg>")
        return svg.toString()
    }
    
    /**
     * 生成折线图
     * @param data 图表数据
     * @param config 图表配置
     * @return SVG 字符串
     */
    fun generateLineChart(data: ChartData, config: ChartConfig = ChartConfig()): String {
        val svg = StringBuilder()
        svg.append("<svg width='${config.width}' height='${config.height}' xmlns='http://www.w3.org/2000/svg'>\n")
        svg.append("<rect width='${config.width}' height='${config.height}' fill='${config.backgroundColor}'/>\n")
        
        if (data.values.size >= 2) {
            val maxValue = data.values.maxOrNull() ?: 1.0
            val pointSpacing = (config.width * 0.8) / (data.values.size - 1)
            val chartHeight = config.height * 0.7
            
            var pathData = "M"
            data.values.forEachIndexed { index, value ->
                val x = (config.width * 0.1) + (index * pointSpacing)
                val y = config.height - ((value / maxValue) * chartHeight) - 50
                pathData += " $x,$y"
            }
            
            svg.append("<polyline points='$pathData' fill='none' stroke='#4DB6AC' stroke-width='2'/>\n")
            
            data.values.forEachIndexed { index, _ ->
                val x = (config.width * 0.1) + (index * pointSpacing)
                val y = config.height - ((data.values[index] / maxValue) * chartHeight) - 50
                svg.append("<circle cx='$x' cy='$y' r='4' fill='#1A237E'/>\n")
            }
        }
        
        svg.append("</svg>")
        return svg.toString()
    }
    
    /**
     * 生成饼图
     * @param data 图表数据
     * @param config 图表配置
     * @return SVG 字符串
     */
    fun generatePieChart(data: ChartData, config: ChartConfig = ChartConfig()): String {
        val svg = StringBuilder()
        svg.append("<svg width='${config.width}' height='${config.height}' xmlns='http://www.w3.org/2000/svg'>\n")
        svg.append("<rect width='${config.width}' height='${config.height}' fill='${config.backgroundColor}'/>\n")
        
        val total = data.values.sum()
        if (total > 0) {
            val centerX = config.width / 2.0
            val centerY = config.height / 2.0
            val radius = Math.min(config.width, config.height) / 3.0
            
            var startAngle = 0.0
            data.values.forEachIndexed { index, value ->
                val sliceAngle = (value / total) * 360
                val endAngle = startAngle + sliceAngle
                
                val color = if (data.colors.isNotEmpty()) data.colors[index % data.colors.size] else "#4DB6AC"
                svg.append(createPieSlice(centerX, centerY, radius, startAngle, endAngle, color))
                
                startAngle = endAngle
            }
        }
        
        svg.append("</svg>")
        return svg.toString()
    }
    
    /**
     * 创建饼图切片
     */
    private fun createPieSlice(cx: Double, cy: Double, r: Double, startAngle: Double, endAngle: Double, color: String): String {
        val startRad = Math.toRadians(startAngle)
        val endRad = Math.toRadians(endAngle)
        
        val x1 = cx + r * Math.cos(startRad)
        val y1 = cy + r * Math.sin(startRad)
        val x2 = cx + r * Math.cos(endRad)
        val y2 = cy + r * Math.sin(endRad)
        
        val largeArc = if (endAngle - startAngle > 180) 1 else 0
        val pathData = "M $cx,$cy L $x1,$y1 A $r,$r 0 $largeArc,1 $x2,$y2 Z"
        
        return "<path d='$pathData' fill='$color' stroke='white' stroke-width='2'/>\n"
    }
    
    /**
     * 生成散点图
     * @param data 图表数据
     * @param config 图表配置
     * @return SVG 字符串
     */
    fun generateScatterChart(data: ChartData, config: ChartConfig = ChartConfig()): String {
        val svg = StringBuilder()
        svg.append("<svg width='${config.width}' height='${config.height}' xmlns='http://www.w3.org/2000/svg'>\n")
        svg.append("<rect width='${config.width}' height='${config.height}' fill='${config.backgroundColor}'/>\n")
        
        if (data.values.isNotEmpty()) {
            val maxValue = data.values.maxOrNull() ?: 1.0
            val chartWidth = config.width * 0.8
            val chartHeight = config.height * 0.7
            
            data.values.forEachIndexed { index, value ->
                val x = (config.width * 0.1) + ((index.toDouble() / data.values.size) * chartWidth)
                val y = config.height - ((value / maxValue) * chartHeight) - 50
                val color = if (data.colors.isNotEmpty()) data.colors[index % data.colors.size] else "#4DB6AC"
                
                svg.append("<circle cx='$x' cy='$y' r='6' fill='$color' opacity='0.7'/>\n")
            }
        }
        
        svg.append("</svg>")
        return svg.toString()
    }
    
    /**
     * 计算数据统计信息
     * @param data 数据列表
     * @return 统计信息映射
     */
    fun calculateStatistics(data: List<Double>): Map<String, Double> {
        if (data.isEmpty()) return emptyMap()
        
        val sum = data.sum()
        val average = sum / data.size
        val max = data.maxOrNull() ?: 0.0
        val min = data.minOrNull() ?: 0.0
        
        val variance = data.map { (it - average) * (it - average) }.sum() / data.size
        val stdDev = Math.sqrt(variance)
        
        return mapOf(
            "sum" to sum,
            "average" to average,
            "max" to max,
            "min" to min,
            "variance" to variance,
            "stdDev" to stdDev,
            "count" to data.size.toDouble()
        )
    }
    
    /**
     * 数据归一化
     * @param data 原始数据
     * @return 归一化后的数据
     */
    fun normalizeData(data: List<Double>): List<Double> {
        if (data.isEmpty()) return emptyList()
        
        val max = data.maxOrNull() ?: 1.0
        val min = data.minOrNull() ?: 0.0
        val range = max - min
        
        return if (range == 0.0) {
            data.map { 0.5 }
        } else {
            data.map { (it - min) / range }
        }
    }
    
    /**
     * 生成图表配置字符串
     * @param config 图表配置
     * @return 配置字符串
     */
    fun generateConfigString(config: ChartConfig): String {
        return """
            图表配置:
            宽度: ${config.width}px
            高度: ${config.height}px
            显示图例: ${config.showLegend}
            显示网格: ${config.showGrid}
            背景色: ${config.backgroundColor}
        """.trimIndent()
    }
}

Kotlin 实现的核心特点

Kotlin 实现中的图表生成功能充分利用了 Kotlin 标准库的数学和字符串处理能力。图表生成使用了 SVG 格式来创建矢量图表。数据统计使用了集合的聚合操作。

数据归一化使用了数学公式。饼图生成使用了三角函数计算。所有图表都支持自定义样式和配置。

JavaScript 实现

编译后的 JavaScript 代码

// 文件: build/js/packages/kmp_openharmony-js/kotlin/kmp_openharmony.js
// (由 Kotlin 编译器自动生成)

/**
 * ChartGenerator 类的 JavaScript 版本
 * 通过 Kotlin/JS 编译器从 Kotlin 源代码生成
 */
class ChartGenerator {
  /**
   * 生成柱状图
   * @param {Object} data - 图表数据
   * @param {Object} config - 图表配置
   * @returns {string} SVG 字符串
   */
  generateBarChart(data, config = {}) {
    config = { width: 800, height: 600, backgroundColor: '#FFFFFF', ...config };
    
    let svg = `<svg width='${config.width}' height='${config.height}' xmlns='http://www.w3.org/2000/svg'>\n`;
    svg += `<rect width='${config.width}' height='${config.height}' fill='${config.backgroundColor}'/>\n`;
    
    if (data.values && data.values.length > 0) {
      const maxValue = Math.max(...data.values);
      const barWidth = (config.width * 0.8) / data.values.length;
      const barHeight = config.height * 0.7;
      
      data.values.forEach((value, index) => {
        const x = (config.width * 0.1) + (index * barWidth);
        const height = (value / maxValue) * barHeight;
        const y = config.height - height - 50;
        const color = data.colors && data.colors[index % data.colors.length] ? data.colors[index % data.colors.length] : '#4DB6AC';
        
        svg += `<rect x='${x}' y='${y}' width='${barWidth * 0.8}' height='${height}' fill='${color}'/>\n`;
        svg += `<text x='${x + barWidth * 0.4}' y='${config.height - 20}' text-anchor='middle'>${data.labels[index] || ''}</text>\n`;
      });
    }
    
    svg += '</svg>';
    return svg;
  }

  /**
   * 生成折线图
   * @param {Object} data - 图表数据
   * @param {Object} config - 图表配置
   * @returns {string} SVG 字符串
   */
  generateLineChart(data, config = {}) {
    config = { width: 800, height: 600, backgroundColor: '#FFFFFF', ...config };
    
    let svg = `<svg width='${config.width}' height='${config.height}' xmlns='http://www.w3.org/2000/svg'>\n`;
    svg += `<rect width='${config.width}' height='${config.height}' fill='${config.backgroundColor}'/>\n`;
    
    if (data.values && data.values.length >= 2) {
      const maxValue = Math.max(...data.values);
      const pointSpacing = (config.width * 0.8) / (data.values.length - 1);
      const chartHeight = config.height * 0.7;
      
      let pathData = 'M';
      data.values.forEach((value, index) => {
        const x = (config.width * 0.1) + (index * pointSpacing);
        const y = config.height - ((value / maxValue) * chartHeight) - 50;
        pathData += ` ${x},${y}`;
      });
      
      svg += `<polyline points='${pathData}' fill='none' stroke='#4DB6AC' stroke-width='2'/>\n`;
      
      data.values.forEach((value, index) => {
        const x = (config.width * 0.1) + (index * pointSpacing);
        const y = config.height - ((value / maxValue) * chartHeight) - 50;
        svg += `<circle cx='${x}' cy='${y}' r='4' fill='#1A237E'/>\n`;
      });
    }
    
    svg += '</svg>';
    return svg;
  }

  /**
   * 计算数据统计信息
   * @param {number[]} data - 数据列表
   * @returns {Object} 统计信息
   */
  calculateStatistics(data) {
    if (data.length === 0) return {};
    
    const sum = data.reduce((a, b) => a + b, 0);
    const average = sum / data.length;
    const max = Math.max(...data);
    const min = Math.min(...data);
    
    const variance = data.reduce((sum, val) => sum + Math.pow(val - average, 2), 0) / data.length;
    const stdDev = Math.sqrt(variance);
    
    return {
      sum: sum,
      average: average,
      max: max,
      min: min,
      variance: variance,
      stdDev: stdDev,
      count: data.length
    };
  }

  /**
   * 数据归一化
   * @param {number[]} data - 原始数据
   * @returns {number[]} 归一化后的数据
   */
  normalizeData(data) {
    if (data.length === 0) return [];
    
    const max = Math.max(...data);
    const min = Math.min(...data);
    const range = max - min;
    
    if (range === 0) {
      return data.map(() => 0.5);
    }
    
    return data.map(val => (val - min) / range);
  }
}

JavaScript 实现的特点

JavaScript 版本完全由 Kotlin/JS 编译器自动生成,确保了与 Kotlin 版本的行为完全一致。JavaScript 的字符串模板和数组方法提供了必要的图表生成能力。

reduce 方法用于数据聚合。map 方法用于数据转换。Math 对象用于数学计算。

ArkTS 调用代码

OpenHarmony 应用集成

// 文件: kmp_ceshiapp/entry/src/main/ets/pages/ChartGeneratorPage.ets

import { ChartGenerator } from '../../../../../../../build/js/packages/kmp_openharmony-js/kotlin/kmp_openharmony';

@Entry
@Component
struct ChartGeneratorPage {
  @State selectedChart: string = 'bar';
  @State chartSvg: string = '';
  @State result: string = '';
  @State resultTitle: string = '';

  private generator = new ChartGenerator();

  private chartTypes = [
    { name: '柱状图', value: 'bar' },
    { name: '折线图', value: 'line' },
    { name: '饼图', value: 'pie' },
    { name: '散点图', value: 'scatter' },
    { name: '数据统计', value: 'statistics' },
    { name: '数据归一化', value: 'normalize' }
  ];

  build() {
    Column() {
      // 标题
      Text('📊 图表生成高级工具库')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .padding(20)
        .backgroundColor('#1A237E')
        .textAlign(TextAlign.Center)

      Scroll() {
        Column() {
          // 图表类型选择
          Column() {
            Text('选择图表类型')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .margin({ bottom: 12 })

            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(this.chartTypes, (chart: { name: string; value: string }) => {
                Button(chart.name)
                  .layoutWeight(1)
                  .height(40)
                  .margin({ right: 8, bottom: 8 })
                  .backgroundColor(this.selectedChart === chart.value ? '#1A237E' : '#E0E0E0')
                  .fontColor(this.selectedChart === chart.value ? '#FFFFFF' : '#333333')
                  .fontSize(12)
                  .onClick(() => {
                    this.selectedChart = chart.value;
                    this.chartSvg = '';
                    this.result = '';
                    this.resultTitle = '';
                  })
              })
            }
            .width('100%')
          }
          .width('95%')
          .margin({ top: 16, left: '2.5%', right: '2.5%', bottom: 16 })
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(6)

          // 生成按钮
          Row() {
            Button('✨ 生成图表')
              .layoutWeight(1)
              .height(44)
              .backgroundColor('#1A237E')
              .fontColor('#FFFFFF')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .borderRadius(6)
              .onClick(() => this.generateChart())

            Blank()
              .width(12)

            Button('🔄 清空')
              .layoutWeight(1)
              .height(44)
              .backgroundColor('#F5F5F5')
              .fontColor('#1A237E')
              .fontSize(14)
              .border({ width: 1, color: '#4DB6AC' })
              .borderRadius(6)
              .onClick(() => {
                this.chartSvg = '';
                this.result = '';
                this.resultTitle = '';
              })
          }
          .width('95%')
          .margin({ left: '2.5%', right: '2.5%', bottom: 16 })

          // 图表显示区域
          if (this.chartSvg) {
            Column() {
              Text('📈 生成的图表')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .width('100%')
                .padding(12)
                .backgroundColor('#1A237E')
                .borderRadius(6)
                .textAlign(TextAlign.Center)
                .margin({ bottom: 12 })

              Scroll() {
                Text(this.chartSvg)
                  .fontSize(11)
                  .fontColor('#333333')
                  .fontFamily('monospace')
                  .textAlign(TextAlign.Start)
                  .width('100%')
                  .padding(12)
                  .selectable(true)
              }
              .width('100%')
              .height(250)
              .backgroundColor('#F9F9F9')
              .border({ width: 1, color: '#4DB6AC' })
              .borderRadius(6)
            }
            .width('95%')
            .margin({ left: '2.5%', right: '2.5%', bottom: 16 })
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(6)
          }

          // 结果显示
          if (this.resultTitle) {
            Column() {
              Text(this.resultTitle)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .width('100%')
                .padding(12)
                .backgroundColor('#1A237E')
                .borderRadius(6)
                .textAlign(TextAlign.Center)
                .margin({ bottom: 12 })

              Scroll() {
                Text(this.result)
                  .fontSize(12)
                  .fontColor('#333333')
                  .fontFamily('monospace')
                  .textAlign(TextAlign.Start)
                  .width('100%')
                  .padding(12)
                  .selectable(true)
              }
              .width('100%')
              .height(250)
              .backgroundColor('#F9F9F9')
              .border({ width: 1, color: '#4DB6AC' })
              .borderRadius(6)
            }
            .width('95%')
            .margin({ left: '2.5%', right: '2.5%', bottom: 16 })
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(6)
          }
        }
        .width('100%')
      }
      .layoutWeight(1)
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  private generateChart() {
    const sampleData = {
      labels: ['一月', '二月', '三月', '四月', '五月'],
      values: [65, 78, 45, 92, 58],
      title: '销售数据',
      colors: ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']
    };

    const config = {
      width: 600,
      height: 400,
      showLegend: true,
      showGrid: true,
      backgroundColor: '#FFFFFF'
    };

    try {
      switch (this.selectedChart) {
        case 'bar':
          this.chartSvg = this.generator.generateBarChart(sampleData, config);
          this.resultTitle = '📊 柱状图';
          this.result = '柱状图生成成功!显示了五个月的销售数据。';
          break;

        case 'line':
          this.chartSvg = this.generator.generateLineChart(sampleData, config);
          this.resultTitle = '📈 折线图';
          this.result = '折线图生成成功!展示了销售趋势。';
          break;

        case 'pie':
          this.chartSvg = this.generator.generatePieChart(sampleData, config);
          this.resultTitle = '🥧 饼图';
          this.result = '饼图生成成功!显示了各月份的销售占比。';
          break;

        case 'scatter':
          this.chartSvg = this.generator.generateScatterChart(sampleData, config);
          this.resultTitle = '⚫ 散点图';
          this.result = '散点图生成成功!展示了数据分布。';
          break;

        case 'statistics':
          const stats = this.generator.calculateStatistics(sampleData.values);
          this.resultTitle = '📊 数据统计';
          this.result = `总和: ${stats.sum}\n平均值: ${stats.average.toFixed(2)}\n最大值: ${stats.max}\n最小值: ${stats.min}\n标准差: ${stats.stdDev.toFixed(2)}\n数据个数: ${stats.count}`;
          break;

        case 'normalize':
          const normalized = this.generator.normalizeData(sampleData.values);
          this.resultTitle = '🔄 数据归一化';
          this.result = `原始数据: ${sampleData.values.join(', ')}\n\n归一化后: ${normalized.map(v => v.toFixed(4)).join(', ')}`;
          break;
      }
    } catch (e) {
      this.resultTitle = '❌ 生成出错';
      this.result = `错误: ${e}`;
    }
  }
}

ArkTS 集成的关键要点

在 OpenHarmony 应用中集成图表生成工具库需要考虑多种图表类型和用户体验。我们设计了一个灵活的 UI,能够支持不同的图表生成操作。

图表类型选择界面使用了 Flex 布局和 FlexWrap 来实现响应式的按钮排列。图表显示区域使用了可滚动的文本来展示生成的 SVG 代码。

结果显示使用了可选择的文本,这样用户可以轻松复制统计结果。对于不同的操作,我们显示了相应的图表或统计信息。

工作流程详解

图表生成的完整流程

  1. 类型选择: 用户在 ArkTS UI 中选择要生成的图表类型
  2. 数据准备: 准备图表所需的数据和配置
  3. 生成执行: 调用 ChartGenerator 的相应方法
  4. 结果展示: 将生成的图表或统计结果显示在 UI 中

跨平台一致性

通过 KMP 技术,我们确保了在所有平台上的行为一致性。无论是在 Kotlin/JVM、Kotlin/JS 还是通过 ArkTS 调用,图表生成的逻辑和结果都是完全相同的。

实际应用场景

数据分析工具

在数据分析工具中,需要生成各种图表来展示数据。这个工具库提供了多种图表生成功能。

业务报告系统

在业务报告系统中,需要生成专业的图表。这个工具库提供了高质量的图表生成能力。

性能监控仪表板

在性能监控中,需要实时展示性能数据。这个工具库提供了动态图表生成功能。

数据可视化应用

在数据可视化应用中,需要多种图表类型。这个工具库提供了完整的图表生成解决方案。

性能优化

图表缓存

在频繁生成相同图表时,可以缓存生成结果以避免重复计算。

增量更新

在数据更新时,应该考虑使用增量更新的方式以提高性能。

安全性考虑

数据验证

在处理用户输入的数据时,应该进行验证以确保数据的有效性。

输出转义

在生成 SVG 时,应该对特殊字符进行转义以防止注入攻击。

总结

这个 KMP OpenHarmony 图表生成高级工具库展示了如何使用现代的跨平台技术来处理常见的数据可视化任务。通过 Kotlin Multiplatform 技术,我们可以在一个地方编写业务逻辑,然后在多个平台上使用。

图表生成是数据应用开发中的重要功能。通过使用这样的工具库,开发者可以快速、可靠地生成各种图表,从而提高应用的数据展示能力和用户体验。

在实际应用中,建议根据具体的需求进行定制和扩展,例如添加更多的图表类型、实现更复杂的交互功能等高级特性。同时,定期进行性能测试和优化,确保应用在处理大量数据时仍然保持良好的性能。在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐