【Flutter】graphic图表实现自定义tooltip

renderer

在这里插入图片描述

graphic中tooltip的TooltipGuide类提供了renderer方法,接收三个参数Size类型,Offset类型,Map<int, Tuple>类型。可查到的文档是真的少,所以只能在源码中扒拉例子,做符合需求的修改。

官方github示例

官方示例

这个例子感觉像是tooltip和提供的那些属性的源码实现,然后改变了背景颜色等,但如果实现想echarts那样对每条线的数据前增加颜色块区分,还是要自己摸索。先看一下这个例子都做了什么吧

List<MarkElement> simpleTooltip(Size size,Offset anchor,Map<int, Tuple> selectedTuples,) {// 返回的元素列表List<MarkElement> elements;// 生成tooltip内容String textContent = '';// 选中的数据 ({date: xxx, name: 线条1, points: xxx}, {date: xx, name: 线条2, points: xx}...)final selectedTupleList = selectedTuples.values;// 选中的数据的字段名列表// 单条线:[date, points]// 多条线:会通过name区分不同线的数值[date, name, points]final fields = selectedTupleList.first.keys.toList();// 如果只有一条线if (selectedTuples.length == 1) {// 取出选中的数据final original = selectedTupleList.single;// 取出第一个字段的值var field = fields.first;// 将第一个字段的值放入到tooltip的第一行/*** 此时的数据结构是:* date: 2023-11-24*/textContent += '$field: ${original[field]}';// 遍历字段名列表for (var i = 1; i < fields.length; i++) {// 取出第i个字段field = fields[i];// 将第i个字段的值放入到tooltip的第二行/*** 遍历后的数据结构是:* date: 2023-11-24* points: 123*/textContent += '\n$field: ${original[field]}';}} else {// 如果有多条线// 遍历选中的数据(几条线几条数据),将每个数据的第二个字段和第三个字段的值放入到tooltip的第二行和第三行for (var original in selectedTupleList) {// 取出第一个字段final domainField = fields.first;// 取出最后一个字段final measureField = fields.last;/*** 遍历结束后的数据结构是:* 2023-11-24:线条1* 2023-11-24:线条2* ....*/textContent += '\n${original[domainField]}: ${original[measureField]}';}}// 提出一些变量const textStyle = TextStyle(fontSize: 12, color: Colors.white);const padding = EdgeInsets.all(5);const align = Alignment.topRight;const offset = Offset(5, -5);const elevation = 1.0;const backgroundColor = Colors.black;final painter = TextPainter(text: TextSpan(text: textContent, style: textStyle),textDirection: TextDirection.ltr,);painter.layout();// 计算tooltip的宽高final width = padding.left + painter.width + padding.right;final height = padding.top + painter.height + padding.bottom;// 调整tooltip弹框(包含内容)的位置final paintPoint = getBlockPaintPoint(anchor + offset,width,height,align,);// 调整tooltip弹框(不包含内容)的位置final window = Rect.fromLTWH(paintPoint.dx,paintPoint.dy,width,height,);// 计算tooltip文本的位置var textPaintPoint = paintPoint + padding.topLeft;elements = <MarkElement>[RectElement(rect: window,style: PaintStyle(fillColor: backgroundColor, elevation: elevation)),LabelElement(text: textContent,anchor: textPaintPoint,style:LabelStyle(textStyle: textStyle, align: Alignment.bottomRight)),];return elements;}

效果
在这里插入图片描述

根据需求调整

在这里插入图片描述

改动后代码

List<MarkElement> simpleTooltip(Size size,Offset anchor,Map<int, Tuple> selectedTuples,) {// 返回的元素列表List<MarkElement> elements;// 标识元素列表List<MarkElement> tagElements = [];// 生成tooltip内容String textContent = '';// 选中的数据 ({date: xxx, name: 线条1, points: xxx}, {date: xx, name: 线条2, points: xx}...)final selectedTupleList = selectedTuples.values;// 选中的数据的字段名列表 [date, name, points]final fields = selectedTupleList.first.keys.toList();// 选中的数据的第一个数据的第一个字段的值,放入到tooltip的第一行/*** 目前的数据结构是:* 2023-11-24*/textContent = '${selectedTupleList.first[fields.first]}';// 遍历选中的数据(几条线几条数据),将每个数据的第二个字段和第三个字段的值放入到tooltip的第二行和第三行for (var original in selectedTupleList) {final domainField = fields[1];final measureField = fields.last;/*** 遍历结束后的数据结构是:* 2023-11-24* 线条1: 123* 线条2: 456* ....*/textContent += '\n  ${original[domainField]}: ${original[measureField]}';}// 提出一些变量const textStyle = TextStyle(fontSize: 12, color: Colors.black, height: 2);const padding = EdgeInsets.all(5);const align = Alignment.topRight;const offset = Offset(5, -5);const elevation = 1.0;const backgroundColor = Colors.white;final painter = TextPainter(text: TextSpan(text: textContent, style: textStyle),textDirection: ui.TextDirection.ltr,);painter.layout();// tooltip的宽高final width = padding.left + painter.width + padding.right;final height = padding.top + painter.height + padding.bottom;// tooltip的位置// 大概根据中间的数据判断算了下位置,避免一直在左或右,边界超出屏幕final move = anchor < const Offset(250, 90)? anchor + offset - const Offset(-10, -40): anchor + Offset(-width - 20, 40);final paintPoint = getBlockPaintPoint(move,width,height,align,);final window = Rect.fromLTWH(paintPoint.dx - 10, //横向位置paintPoint.dy,width + 20,height,);var textPaintPoint = paintPoint + padding.topLeft;// 生成tooltip线条前的标识元素for (int i = 0; i < selectedTupleList.length; i++) {tagElements.add(LabelElement(text: '●',anchor: textPaintPoint + padding.topLeft + Offset(-15, 26 + i * 23),style: LabelStyle(textStyle: TextStyle(color: Defaults.colors10[i],fontWeight: FontWeight.w900,fontSize: 12),align: Alignment.bottomRight)),);}elements = <MarkElement>[RectElement(rect: window,style: PaintStyle(fillColor: backgroundColor, elevation: elevation)),...tagElements,LabelElement(text: textContent,anchor: textPaintPoint,style:LabelStyle(textStyle: textStyle, align: Alignment.bottomRight)),];return elements;}

效果
在这里插入图片描述

整体代码

// linePage.dart
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:graphic/graphic.dart';
import 'dart:ui' as ui;
import './components/static/data.dart';class linePage extends StatelessWidget {linePage({super.key});final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();List<MarkElement> simpleTooltip(Size size,Offset anchor,Map<int, Tuple> selectedTuples,) {// 返回的元素列表List<MarkElement> elements;// 标识元素列表List<MarkElement> tagElements = [];// 生成tooltip内容String textContent = '';// 选中的数据 ({date: xxx, name: 线条1, points: xxx}, {date: xx, name: 线条2, points: xx}...)final selectedTupleList = selectedTuples.values;// 选中的数据的字段名列表 [date, name, points]final fields = selectedTupleList.first.keys.toList();// 选中的数据的第一个数据的第一个字段的值,放入到tooltip的第一行/*** 目前的数据结构是:* 2023-11-24*/textContent = '${selectedTupleList.first[fields.first]}';// 遍历选中的数据(几条线几条数据),将每个数据的第二个字段和第三个字段的值放入到tooltip的第二行和第三行for (var original in selectedTupleList) {final domainField = fields[1];final measureField = fields.last;/*** 遍历结束后的数据结构是:* 2023-11-24* 线条1: 123* 线条2: 456* ....*/textContent += '\n  ${original[domainField]}: ${original[measureField]}';}// 提出一些变量const textStyle = TextStyle(fontSize: 12, color: Colors.black, height: 2);const padding = EdgeInsets.all(5);const align = Alignment.topRight;const offset = Offset(5, -5);const elevation = 1.0;const backgroundColor = Colors.white;final painter = TextPainter(text: TextSpan(text: textContent, style: textStyle),textDirection: ui.TextDirection.ltr,);painter.layout();// tooltip的宽高final width = padding.left + painter.width + padding.right;final height = padding.top + painter.height + padding.bottom;// tooltip的位置// 大概根据中间的数据判断算了下位置,避免一直在左或右,边界超出屏幕final move = anchor < const Offset(250, 90)? anchor + offset - const Offset(-10, -40): anchor + Offset(-width - 20, 40);final paintPoint = getBlockPaintPoint(move,width,height,align,);final window = Rect.fromLTWH(paintPoint.dx - 10, //横向位置paintPoint.dy,width + 20,height,);var textPaintPoint = paintPoint + padding.topLeft;// 生成tooltip线条前的标识元素for (int i = 0; i < selectedTupleList.length; i++) {tagElements.add(LabelElement(text: '●',anchor: textPaintPoint + padding.topLeft + Offset(-15, 26 + i * 23),style: LabelStyle(textStyle: TextStyle(color: Defaults.colors10[i],fontWeight: FontWeight.w900,fontSize: 12),align: Alignment.bottomRight)),);}elements = <MarkElement>[RectElement(rect: window,style: PaintStyle(fillColor: backgroundColor, elevation: elevation)),...tagElements,LabelElement(text: textContent,anchor: textPaintPoint,style:LabelStyle(textStyle: textStyle, align: Alignment.bottomRight)),];return elements;}@overrideWidget build(BuildContext context) {return SingleChildScrollView(child: Center(child: Column(children: <Widget>[Container(padding: const EdgeInsets.fromLTRB(20, 40, 20, 5),child: const Text('Smooth Line and Area chart',style: TextStyle(fontSize: 20),),),Container(margin: const EdgeInsets.only(top: 10),width: 350,height: 300,child: Chart(// 数据源data: invalidData,// 变量配置variables: {'date': Variable(accessor: (Map map) => map['date'] as String,scale: OrdinalScale(tickCount: 5, // x轴刻度数量),),'name': Variable(accessor: (Map map) => map['name'] as String,),'points': Variable(accessor: (Map map) => (map['points'] ?? double.nan) as num,),},marks: [// 线条LineMark(// 如果单线条加name则必须有position属性配置,否则是一条直线position:Varset('date') * Varset('points') / Varset('name'),shape: ShapeEncode(value: BasicLineShape(smooth: true),),// 粗细size: SizeEncode(value: 1.5),),// 线条与X轴之间区域填充AreaMark(// 如果单线条加name则必须有position属性配置,否则不显示position:Varset('date') * Varset('points') / Varset('name'),shape: ShapeEncode(value:BasicAreaShape(smooth: true), // smooth: true 使线条变得平滑),color: ColorEncode(value: Colors.pink.withAlpha(80),),),],// 坐标轴配置axes: [Defaults.horizontalAxis,Defaults.verticalAxis,],selections: {'touchMove': PointSelection(on: {GestureType.scaleUpdate,GestureType.tapDown,GestureType.hover,GestureType.longPressMoveUpdate},dim: Dim.x,)},// 触摸弹框提示tooltip: TooltipGuide(// 跟随鼠标位置// followPointer: [false, true],// align: Alignment.topLeft, // 弹框对于点击位置的对齐方式// offset: const Offset(-20, -20), // 偏移量// 使用自定义需要注释上面的一些配置renderer: simpleTooltip,),// 十字准线crosshair: CrosshairGuide(followPointer: [false, true]),),),Container(padding: const EdgeInsets.fromLTRB(20, 40, 20, 5),child: const Text('Group interactions',style: TextStyle(fontSize: 20),),),Container(margin: const EdgeInsets.only(top: 10),width: 350,height: 300,child: Chart(data: invalidData1,variables

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/177508.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年全国硕士研究生入学统一考试管理类专业学位联考数学试题——解析版

文章目录 一、问题求解&#xff1a;真题&#xff08;2023-01&#xff09;真题&#xff08;2023-02&#xff09;真题&#xff08;2023-03&#xff09;真题&#xff08;2023-04&#xff09;真题&#xff08;2023-05&#xff09;真题&#xff08;2023-06&#xff09;真题&#xff…

【密码学】【安全多方计算】浅析隐私求交PSI

文章目录 隐私求交的定义隐私求交方案介绍1. 基于DH的PSI方案2. 基于OT的PSI方案3.基于OPRF的PSI方案 总结 隐私求交的定义 隐私集合求交使得持有数据参与方通过计算得到集合的交集数据&#xff0c;而不泄露任何交集以外的数据信息。 隐私求交方案介绍 1. 基于DH的PSI方案 …

Aapche Dubbo 不安全的 Java 反序列化 (CVE-2019-17564)

漏洞描述 Apache Dubbo 是一个高性能的、基于 Java 的开源 RPC 框架。 Apache Dubbo 支持不同的协议&#xff0c;它的 HTTP 协议处理程序是 Spring Framework 的 .org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter Spring Framework 的安全警告显示&am…

计算机系统漫游

编译系统 预处理&#xff08;Preprocessing&#xff09;&#xff1a; 预处理器根据源代码中的预处理指令&#xff0c;如#include和#define等&#xff0c;将源代码转换为另一份源代码。预处理器的输出通常会保存在hello.i的文件中。编译&#xff08;Compilation&#xff09;&…

为什么要坚持每天做公域引流

做公域不一定等于拍视频&#xff0c;真正适合小白的引流方式其实还是做图文内容&#xff0c;比如小红书发帖&#xff0c;知乎问答&#xff0c;微信读书&#xff0c;问一问等。 长期坚持每天做公域引流有以下好处&#xff1a; 提高品牌知名度&#xff1a;每天坚持做公域流量&a…

nodejs+vue+elementui学生竞赛管理系统65o97

高校人才培养计划的重要组成部分&#xff0c;是实现人才培养目标、培养学生体育 能力与创新思维、学生竟赛管理系统检验学生综合素质与实践能力的重要手段与综合性实践教学环节。而我所在学院多采用半手工管理学生竟赛的方式&#xff0c;所以有必要开发学生竟赛管理系统来对学生…

2015年五一杯数学建模B题空气污染问题研究解题全过程文档及程序

2015年五一杯数学建模 B题 空气污染问题研究 原题再现 近十年来&#xff0c;我国 GDP 持续快速增长&#xff0c;但经济增长模式相对传统落后&#xff0c;对生态平衡和自然环境造成一定的破坏&#xff0c;空气污染的弊病日益突出&#xff0c;特别是日益加重的雾霾天气已经干扰…

前端管理制度

数据运营中心的管理形式&#xff1a; 数据运营中心的管理形式 竖向是各小组 横向是项目管理 负责人的定位&#xff1a; 只是工作的内容不同&#xff0c;没有上下级之分 帮助组员找到适合的位置&#xff0c;帮助大家解决问题&#xff0c;给大家提供资源 前端组的工作形式&am…

MySQL数据库如何实现跨服务器访问数据

点击上方蓝字关注我 在使用MySQL数据库时&#xff0c;很多同学经常会问&#xff0c;我能跨服务器访问另一库的数据么&#xff1f;得到的答案很多时候是让人失望的。那么如果真的需要访问&#xff0c;又不想使用拷贝表及数据的方式&#xff0c;可以实现么&#xff0c;又该如何实…

无货源商城搭建的几种方案(电商商品采集API)

随着电商行业的快速发展&#xff0c;越来越多的商家开始考虑搭建自己的电商商城。然而&#xff0c;对于一些没有自己的货源或者想要快速进入电商领域的商家来说&#xff0c;从哪里获取商品成为了他们面临的首要问题。本文将介绍几种无货源商城的搭建方案&#xff0c;帮助商家快…

【创建和排查隐藏进程和隐藏计划任务】

Window 创建隐藏进程和隐藏计划任务&#xff1a; 隐藏进程&#xff1a; 在Windows中&#xff0c;隐藏进程主要通过修改进程属性或使用第三方工具实现。以下是一个使用PowerShell脚本创建隐藏进程的示例&#xff1a; $Script {Start-Process -FilePath "notepad.exe"…

设二维数组a[1...m,1...n]()含有m*n个整数。写一个算法判断a中所有元素是否互不相同,并输出相关信息(yes/no)

设二维数组a[1…m&#xff0c;1…n]&#xff08;&#xff09;含有m*n个整数。 写一个算法判断a中所有元素是否互不相同&#xff0c;并输出相关信息&#xff08;yes/no) 分析其时间复杂度 代码思路&#xff1a; 这种如果纯暴力做的话时间复杂度非常高。 我这里考虑把题目中的二…

地理坐标系转换

1.EPSG代码 搜索地理坐标系对应的EPSG代码 https://epsg.io/ 常用的地理坐标系EPSG代码&#xff1a; 2. 坐标系转换 转换网址&#xff1a; https://epsg.io/transform &#xff08;1&#xff09;修改 input coordinate system 和 output coordinate system&#xff0c; 可以…

Zookeeper 实战 | Zookeeper 和Spring Cloud相结合解决分布式锁、服务注册与发现、配置管理

专栏集锦&#xff0c;大佬们可以收藏以备不时之需&#xff1a; Spring Cloud 专栏&#xff1a;http://t.csdnimg.cn/WDmJ9 Python 专栏&#xff1a;http://t.csdnimg.cn/hMwPR Redis 专栏&#xff1a;http://t.csdnimg.cn/Qq0Xc TensorFlow 专栏&#xff1a;http://t.csdni…

攻关眼科难题!第一届爱尔眼科-四川大学科研基金完成立项

当前我国眼科患者数量不断增长&#xff0c;人民群众对高质量的眼健康的需要不断攀升&#xff0c;而目前国内眼科医疗资源远不能满足需求&#xff0c;疑难眼病诊疗能力及学术科研体系建设仍有较大进步空间。基于此&#xff0c;爱尔眼科携手四川大学共同设立爱尔眼科-四川大学科研…

小程序----使用图表显示数据--canvas

需求&#xff1a;在小程序上实现数据可视化 思路&#xff1a;本来想用的是echarts或者相关的可视化插件&#xff0c;但因为用的是vue3&#xff0c;大多数插件不支持&#xff0c;所以用了echarts&#xff0c;但最后打包的时候说包太大超过2M无法上传&#xff0c;百度了一下&…

LeetCode Hot100 33.搜索旋转排序数组

题目&#xff1a; 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转&#xff0c;使数组变为 [nums[k], nums[k1], ..., nums[n-1], nu…

构建沉浸式 AI 文本编辑器:开源 3B 编辑器的设计原则与思路

借助于在 AutoDev 与 IDE 上的 AI 沉浸式体验设计&#xff0c;我们开始构建一个 AI 原生的文本编辑器&#xff0c;以探索沉浸式创作体验。其适用于需求编写、架构文档等等文档场景&#xff0c;以加速软件开发中的多种角色的日常工作。 GitHub&#xff1a;https://github.com/un…

【MySql】14- 实践篇(十二)-grant权限/分区表/自增Id用完怎么办

文章目录 1.grant之后要跟着flush privileges吗&#xff1f;1.1 全局权限1.2 db 权限1.3 表权限和列权限1.4 flush privileges 使用场景 2. 要不要使用分区表?2.1 分区表是什么?2.2 分区表的引擎层行为2.3 分区策略2.4 分区表的 server 层行为2.5 分区表的应用场景 3. 自增Id…

Python自动化办公:PDF文件的分割与合并

我们平时办公中&#xff0c;可能需要对pdf进行合并或者分割&#xff0c;但奈何没有可以白嫖的工具&#xff0c;此时python就是一个万能工具库。 其中PyPDF2是一个用于处理PDF文件的Python库&#xff0c;它提供了分割和合并PDF文件的功能。 在本篇博客中&#xff0c;我们将详细…