SignalTimingChart.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. <template>
  2. <div ref="chartRef" class="chart-container"></div>
  3. </template>
  4. <script>
  5. import * as echarts from 'echarts';
  6. import echartsResize from '@/mixins/echartsResize.js';
  7. // 模块级共享定时器:所有实例订阅同一个 tick,保证扫描线完全同步
  8. let sharedEpoch = 0;
  9. let sharedTimer = null;
  10. let sharedListeners = new Set();
  11. let sharedBatchId = 0; // 批次ID,切换分页时递增
  12. function joinSharedTimer(listener, batchId) {
  13. // 新批次:重置定时器和 epoch
  14. if (batchId !== sharedBatchId) {
  15. sharedBatchId = batchId;
  16. if (sharedTimer) { clearInterval(sharedTimer); sharedTimer = null; }
  17. sharedListeners.clear();
  18. sharedEpoch = Math.floor(Date.now() / 1000);
  19. }
  20. if (!sharedTimer) {
  21. sharedEpoch = Math.floor(Date.now() / 1000);
  22. sharedTimer = setInterval(() => {
  23. const elapsed = Math.floor(Date.now() / 1000) - sharedEpoch;
  24. sharedListeners.forEach(fn => fn(elapsed));
  25. }, 1000);
  26. }
  27. sharedListeners.add(listener);
  28. // 立即触发一次,避免 mounted 到首次 tick 之间的闪跳
  29. listener(Math.floor(Date.now() / 1000) - sharedEpoch);
  30. }
  31. function leaveSharedTimer(listener) {
  32. sharedListeners.delete(listener);
  33. if (sharedListeners.size === 0 && sharedTimer) {
  34. clearInterval(sharedTimer);
  35. sharedTimer = null;
  36. sharedEpoch = 0;
  37. }
  38. }
  39. const COLORS = {
  40. GREEN_LIGHT: '#8dc453', GREEN_DARK: '#73a542', YELLOW: '#fbd249', RED: '#ff7575', STRIPE_GREEN: '#a3d76e',
  41. TEXT_DARK: '#1e2638', AXIS_LINE: '#758599', DIVIDER_LINE: '#111827', MARK_BLUE: '#00E5FF', TEXT_LIGHT: '#d1d5db'
  42. };
  43. // 绘制条纹图案用于绿闪/预警
  44. const stripeCanvas = document.createElement('canvas');
  45. stripeCanvas.width = 6; stripeCanvas.height = 20;
  46. const ctx = stripeCanvas.getContext('2d');
  47. ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 6, 20);
  48. ctx.fillStyle = COLORS.STRIPE_GREEN; ctx.fillRect(0, 0, 3, 20);
  49. const stripePattern = { image: stripeCanvas, repeat: 'repeat' };
  50. const IMAGE_MAP = {
  51. 'STRAIGHT_DOWN': require('@/assets/images/icon_straight_down.png'),
  52. 'TURN_DOWN_LEFT': require('@/assets/images/icon_turn_down_left.png'),
  53. 'TURN_DOWN_LEFT_UTURN': require('@/assets/images/icon_turn_down_left_uturn.png'),
  54. 'STRAIGHT_UP': require('@/assets/images/icon_straight_up.png'),
  55. 'TURN_UP_LEFT': require('@/assets/images/icon_turn_up_left.png'),
  56. 'TURN_UP_LEFT_UTURN': require('@/assets/images/icon_turn_up_left_uturn.png'),
  57. 'STRAIGHT_LEFT': require('@/assets/images/icon_straight_left.png'),
  58. 'TURN_LEFT_DOWN': require('@/assets/images/icon_turn_left_down.png'),
  59. 'TURN_LEFT_DOWN_UTURN': require('@/assets/images/icon_turn_left_down_uturn.png'),
  60. 'STRAIGHT_RIGHT': require('@/assets/images/icon_straight_right.png'),
  61. 'TURN_RIGHT_UP': require('@/assets/images/icon_turn_right_up.png'),
  62. 'TURN_RIGHT_UP_UTURN': require('@/assets/images/icon_turn_right_up_uturn.png' )
  63. };
  64. // ==========================================
  65. // 核心逻辑:基于真实物理空间的对齐与自定义偏移/尺寸配置
  66. // pos: 位置(LT/RT/LB/RB), padX/padY: 基础像素偏移, baseW/baseH: 基础原始宽高
  67. // ==========================================
  68. const POS_MAP = {
  69. // 1. 上方驶入 -> 靠左上角 (LT)
  70. 'STRAIGHT_DOWN': { pos: 'LT', padX: 10, padY: 0, baseW: 7, baseH: 20.67 },
  71. 'TURN_DOWN_LEFT': { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },
  72. 'TURN_DOWN_LEFT_UTURN': { pos: 'LT', padX: 10, padY: 0, baseW: 13, baseH: 22.67 },
  73. // 2. 下方驶入 -> 靠右下角 (RB)
  74. 'STRAIGHT_UP': { pos: 'RB', padX: 10, padY: 0, baseW: 7, baseH: 20.67 },
  75. 'TURN_UP_LEFT': { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 21.33 },
  76. 'TURN_UP_LEFT_UTURN': { pos: 'RB', padX: 10, padY: 0, baseW: 13, baseH: 22.67 },
  77. // 3. 右侧驶入 -> 靠右上角 (RT)
  78. 'STRAIGHT_LEFT': { pos: 'RT', padX: 0, padY: 10, baseW: 20.33, baseH: 6.33 },
  79. 'TURN_LEFT_DOWN': { pos: 'RT', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },
  80. 'TURN_LEFT_DOWN_UTURN': { pos: 'RT', padX: 0, padY: 10, baseW: 22.67, baseH: 12.33 },
  81. // 4. 左侧驶入 -> 靠左下角 (LB)
  82. 'STRAIGHT_RIGHT': { pos: 'LB', padX: 0, padY: 10, baseW: 20.33, baseH: 6.33 },
  83. 'TURN_RIGHT_UP': { pos: 'LB', padX: 0, padY: 10, baseW: 20.67, baseH: 12.33 },
  84. 'TURN_RIGHT_UP_UTURN': { pos: 'LB', padX: 0, padY: 10, baseW: 22.67, baseH: 12.33 },
  85. };
  86. export default {
  87. name: 'SignalTimingChart',
  88. mixins: [echartsResize],
  89. props: {
  90. cycleLength: { type: Number, default: 140 },
  91. currentTime: { type: Number, default: 0 },
  92. phaseData: { type: Array, default: () => [] },
  93. showAxis: { type: Boolean, default: true },
  94. showScanLine: { type: Boolean, default: true },
  95. showScanLineLabel: { type: Boolean, default: true },
  96. autoScan: { type: Boolean, default: false },
  97. syncScan: { type: Boolean, default: false }
  98. },
  99. data() {
  100. return { scaleFactor: 1, internalTime: 0 };
  101. },
  102. computed: {
  103. activeTime() {
  104. return this.autoScan ? this.internalTime : this.currentTime;
  105. }
  106. },
  107. mounted() {
  108. this.internalTime = this.currentTime;
  109. this.initChart();
  110. if (this.autoScan) this.startAutoScan();
  111. },
  112. beforeDestroy() {
  113. this.stopAutoScan();
  114. },
  115. watch: {
  116. currentTime(val) {
  117. if (!this.autoScan) {
  118. if (this.$_chart) this.updateScanLine();
  119. } else {
  120. // 页切换时 currentTime 变化,重新加入共享定时器触发 epoch 重置
  121. this.startAutoScan();
  122. }
  123. },
  124. autoScan(val) {
  125. if (val) { this.startAutoScan(); } else { this.stopAutoScan(); }
  126. },
  127. syncScan() {
  128. if (this.autoScan) { this.startAutoScan(); }
  129. },
  130. showScanLine(val) {
  131. this.updateChart();
  132. if (val && this.autoScan) { this.startAutoScan(); }
  133. },
  134. phaseData: { deep: true, handler(newVal) { if (this.$_chart && newVal.length > 0) this.updateChart(); } },
  135. showAxis() { this.updateChart(); },
  136. showScanLineLabel() { this.updateChart(); }
  137. },
  138. methods: {
  139. updateScale() {
  140. const el = this.$el;
  141. if (!el) return;
  142. this.scaleFactor = Math.max(0.5, el.clientWidth / 600);
  143. },
  144. startAutoScan() {
  145. this.stopAutoScan();
  146. const VISUAL_PERIOD = 120;
  147. if (this.syncScan) {
  148. // 共享定时器:所有行扫描线完全同步
  149. this._scanListener = (elapsed) => {
  150. const realMax = this.getMaxTime();
  151. const offset = this.currentTime || 0;
  152. const ratio = ((offset + elapsed) % VISUAL_PERIOD) / VISUAL_PERIOD;
  153. this.internalTime = ratio * realMax;
  154. if (this.$_chart) this.updateScanLine();
  155. this.$emit('scan-tick', this.internalTime);
  156. };
  157. joinSharedTimer(this._scanListener, this.currentTime || 0);
  158. } else {
  159. // 独立定时器:每行扫描线从 currentTime 位置开始独立移动
  160. this.internalTime = this.currentTime || 0;
  161. this._soloTimer = setInterval(() => {
  162. const realMax = this.getMaxTime();
  163. this.internalTime += 1;
  164. if (this.internalTime > realMax) this.internalTime = 0;
  165. if (this.$_chart) this.updateScanLine();
  166. this.$emit('scan-tick', this.internalTime);
  167. }, 1000);
  168. }
  169. },
  170. stopAutoScan() {
  171. if (this._scanListener) { leaveSharedTimer(this._scanListener); this._scanListener = null; }
  172. if (this._soloTimer) { clearInterval(this._soloTimer); this._soloTimer = null; }
  173. },
  174. initChart() {
  175. const chartDom = this.$refs.chartRef;
  176. if (!chartDom) return;
  177. this.updateScale();
  178. this.$_chart = echarts.init(chartDom);
  179. if (this.phaseData.length > 0) this.updateChart();
  180. },
  181. updateChart() {
  182. if (!this.$_chart) return;
  183. this.updateScale();
  184. this.$_chart.setOption(this.getChartOption(), true);
  185. },
  186. updateScanLine() {
  187. if (!this.$_chart) return;
  188. this.updateScale();
  189. const s = this.scaleFactor;
  190. const realMaxTime = this.getMaxTime();
  191. this.$_chart.setOption({
  192. series: [{
  193. markLine: !this.showScanLine ? false : {
  194. symbol: ['none', 'none'],
  195. silent: true,
  196. animation: false,
  197. label: {
  198. show: this.showScanLineLabel,
  199. position: 'start',
  200. formatter: `${Math.round(this.activeTime)}/${realMaxTime}`,
  201. color: '#fff', backgroundColor: COLORS.MARK_BLUE,
  202. padding: [Math.round(4 * s), Math.round(8 * s)],
  203. borderRadius: 2, fontSize: Math.max(10, Math.round(10 * s)),
  204. offset: [0, Math.round(1 * s)]
  205. },
  206. lineStyle: { color: COLORS.MARK_BLUE, type: 'solid', width: Math.max(2, Math.round(5 * s)), z: 100 },
  207. data: [{ xAxis: this.activeTime }]
  208. }
  209. }]
  210. });
  211. },
  212. getMaxTime() {
  213. if (!this.phaseData || this.phaseData.length === 0) return this.cycleLength;
  214. const maxDataTime = Math.max(...this.phaseData.map(item => item[2]));
  215. return Math.max(this.cycleLength, maxDataTime);
  216. },
  217. getChartOption() {
  218. const s = this.scaleFactor;
  219. const isTwoRows = this.phaseData.some(item => item[0] === 1);
  220. const yAxisData = isTwoRows ? ['Track 0', 'Track 1'] : ['Track 0'];
  221. const realMaxTime = this.getMaxTime();
  222. return {
  223. backgroundColor: 'transparent',
  224. grid: {
  225. left: 0, right: 0,
  226. // 当隐藏坐标轴/扫描线时(即在表格中显示时),将上下边距设为 0,让色块铺满高度
  227. top: (this.showAxis || this.showScanLineLabel) ? Math.round(35 * s) : 0,
  228. bottom: (this.showAxis || this.showScanLineLabel) ? Math.round(10 * s) : 0,
  229. containLabel: false
  230. },
  231. xAxis: { type: 'value', min: 0, max: realMaxTime, show: false },
  232. yAxis: { type: 'category', data: yAxisData, inverse: true, show: false },
  233. series: [{
  234. type: 'custom',
  235. renderItem: (params, api) => this.renderCustomItem(params, api, isTwoRows, realMaxTime),
  236. encode: { x: [1, 2], y: 0 },
  237. data: this.phaseData,
  238. markLine: !this.showScanLine ? false : {
  239. symbol: ['none', 'none'],
  240. silent: true,
  241. animation: false,
  242. label: {
  243. show: this.showScanLineLabel,
  244. position: 'start', formatter: `${Math.round(this.activeTime)}/${realMaxTime}`,
  245. color: '#fff', backgroundColor: COLORS.MARK_BLUE, padding: [Math.round(4 * s), Math.round(8 * s)],
  246. borderRadius: 2, fontSize: Math.max(10, Math.round(10 * s)),
  247. offset: [0, Math.round(1 * s)]
  248. },
  249. lineStyle: { color: COLORS.MARK_BLUE, type: 'solid', width: Math.max(2, Math.round(5 * s)), z: 100 },
  250. data: [ { xAxis: this.activeTime } ]
  251. }
  252. }]
  253. };
  254. },
  255. renderCustomItem(params, api, isTwoRows, realMaxTime) {
  256. const s = this.scaleFactor;
  257. const trackIndex = api.value(0);
  258. const start = api.coord([api.value(1), trackIndex]);
  259. const end = api.coord([api.value(2), trackIndex]);
  260. const blockHeight = api.size([0, 1])[1];
  261. const yPos = start[1] - blockHeight / 2;
  262. const blockWidth = end[0] - start[0];
  263. const phaseName = api.value(3);
  264. const duration = api.value(4);
  265. const type = api.value(5);
  266. const iconValue = api.value(6);
  267. let fillStyle = COLORS.GREEN_LIGHT;
  268. if (type === 'stripe') fillStyle = stripePattern;
  269. else if (type === 'yellow') fillStyle = COLORS.YELLOW;
  270. else if (type === 'red') fillStyle = COLORS.RED;
  271. const rectShape = echarts.graphic.clipRectByRect(
  272. { x: start[0], y: yPos, width: blockWidth, height: blockHeight },
  273. { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }
  274. );
  275. if (!rectShape) return;
  276. const children = [];
  277. // A. 绘制阶段刻度 (S1, S2...)
  278. if (params.dataIndex === 0 && this.showAxis) {
  279. const axisBaseY = params.coordSys.y - Math.round(15 * s);
  280. const track0Data = this.phaseData.filter(item => item[0] === 0);
  281. let stagePoints = track0Data.filter(item => item[5] === 'green').map(item => item[1]);
  282. if (!stagePoints.includes(0)) stagePoints.unshift(0);
  283. stagePoints.push(realMaxTime);
  284. stagePoints = Array.from(new Set(stagePoints)).sort((a, b) => a - b);
  285. stagePoints.forEach(val => {
  286. const x = api.coord([val, 0])[0];
  287. children.push({ type: 'line', shape: { x1: x, y1: axisBaseY - Math.round(5 * s), x2: x, y2: axisBaseY + Math.round(5 * s) }, style: { stroke: COLORS.AXIS_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
  288. });
  289. for (let i = 0; i < stagePoints.length - 1; i++) {
  290. const startX = api.coord([stagePoints[i], 0])[0];
  291. const endX = api.coord([stagePoints[i + 1], 0])[0];
  292. const midX = (startX + endX) / 2;
  293. const textHalf = Math.round(14 * s);
  294. children.push({ type: 'line', shape: { x1: startX, y1: axisBaseY, x2: midX - textHalf, y2: axisBaseY }, style: { stroke: COLORS.AXIS_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
  295. children.push({ type: 'line', shape: { x1: midX + textHalf, y1: axisBaseY, x2: endX, y2: axisBaseY }, style: { stroke: COLORS.AXIS_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
  296. children.push({ type: 'text', style: { text: `S${i + 1}`, x: midX, y: axisBaseY, fill: COLORS.TEXT_LIGHT, fontSize: Math.max(10, Math.round(14 * s)), align: 'center', verticalAlign: 'middle', fontWeight: 'bold' } });
  297. }
  298. }
  299. // B. 画色块背景
  300. children.push({ type: 'rect', shape: rectShape, style: { fill: fillStyle, stroke: 'none' } });
  301. // C. 绘制内部图标与文本
  302. // 提取基础缩放率
  303. const baseFs = Math.max(0.8, s * 0.9);
  304. // 只要宽度大于 5 像素就尝试去渲染(原版限制是 > 15,改小以支持极限压缩)
  305. if (type === 'green' && blockWidth > 5) {
  306. // --- 1. 将 iconValue 统一解析为数组,提前判断需要多宽的背景 ---
  307. let iconList = [];
  308. if (Array.isArray(iconValue)) {
  309. iconList = iconValue;
  310. } else if (typeof iconValue === 'string' && iconValue.trim() !== '') {
  311. iconList = iconValue.split(',');
  312. } else if (iconValue) {
  313. iconList = [iconValue];
  314. }
  315. // 判断是否有靠左(LT/LB)和靠右(RT/RB)的图标
  316. let hasL = false, hasR = false;
  317. iconList.forEach(icon => {
  318. const valStr = String(icon).trim().toUpperCase();
  319. const posConfig = POS_MAP[valStr] || { pos: 'RB' };
  320. const pos = typeof posConfig === 'string' ? posConfig : (posConfig.pos || 'RB');
  321. if (pos.includes('L')) hasL = true;
  322. if (pos.includes('R')) hasR = true;
  323. });
  324. // 核心:动态赋予深绿色区域的基础宽度!
  325. // 如果左右都有图标,给46宽度;如果只有一侧有,收缩到28;啥都没给8
  326. let idealDarkWidthBase = (hasL && hasR) ? 46 : (iconList.length > 0 ? 28 : 0);
  327. if (idealDarkWidthBase === 0 && phaseName) idealDarkWidthBase = 8;
  328. // 计算当前缩放下,理想状态需要的总像素宽度
  329. let idealDarkWidth = idealDarkWidthBase * baseFs;
  330. let idealTextWidth = 26 * baseFs; // 预留给 "P1\n24" 这类文本的宽度
  331. let totalNeededWidth = idealDarkWidth + 6 * baseFs + idealTextWidth;
  332. // --- 2. 计算动态弹性缩放率 (如果外部方块太小,内部按比例整体缩小) ---
  333. let innerScale = 1;
  334. if (blockWidth < totalNeededWidth) {
  335. // 最极限缩小到 15%,防止变成一个点引发渲染错误
  336. innerScale = Math.max(0.15, blockWidth / totalNeededWidth);
  337. }
  338. // 应用弹性缩放
  339. const dynamicFs = baseFs * innerScale;
  340. const darkWidth = idealDarkWidthBase * dynamicFs;
  341. const midY = yPos + blockHeight / 2;
  342. const pointerW = 4 * dynamicFs; // 中间那个小三角指针的大小也跟着缩放
  343. const innerGroup = {
  344. type: 'group',
  345. // 用 clipPath 限制死边界,防止文字或图标因为四舍五入溢出色块
  346. clipPath: { type: 'rect', shape: { x: start[0], y: yPos, width: blockWidth, height: blockHeight } },
  347. children: [
  348. { type: 'rect', shape: { x: start[0], y: yPos, width: darkWidth, height: blockHeight }, style: { fill: COLORS.GREEN_DARK } },
  349. {
  350. type: 'polygon',
  351. shape: { points: [
  352. [start[0] + darkWidth, midY - pointerW],
  353. [start[0] + darkWidth, midY + pointerW],
  354. [start[0] + darkWidth + pointerW, midY]
  355. ] },
  356. style: { fill: COLORS.GREEN_DARK }
  357. }
  358. ]
  359. };
  360. // --- 3. 绘制内部图标 ---
  361. iconList.forEach(icon => {
  362. const valStr = String(icon).trim().toUpperCase();
  363. const posConfig = POS_MAP[valStr] || { pos: 'RB', padX: 0, padY: 0, baseW: 20, baseH: 20 };
  364. const pos = typeof posConfig === 'string' ? posConfig : (posConfig.pos || 'RB');
  365. // 图标尺寸和边距也应用了 dynamicFs 动态缩放
  366. const drawW = Math.round((posConfig.baseW || 20) * dynamicFs);
  367. const drawH = Math.round((posConfig.baseH || 20) * dynamicFs);
  368. const padX = Math.round((posConfig.padX || 0) * dynamicFs);
  369. const padY = Math.round((posConfig.padY || 0) * dynamicFs);
  370. let iconX, iconY;
  371. if (pos === 'LT') {
  372. iconX = start[0] + padX;
  373. iconY = yPos + padY;
  374. } else if (pos === 'RT') {
  375. iconX = start[0] + darkWidth - drawW - padX;
  376. iconY = yPos + padY;
  377. } else if (pos === 'LB') {
  378. iconX = start[0] + padX;
  379. iconY = yPos + blockHeight - drawH - padY;
  380. } else { // RB
  381. iconX = start[0] + darkWidth - drawW - padX;
  382. iconY = yPos + blockHeight - drawH - padY;
  383. }
  384. if (IMAGE_MAP[valStr]) {
  385. innerGroup.children.push({
  386. type: 'image',
  387. style: {
  388. image: IMAGE_MAP[valStr],
  389. x: iconX,
  390. y: iconY,
  391. width: drawW,
  392. height: drawH,
  393. objectFit: 'contain'
  394. }
  395. });
  396. }
  397. });
  398. // --- 4. 渲染右侧文字 (相位号与时长) ---
  399. // 彻底移除 8px 的硬性下限兜底,让字体完全跟随 dynamicFs 比例等比缩小
  400. const fontSize = Math.max(1, 12 * dynamicFs);
  401. // 计算文本起点的X坐标
  402. const textStartX = start[0] + darkWidth + pointerW + (2 * dynamicFs);
  403. // 如果剩余空间大于 0(有哪怕一丁点空间),才进行文字渲染
  404. if (blockWidth > (darkWidth + pointerW + 2)) {
  405. innerGroup.children.push({
  406. type: 'text',
  407. style: {
  408. text: `${phaseName}\n${duration}`,
  409. x: textStartX,
  410. y: midY,
  411. fill: COLORS.TEXT_DARK,
  412. fontSize: fontSize,
  413. fontWeight: 'bold',
  414. align: 'left',
  415. verticalAlign: 'middle',
  416. // 行高也严格跟随动态字号
  417. lineHeight: fontSize * 1.2
  418. }
  419. });
  420. }
  421. children.push(innerGroup);
  422. }
  423. // D. 轨道分割线 (仅在两排模式下 Track 1 顶部绘制)
  424. if (isTwoRows && trackIndex === 1) {
  425. const dividerY = (api.coord([0, 1])[1] + api.coord([0, 0])[1]) / 2;
  426. children.push({ type: 'line', shape: { x1: start[0], y1: dividerY, x2: end[0], y2: dividerY }, style: { stroke: COLORS.DIVIDER_LINE, lineWidth: Math.max(1, Math.round(1.5 * s)) } });
  427. }
  428. return { type: 'group', children: children };
  429. }
  430. }
  431. };
  432. </script>
  433. <style scoped>
  434. .chart-container {
  435. width: 100%; height: 100%; flex: 1; min-height: 0; overflow: hidden;
  436. }
  437. </style>