tui-charts-line.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. <template>
  2. <view class="tui-charts__line-wrap" :style="{width:width+'rpx'}">
  3. <view class="tui-line__legend" v-if="legend.show">
  4. <view class="tui-line__legend-item" v-for="(item,index) in dataset" :key="index">
  5. <view class="tui-line__legend-circle" :style="{backgroundColor:item.color}"></view>
  6. <text
  7. :style="{fontSize:(legend.size || 24)+'rpx',lineHeight:(legend.size || 24)+'rpx',color:legend.color || '#333'}">{{item.name}}</text>
  8. </view>
  9. </view>
  10. <view class="tui-charts__line-box" v-if="xAxis.length>0 && dataset.length>0" :style="{width:width+'rpx'}">
  11. <scroll-view :scroll-x="scrollable" class="tui-line__scroll-view" :style="{height:scrollViewH+'rpx'}">
  12. <view :style="{height:(xAxisVal.height || 48) +'rpx'}"></view>
  13. <view class="tui-charts__line" :style="{height:height+'rpx'}">
  14. <view class="tui-line__item" :class="{'tui-line__flex-1':!scrollable}"
  15. :style="{width:(xAxisLine.itemGap || 120)+'rpx'}" v-for="(item,index) in xAxis" :key="index">
  16. <view class="tui-line__xAxis-text"
  17. :style="{color:xAxisLabel.color || '#333',fontSize:(xAxisLabel.size || 24)+'rpx' }">
  18. {{item}}
  19. </view>
  20. <view class="tui-yAxis__split-line"
  21. :style="{borderRightStyle:yAxisSplitLine.type || 'dashed',borderRightColor:yAxisSplitLine.color || '#e3e3e3'}"
  22. v-if="tooltipShow && index==activeIdx">
  23. </view>
  24. <view class="tui-xAxis__tickmarks"
  25. :style="{height:xAxisTick.height || '12rpx',backgroundColor:xAxisTick.color || '#e3e3e3'}">
  26. </view>
  27. </view>
  28. <view v-for="(dot,i) in dots" :key="dot.id">
  29. <view class="tui-charts__line-dot"
  30. :class="{'tui-charts__dot-enlarge':tooltipShow && j==activeIdx}" @tap.stop="dotClick(i,j)"
  31. v-for="(d,j) in dot.source" :key="d.id"
  32. :style="{bottom: d.y+'rpx', left: d.x+'rpx',width:(brokenDot.width || 12)+'rpx',height:(brokenDot.width || 12)+'rpx',borderColor:dot.color || brokenDot.color,background:brokenDot.color || dot.color}">
  33. <text class="tui-line__val"
  34. :style="{fontSize:(xAxisVal.size || 24)+'rpx',color:xAxisVal.color}"
  35. v-if="xAxisVal.show">
  36. {{getYAxisVal(i,j)}}
  37. </text>
  38. </view>
  39. </view>
  40. <view v-for="(line,idx) in lines" :key="line.id">
  41. <view class="tui-charts__broken-line" v-for="(l,k) in line.source" :key="l.id"
  42. :style="{height:brokenLineHeight+'px',background:line.color,bottom: l.y+'rpx', left: l.x+'rpx',width: l.width+'rpx','-webkit-transform': `rotate(${l.angle}deg)`,transform: `rotate(${l.angle}deg)`}">
  43. </view>
  44. </view>
  45. </view>
  46. </scroll-view>
  47. <view class="tui-line__border-left"
  48. :style="{height:height+(xAxisVal.height || 48)+'rpx',backgroundColor:yAxisLine.color || '#e3e3e3'}">
  49. </view>
  50. <view class="tui-xAxis__line" :class="{'tui-line__first':index===0}"
  51. :style="{bottom:index*(yAxisLine.itemGap || 60)+(xAxisLabel.height || 60)+'rpx',borderTopStyle:index===0?'solid':splitLine.type,borderTopColor:index===0?xAxisLine.color:splitLine.color}"
  52. v-for="(item,index) in yAxisData" :key="index">
  53. <text class="tui-yAxis__val"
  54. :style="{color:item.color || yAxisLabel.color,fontSize:(yAxisLabel.size||24)+'rpx'}"
  55. v-if="yAxisLabel.show">{{item.value}}</text>
  56. </view>
  57. </view>
  58. <view class="tui-line__tooltip" v-if="tooltip" :class="{'tui-line__tooltip-show':tooltipShow}">
  59. <view class="tui-tooltip__title">{{xAxis[activeIdx] || ''}}</view>
  60. <view class="tui-line__tooltip-item" v-for="(item,index) in tooltips" :key="index">
  61. <view class="tui-line__legend-circle" :style="{backgroundColor:item.color}"></view>
  62. <text class="tui-tooltip__val">{{item.name}}</text>
  63. <text class="tui-tooltip__val tui-tooltip__val-ml">{{item.val}}</text>
  64. </view>
  65. </view>
  66. </view>
  67. </template>
  68. <script>
  69. export default {
  70. name: "tui-charts-line",
  71. emits: ['click'],
  72. props: {
  73. //图表宽度
  74. width: {
  75. type: [Number, String],
  76. default: 620
  77. },
  78. //图例,说明
  79. legend: {
  80. type: Object,
  81. default () {
  82. return {
  83. show: false,
  84. size: 24,
  85. color: '#333'
  86. }
  87. }
  88. },
  89. tooltip: {
  90. type: Boolean,
  91. default: false
  92. },
  93. xAxis: {
  94. type: Array,
  95. default () {
  96. return []
  97. }
  98. },
  99. //默认选中x轴索引
  100. currentIndex: {
  101. type: Number,
  102. default: -1
  103. },
  104. //分割线
  105. splitLine: {
  106. type: Object,
  107. default () {
  108. return {
  109. //分割线颜色,不显示则将颜色设置为transparent
  110. color: "#e3e3e3",
  111. type: "dashed"
  112. }
  113. }
  114. },
  115. //x轴刻度线
  116. xAxisTick: {
  117. type: Object,
  118. default () {
  119. return {
  120. height: '12rpx',
  121. //不显示则将颜色设置为transparent
  122. color: '#e3e3e3'
  123. }
  124. }
  125. },
  126. //x轴线条
  127. xAxisLine: {
  128. type: Object,
  129. default () {
  130. return {
  131. color: '#e3e3e3',
  132. //x轴item间距 rpx
  133. itemGap: 120
  134. }
  135. }
  136. },
  137. xAxisLabel: {
  138. type: Object,
  139. default () {
  140. return {
  141. color: "#333",
  142. size: 24,
  143. height: 60
  144. }
  145. }
  146. },
  147. xAxisVal: {
  148. type: Object,
  149. default () {
  150. return {
  151. show: true,
  152. color: "#333",
  153. size: 24,
  154. //如果show为true且val显示的时候,height需要设置一定的值保证val能显示完整 rpx
  155. height: 48
  156. }
  157. }
  158. },
  159. //点击坐标点所显示的分割线
  160. yAxisSplitLine: {
  161. type: Object,
  162. default () {
  163. return {
  164. //分割线颜色,不显示则将颜色设置为transparent
  165. color: "transparent",
  166. type: "dashed"
  167. }
  168. }
  169. },
  170. //折线坐标点宽度 rpx
  171. brokenDot: {
  172. type: Object,
  173. default () {
  174. return {
  175. width: 12,
  176. //点的背景色
  177. color: '#F8F8F8'
  178. }
  179. }
  180. },
  181. //折线高度/粗细 px
  182. brokenLineHeight: {
  183. type: [Number, String],
  184. default: 1
  185. },
  186. //y轴数据,如果不传则默认使用min,max值计算
  187. // {
  188. // value: 0,
  189. // color: "#333"
  190. // }
  191. yAxis: {
  192. type: Array,
  193. default () {
  194. return []
  195. }
  196. },
  197. //y轴最小值
  198. min: {
  199. type: Number,
  200. default: 0
  201. },
  202. //y轴最大值
  203. max: {
  204. type: Number,
  205. default: 100
  206. },
  207. //y轴分段递增数值
  208. splitNumber: {
  209. type: Number,
  210. default: 20
  211. },
  212. yAxisLine: {
  213. type: Object,
  214. default () {
  215. return {
  216. //不显示则将颜色设置为transparent
  217. color: '#e3e3e3',
  218. //y轴item间距 rpx
  219. itemGap: 60
  220. }
  221. }
  222. },
  223. yAxisLabel: {
  224. type: Object,
  225. default () {
  226. return {
  227. show: true,
  228. color: "#333",
  229. size: 24
  230. }
  231. }
  232. },
  233. //是否可滚动
  234. scrollable: {
  235. type: Boolean,
  236. default: false
  237. }
  238. },
  239. data() {
  240. return {
  241. height: 0,
  242. scrollViewH: 0,
  243. sections: 0,
  244. yAxisData: [],
  245. activeIndex: -1,
  246. activeIdx: -1,
  247. tooltips: [],
  248. tooltipShow: false,
  249. timer: null,
  250. dots: [],
  251. lines: [],
  252. /*========options============*/
  253. /*
  254. name: '',
  255. color: '',
  256. source: []
  257. colorFormatter:Function
  258. */
  259. dataset: [],
  260. xAxisValFormatter: null,
  261. maxValue: 1
  262. };
  263. },
  264. created() {
  265. this.init()
  266. this.activeIdx = this.currentIndex;
  267. },
  268. // #ifndef VUE3
  269. beforeDestroy() {
  270. this.clearTimer()
  271. },
  272. // #endif
  273. // #ifdef VUE3
  274. beforeUnmount() {
  275. this.clearTimer()
  276. },
  277. // #endif
  278. methods: {
  279. getYAxisVal(idx, index) {
  280. let showVal = this.dataset[idx].source[index];
  281. if (this.xAxisVal.formatter && typeof this.xAxisVal.formatter === 'function') {
  282. showVal = this.xAxisVal.formatter(showVal)
  283. } else if (this.xAxisValFormatter && typeof this.xAxisValFormatter === 'function') {
  284. showVal = this.xAxisValFormatter(showVal)
  285. }
  286. return showVal
  287. },
  288. generateArray(start, end) {
  289. return Array.from(new Array(end + 1).keys()).slice(start);
  290. },
  291. getValue(val) {
  292. return val < 0 ? 0 : val;
  293. },
  294. getCoordinatePoint() {
  295. const xAxis = [...this.xAxis];
  296. const xSections = xAxis.length;
  297. const ySections = this.yAxisData.length - 1;
  298. const itemGap = this.scrollable ? (this.xAxisLine.itemGap || 120) : (this.width / xSections);
  299. let dots = [];
  300. let radius = (this.brokenDot.width || 12) / 2;
  301. this.dataset.map((item, index) => {
  302. let source = item.source || []
  303. let dotArr = []
  304. source.map((val, idx) => {
  305. dotArr.push({
  306. id: 'd' + idx,
  307. x: this.getValue((0.5 + idx) * itemGap - radius),
  308. y: this.getValue((val - this.min) / (this.maxValue - this.min) * (this
  309. .yAxisLine
  310. .itemGap || 60) *
  311. ySections - radius)
  312. })
  313. })
  314. dots.push({
  315. id: 'dd' + index,
  316. color: item.color,
  317. source: dotArr
  318. })
  319. })
  320. this.dots = dots;
  321. this.drawLines(dots);
  322. },
  323. drawLines(dots) {
  324. let lines = []
  325. // dots是点的集合 : Array<{ x: number; y: number; }>
  326. let radius = (this.brokenDot.width || 12) / 2;
  327. dots.map((item, idx) => {
  328. let dotArr = item.source;
  329. let lineArr = [];
  330. dotArr.map((dot, index) => {
  331. // 最后一个点没有连线
  332. if (!dotArr[index + 1]) return;
  333. const AB = {
  334. x: dotArr[index + 1].x - dot.x,
  335. y: dotArr[index + 1].y - dot.y,
  336. y1: dot.y - dotArr[index + 1].y
  337. }
  338. // 向量的模
  339. const v = Math.sqrt(Math.pow(AB.x, 2) + Math.pow(AB.y, 2));
  340. // 求出偏转角度
  341. const angle = Math.atan2(AB.y1, AB.x) * (180 / Math.PI);
  342. lineArr.push({
  343. id: 'l' + index,
  344. x: dot.x + radius,
  345. y: dot.y + radius - 1,
  346. width: v,
  347. angle: AB.y1 > 0 ? Math.sqrt(Math.pow(angle, 2)) : -Math.sqrt(Math.pow(
  348. angle,
  349. 2))
  350. })
  351. })
  352. lines.push({
  353. id: 'll' + idx,
  354. color: item.color,
  355. source: lineArr
  356. })
  357. })
  358. this.lines = lines
  359. },
  360. init() {
  361. this.maxValue = this.max;
  362. let itemGap = this.yAxisLine.itemGap || 60;
  363. let sections = this.yAxis.length - 1;
  364. let yAxis = this.yAxis;
  365. if (sections <= 0) {
  366. sections = Math.ceil((this.max - this.min) / this.splitNumber)
  367. let sectionsArr = this.generateArray(0, sections)
  368. yAxis = sectionsArr.map(item => {
  369. return {
  370. value: item * this.splitNumber + this.min
  371. }
  372. })
  373. this.maxValue = yAxis[yAxis.length - 1].value
  374. }
  375. this.yAxisData = yAxis;
  376. this.sections = sections + 1;
  377. this.height = itemGap * sections;
  378. const valH = this.xAxisVal.height || 48;
  379. this.scrollViewH = this.height + (this.xAxisLabel.height || 60) + valH;
  380. this.getCoordinatePoint();
  381. },
  382. /*
  383. dataset:折线图表数据
  384. xAxisValFormatter :格式化折线拐点value值(此处传值是为了做兼容处理)
  385. */
  386. draw(dataset, xAxisValFormatter) {
  387. this.xAxisValFormatter = xAxisValFormatter || null;
  388. this.dataset = dataset || [];
  389. this.init();
  390. },
  391. clearTimer() {
  392. clearTimeout(this.timer)
  393. this.timer = null;
  394. },
  395. tooltipHandle(index) {
  396. let data = [...this.dataset]
  397. let tooltips = []
  398. data.forEach(item => {
  399. let color = item.color;
  400. if (item.colorFormatter && typeof item.colorFormatter === 'function') {
  401. color = item.colorFormatter(item.source[index])
  402. }
  403. tooltips.push({
  404. color: color,
  405. name: item.name,
  406. val: item.source[index]
  407. })
  408. })
  409. this.tooltips = tooltips;
  410. this.clearTimer()
  411. this.tooltipShow = true;
  412. this.timer = setTimeout(() => {
  413. this.tooltipShow = false
  414. }, 5000)
  415. },
  416. dotClick(index, idx) {
  417. this.activeIndex = index;
  418. this.activeIdx = idx;
  419. this.tooltipHandle(idx);
  420. this.$emit('click', {
  421. datasetIndex: index,
  422. sourceIndex: idx,
  423. ...this.dataset[index]
  424. })
  425. }
  426. }
  427. }
  428. </script>
  429. <style scoped>
  430. .tui-charts__line-wrap {
  431. position: relative;
  432. transform: rotate(0deg) scale(1);
  433. /* margin: 0 auto; */
  434. }
  435. .tui-line__legend {
  436. display: flex;
  437. align-items: center;
  438. flex-wrap: wrap;
  439. }
  440. .tui-line__legend-item {
  441. display: flex;
  442. align-items: center;
  443. margin-left: 24rpx;
  444. margin-bottom: 30rpx;
  445. }
  446. .tui-line__legend-circle {
  447. height: 20rpx;
  448. width: 20rpx;
  449. border-radius: 50%;
  450. margin-right: 8rpx;
  451. flex-shrink: 0;
  452. }
  453. .tui-charts__line-box {
  454. position: relative;
  455. padding-left: 1px;
  456. box-sizing: border-box;
  457. transform-origin: 0 0;
  458. overflow: visible;
  459. transform: scale(1);
  460. }
  461. .tui-line__scroll-view {
  462. position: relative;
  463. z-index: 10;
  464. box-sizing: border-box;
  465. }
  466. .tui-charts__line {
  467. min-width: 100%;
  468. position: relative;
  469. display: flex;
  470. align-items: flex-end;
  471. /* overflow: hidden; */
  472. transform: rotate(0deg) scale(1);
  473. }
  474. .tui-line__between {
  475. justify-content: space-between;
  476. }
  477. .tui-line__item {
  478. height: 100%;
  479. display: flex;
  480. align-items: flex-end;
  481. justify-content: center;
  482. position: relative;
  483. text-align: center;
  484. box-sizing: border-box;
  485. z-index: 10;
  486. transition: all 0.3s;
  487. flex-shrink: 0;
  488. }
  489. .tui-line__flex-1 {
  490. flex: 1;
  491. }
  492. .tui-xAxis__tickmarks {
  493. position: absolute;
  494. right: 0;
  495. width: 1px;
  496. transform: translateY(100%);
  497. bottom: 0;
  498. }
  499. .tui-yAxis__split-line {
  500. position: absolute;
  501. height: 100%;
  502. width: 0;
  503. border-right-width: 1px;
  504. left: 50%;
  505. transform: translateX(-50%);
  506. z-index: 20;
  507. }
  508. .tui-line__xAxis-text {
  509. width: 100%;
  510. position: absolute;
  511. left: 50%;
  512. bottom: 0;
  513. flex: 1;
  514. transform: translate(-50%, 100%);
  515. padding-top: 8rpx;
  516. word-break: break-all;
  517. }
  518. .tui-line__border-left {
  519. position: absolute;
  520. left: 0;
  521. top: 0;
  522. width: 1px;
  523. z-index: 11;
  524. }
  525. .tui-xAxis__line {
  526. width: 100%;
  527. height: 0;
  528. border-top-width: 1px;
  529. position: absolute;
  530. left: 0;
  531. display: flex;
  532. align-items: center;
  533. }
  534. .tui-line__first {
  535. z-index: 12;
  536. }
  537. .tui-yAxis__val {
  538. transform: translateX(-100%);
  539. padding-right: 12rpx;
  540. -webkit-backface-visibility: hidden;
  541. backface-visibility: hidden;
  542. -webkit-font-smoothing: antialiased;
  543. }
  544. .tui-charts__line-dot {
  545. position: absolute;
  546. border-radius: 50%;
  547. transition: all 0.3s;
  548. z-index: 12;
  549. border-width: 1px;
  550. border-style: solid;
  551. box-sizing: border-box;
  552. /* #ifdef H5 */
  553. cursor: pointer;
  554. /* #endif */
  555. }
  556. .tui-line__val {
  557. width: 100%;
  558. position: absolute;
  559. top: 0;
  560. left: 50%;
  561. padding-bottom: 12rpx;
  562. transform: translate(-50%, -100%);
  563. -webkit-backface-visibility: hidden;
  564. backface-visibility: hidden;
  565. -webkit-font-smoothing: antialiased;
  566. white-space: nowrap;
  567. z-index: 20;
  568. }
  569. .tui-charts__dot-enlarge {
  570. transform: scale(1.4);
  571. }
  572. .tui-charts__broken-line {
  573. position: absolute;
  574. transform-origin: 0 0;
  575. transition: all 0.3s;
  576. z-index: 10;
  577. border-color: transparent;
  578. box-sizing: border-box;
  579. /* transform: translateZ(0); */
  580. /* -webkit-backface-visibility:hidden; */
  581. }
  582. .tui-line__tooltip {
  583. padding: 30rpx;
  584. border-radius: 12rpx;
  585. background-color: rgba(0, 0, 0, .6);
  586. display: inline-block;
  587. position: absolute;
  588. top: 50%;
  589. left: 50%;
  590. transform: translate(-50%, -50%);
  591. z-index: 20;
  592. visibility: hidden;
  593. opacity: 0;
  594. transition: all 0.3s;
  595. }
  596. .tui-line__tooltip-show {
  597. visibility: visible;
  598. opacity: 1;
  599. }
  600. .tui-tooltip__title {
  601. font-size: 30rpx;
  602. color: #fff;
  603. line-height: 30rpx;
  604. }
  605. .tui-line__tooltip-item {
  606. display: flex;
  607. align-items: center;
  608. padding-top: 24rpx;
  609. white-space: nowrap;
  610. }
  611. .tui-tooltip__val {
  612. font-size: 24rpx;
  613. line-height: 24rpx;
  614. color: #fff;
  615. margin-left: 6rpx;
  616. }
  617. .tui-tooltip__val-ml {
  618. margin-left: 20rpx;
  619. }
  620. </style>