SwiftUI原生Chart组件如何实现鼠标移动选中才显示数值

原创 吴就业 78 0 2024-08-27

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/f1fc62ba24d64a78a452f18ce28ef39c

作者:吴就业
链接:https://wujiuye.com/article/f1fc62ba24d64a78a452f18ce28ef39c
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

前言

在开发ChatTCP的报表展示功能的时候,我选择使用SwiftUI的原生Chart组件。但是这东西文档太少了,除了官方那不算文档的文档,遇到问题都好难解决。我在做柱形图的时候,想要实现像网站使用ercharts实现的鼠标移动到柱状图上面才弹窗显示对应数值的效果,苦于找不到文档,于是放弃了。

于是就有了后来提交到appstore审核被驳回的事情,驳回原因如下图:

截屏2024-08-27 22.56.04

由于多组数据Chart组件使用了堆叠展示,好像也不支持对比的方式展示。因为堆叠展示,一个BarItem显示两个数值是很容易出现文字重叠的情况的,是很影响用户体验。

为了能够审核通过,只能硬着头皮去解决了。

实现方式

通过官方的介绍,我了解到是可以实现鼠标移动(选中)然后弹窗显示数值的,并且怎么显示还是可以自定义的,只是官方给的代码并不完整,并且案例是一个折线图的。

后面参考这篇文章《Building Pie Charts and Donut Charts with SwiftUI in iOS 17》摸索出了解决方案,这篇文章介绍了饼图怎么实现鼠标移动到某个扇区的时候,把某个扇区高亮显示。然后我再基于官方的折线图案例,通过不断试错,摸索出了柱状图的解决方案。

饼图如何实现

先介绍饼图如何实现。

我想实现的效果是,当鼠标移动到某个扇区时,这个扇区高亮显示,并且显示数值,而其它扇区变暗,并且不显示数值。当鼠标未在任意一个扇区时,所有扇区都恢复高亮显示。

代码如下:

@State private var selectedAngle:Int?
@State private var selectedSector: String?

Chart(chartData) { data in
                    SectorMark(
                        angle: .value("Count", data.count),
                        innerRadius: .ratio(0.618),
                        angularInset: 1.5
                    )
                    .cornerRadius(8)
                    .foregroundStyle(data.color)
                    .opacity(selectedSector == nil || data.name == selectedSector ? 1.0 : 0.3) 
                    .annotation(position: .overlay) {
                        Group {
                            if data.name == selectedSector {
                                Text("\(data.count)")
                                    .foregroundStyle(.themeContentBase)
                            }
                        }
                    }
                }.chartAngleSelection(value: $selectedAngle)
                    .onChange(of: selectedAngle) { _, newValue in
                        if let newValue {
                            selectedSector = findSelectedSector(value: newValue)
                        } else {
                            selectedSector = nil
                        }
                    }

代码解释:

内层SectorMark的调用:

最外层Chart的调用:

onChange方法中通过调用findSelectedSector方法来获取当前选中的扇区对应的是哪个数据项(item),实现代码如下。

private func findSelectedSector(value: Int) -> String? {
        var accumulatedCount = 0
        let selectedItem = chartData.first { item in
            accumulatedCount += Int(item.count)
            return value <= accumulatedCount
        }
        return selectedItem?.name // 返回选中项的名称
}

大致意思就是,按顺序遍历数据项,将每一项的值加起来,如果累加到当前项,这个值大于等于鼠标所在位置角度值,那么就停止遍历,这一项就是当前选中项。

效果如下:

截屏2024-08-27 23.45.58

柱形图如何实现

有了饼图的案例,我们就知道大致的思路了。

柱形图不能用chartAngleSelection,但是可以用chartXSelection和chartYSelection来获取当前鼠标所在位置的x坐标的值,或者y坐标的值。

代码实现如下:

@State private var selectedSector: String?

