【HarmonyOS NEXT】ArkUI Canvas饼图(PieCharts)绘制

效果图:


piecharts_demo.gif

代码实现

demo传送门

使用

build() {
    Navigation() {
      Column() {
        PieCharts({
          options: this.options
        })

        Row() {
          Button('支出')
            .setButtonStyle(this.billType == 0)
            .onClick(() => {
              this.billType = 0
              this.loadData()
            })
          Blank()
            .width(12)
          Button('收入')
            .setButtonStyle(this.billType != 0)
            .onClick(() => {
              this.billType = 1
              this.loadData()
            })
        }
      }
      .size({ width: '100%', height: "50%" })
    }
    .title('收支分析')
    .titleMode(NavigationTitleMode.Mini)
  }

定义options

options.labelFn设置每个区块的文字;options.labelStyleFn设置文字样式,字体大小和字体颜色;options.colorFn设置每个区块的颜色。

let options = new PieChartsOptions()
options.animate = true
options.duration = 300
options.data = data
options.radius = 60
options.innerRadius = 30
options.labelFn = (data: PieChartsData) => {
  return (data as BillModel).category
}
options.labelStyleFn = (data: PieChartsData, index: number) => {
  let colors = [Color.Green, Color.Blue, Color.Orange, Color.Grey, '#FFCC00', Color.Pink, '#800080', '#ff6633', '#ffcc66', Color.Red]
  let style = new PieChartsLabelStyle()
  style.fontColor = colors[index]
  style.fontSize = 12
  return style
}
options.colorFn = (data: PieChartsData, index: number) => {
  let colors = [Color.Green, Color.Blue, Color.Orange, Color.Grey, '#FFCC00', Color.Pink, '#800080', '#ff6633', '#ffcc66', Color.Red]
  return colors[index]
}

计算每个扇形的角度

const radius = this.options.radius
const innerRadius = this.options.innerRadius
const brokenLineLength = 15
const brokenLineWidth = 1.5
const arcWidth = radius - innerRadius
const arcRadius = innerRadius + arcWidth / 2

let centerX = this.context.width / 2
let centerY = this.context.height / 2

const totalValue = data.reduce((acc, item) => acc + item.getValue(), 0)
const percentages: number[] = data.map(item => item.getValue() / totalValue);

let startAngle = -Math.PI / 2
for (let i = 0; i < percentages.length; i++) {
  let percent = percentages[i]
  let angle = percent * 2 * Math.PI * progress
  let endAngle = startAngle + angle

  // 画扇形
  ...
  // 画折线
  ...
  // 画文字
  ...
  startAngle = endAngle
}

画扇形

this.context.beginPath()
this.context.arc(centerX, centerY, arcRadius, startAngle, endAngle)
this.context.lineWidth = arcWidth
this.context.strokeStyle = color
this.context.stroke()
this.context.restore()

画折线

let centerAngle = startAngle + angle / 2
let r = radius + brokenLineLength / 2

let x1 = centerX + (r - brokenLineLength) * Math.cos(centerAngle)
let y1 = centerY + (r - brokenLineLength) * Math.sin(centerAngle)

let x2 = centerX + r * Math.cos(centerAngle)
let y2 = centerY + r * Math.sin(centerAngle)

let x3 = x2
let y3 = y2
if (centerAngle < Math.PI / 2) {
  this.context.textAlign = 'right'
  x3 = x2 + 15
} else {
  this.context.textAlign = 'left'
  x3 = x2 - 15
}

// 折线
let leaderLineColor = this.options.leaderLineColorFn(item, i)
this.context.beginPath()
this.context.lineWidth = brokenLineWidth
this.context.strokeStyle = leaderLineColor
this.context.moveTo(x1, y1)
this.context.lineTo(x2, y2)
this.context.lineTo(x3, y3)
this.context.stroke()

画文字

// 设置字体样式
const labelStyle = this.options.labelStyleFn(item, i)
this.context.textBaseline = 'middle'
this.context.fillStyle = labelStyle.fontColor
this.context.font = fp2px(labelStyle.fontSize) + 'px sans-serif'
// 获取文本
let label = this.options.labelFn(data[i], i)
let textWidth = this.context.measureText(label).width

let x4 = x3
let y4 = y3
if (centerAngle < Math.PI / 2) {
  this.context.textAlign = 'right'
  x3 = x2 + 15
  x4 = x3 + textWidth + 3
} else {
  this.context.textAlign = 'left'
  x3 = x2 - 15
  x4 = x3 - textWidth - 3
}

this.context.fillText(label, x4, y4)
this.context.stroke()

完整代码

export declare interface PieChartsData {
  getValue(): number
}

export class PieChartsLabelStyle {
  fontSize: number = 14
  fontColor: string | Color = Color.Orange
}

@Observed
export class PieChartsOptions {
  radius: number = 60
  innerRadius: number = 0
  data: PieChartsData[] = []
  animate: boolean = true
  duration: number = 500
  labelFn: (data: PieChartsData, index: number) => string = () => {
    return 'unknown'
  }
  labelStyleFn: (data: PieChartsData, index: number) => PieChartsLabelStyle = () => {
    return new PieChartsLabelStyle()
  }
  colorFn: (data: PieChartsData, index: number) => string | Color = (data: PieChartsData, index: number) => {
    let colors = [Color.Red, Color.Green, Color.Blue, Color.Orange, Color.Grey, Color.Pink, '#FFCC00', '#800080', '#ff6633', '#ffcc66']
    return colors[index % colors.length]
  }
  leaderLineColorFn: (data: PieChartsData, index: number) => string | Color = (data: PieChartsData, index: number) => {
    return Color.Orange
  }
}

@Component
export struct PieCharts {
  @Watch('animateDraw') @Prop options: PieChartsOptions
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  animateDraw() {
    console.log(`animateDraw`)
    if (!this.options.animate) {
      this.draw(1)
      return
    }

    let count = 0
    let interval = setInterval(() => {
      count++
      if (count == 60) {
        clearInterval(interval)
      }
      this.draw(count / 60)
    }, this.options.duration / 60)
  }

  draw(progress: number) {
    this.clearCanvas()

    const data = this.options.data
    const radius = this.options.radius
    const innerRadius = this.options.innerRadius
    const brokenLineLength = 15
    const brokenLineWidth = 1.5
    const arcWidth = radius - innerRadius
    const arcRadius = innerRadius + arcWidth / 2

    let centerX = this.context.width / 2
    let centerY = this.context.height / 2

    const totalValue = data.reduce((acc, item) => acc + item.getValue(), 0)
    const percentages: number[] = data.map(item => item.getValue() / totalValue);

    let startAngle = -Math.PI / 2
    for (let i = 0; i < percentages.length; i++) {
      let percent = percentages[i]
      let angle = percent * 2 * Math.PI * progress
      let endAngle = startAngle + angle

      let item = data[i]
      let color = this.options.colorFn(item, i)

      // 画扇形
      this.context.beginPath()
      this.context.arc(centerX, centerY, arcRadius, startAngle, endAngle)
      this.context.lineWidth = arcWidth
      this.context.strokeStyle = color
      this.context.stroke()
      this.context.restore()

      // 角度小于Math.PI / 12,不显示label
      if (angle <= Math.PI / 12) {
        startAngle = endAngle
        continue
      }

      // 画折线
      let centerAngle = startAngle + angle / 2
      let r = radius + brokenLineLength / 2

      let x1 = centerX + (r - brokenLineLength) * Math.cos(centerAngle)
      let y1 = centerY + (r - brokenLineLength) * Math.sin(centerAngle)

      let x2 = centerX + r * Math.cos(centerAngle)
      let y2 = centerY + r * Math.sin(centerAngle)

      let x3 = x2
      let y3 = y2
      if (centerAngle < Math.PI / 2) {
        this.context.textAlign = 'right'
        x3 = x2 + 15
      } else {
        this.context.textAlign = 'left'
        x3 = x2 - 15
      }

      // 折线
      let leaderLineColor = this.options.leaderLineColorFn(item, i)
      this.context.beginPath()
      this.context.lineWidth = brokenLineWidth
      this.context.strokeStyle = leaderLineColor
      this.context.moveTo(x1, y1)
      this.context.lineTo(x2, y2)
      this.context.lineTo(x3, y3)
      this.context.stroke()

      // 画文字
      // 设置字体样式
      const labelStyle = this.options.labelStyleFn(item, i)
      this.context.textBaseline = 'middle'
      this.context.fillStyle = labelStyle.fontColor
      this.context.font = fp2px(labelStyle.fontSize) + 'px sans-serif'
      // 获取文本
      let label = this.options.labelFn(data[i], i)
      let textWidth = this.context.measureText(label).width

      let x4 = x3
      let y4 = y3
      if (centerAngle < Math.PI / 2) {
        this.context.textAlign = 'right'
        x3 = x2 + 15
        x4 = x3 + textWidth + 3
      } else {
        this.context.textAlign = 'left'
        x3 = x2 - 15
        x4 = x3 - textWidth - 3
      }

      this.context.fillText(label, x4, y4)
      this.context.stroke()

      startAngle = endAngle
    }
  }

  clearCanvas() {
    this.context.restore();
    this.context.resetTransform();
    this.context.clearRect(0, 0, this.context.width, this.context.height);
  }

  build() {
    Column() {
      Canvas(this.context)
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
        .onReady(() => {
          this.animateDraw()
        })
    }
    .size({ width: '100%', height: "50%" })
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容