Chart {
            ForEach(barItem) { data in
                BarMark(
                    x: .value("Name", data.name),
                    y: .value("Value", data.value)
                )
                .foregroundStyle(data.color)
                .foregroundStyle(by: .value("Category", data.category))
            }
            if let selectedName = selectedSector {
                RuleMark(
                    x: .value("Selected", selectedName)
                )
                .foregroundStyle(Color.gray.opacity(0))
                .offset(yStart: -8)
                .zIndex(999)
                .annotation(position: .top, spacing: 0, overflowResolution: AnnotationOverflowResolution(x: .fit(to: .chart), y: .disabled)) {
                    HStack(spacing: 8) {
                        ForEach(barItem) { data in
                            if data.name == selectedName {
                                Label(
                                    title: {
                                        Text("\(Int(data.value))")
                                            .foregroundStyle(.themeContentBase)
                                    },
                                    icon: {
                                        Circle()
                                            .fill(data.color)
                                            .frame(width: 8, height: 8)
                                    }
                                )
                            }
                        }
                    }.frame(alignment: .leading)
                }
            }
        }.chartXSelection(value: $selectedSector)

由于我的案例中,柱形图显示的数据,x轴是字符串,y轴是数值,然后我只需要知道当前x轴的值是什么,在对应的BarItem上面显示值就可以了。所以这里字段selectedSector的类型是String。官网的折线图例子x轴坐标是日期,所以官网例子中对应的selectedSector字段的类型是Day。

案例中if let selectedName = selectedSector就是判断当前鼠标是否在某个BarItem上,如果是就通过RuleMark来实现“弹窗”显示这个对应x轴上多个堆叠的BarItem的y轴值。案例中实现的是在x轴位置柱形图的头顶来显示。

其中这一段就是显示数值的代码,可以自由发挥。ForEach循环是遍历多组数据每组数据对应选中的BarItem的值。

							HStack(spacing: 8) {
                        ForEach(barItem) { data in
                            if data.name == selectedName {
                                Label(
                                    title: {
                                        Text("\(Int(data.value))")
                                            .foregroundStyle(.themeContentBase)
                                    },
                                    icon: {
                                        Circle()
                                            .fill(data.color)
                                            .frame(width: 8, height: 8)
                                    }
                                )
                            }
                        }
                    }.frame(alignment: .leading)

效果如下:

截屏2024-08-27 23.46.07


参考文献:

#前端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

SwiftUI如何阻止应用进程重启恢复到之前的状态

假如你不希望你的应用重启时,恢复到上次用户打开的窗口,而是想每次都从启动页开始。那么怎么做呢?

微信小程序原生开发获取input光标位置和监听光标位置改变问题

input失去焦点后,用户点击input重新获取焦点,怎么获取光标位置?如果用户获取到焦点之后,没有输入,而是移动光标,怎么获取移动后的光标位置呢?

SwiftUI开发macOS APP,如何自定义标题栏(TitleBar)

macOS APP应用的标题栏(TitleBar)是指包含“交通信号灯”(红黄绿三个小按钮)的一栏,通常TitleBar只包括“交通信号灯”和一个title,使用SwiftUI开发macOS APP,怎么把TitleBar隐藏掉呢?

如何将macOS app发布到app store?

开发完一个macOS app之后,我们应该如何将其发布到app store呢?这个文档记录了笔者从零开始一步步学习将自己开发好的macOS app发布到app store的过程。

如何编译构建一个同时支持多cpu架构的静态链接库(.a文件)

我想要让自己的mac OS应用支持更多用户可以使用,就必须要打出一个能同在amd64架构cpu的mac os设备上运行,也需要能在arm64架构cpu的mac os设备上运行。

使用node:alpine基础镜像今天突然就构建不了了

我的一个nextjs项目使用node:alpine基础镜像,在此之前部署到线上都能正常构建和运行,今天修改点代码提交到生产环境,发现竟然镜像构建失败了。