axis-renderer.ts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. /**
  2. * 坐标轴和图例绘制函数
  3. *
  4. * 从 u-charts 核心库搬迁的坐标轴和图例绘制相关函数
  5. * 用于处理X轴、Y轴、网格线和图例的绘制操作
  6. */
  7. // @ts-nocheck - 由于从 u-charts 搬迁,类型系统不兼容,暂时禁用类型检查
  8. import type { ChartOptions, UChartsConfig, SeriesItem } from '../data-processing/series-calculator';
  9. import { measureText } from '../utils/text';
  10. import { convertCoordinateOrigin } from '../utils/coordinate';
  11. import { hexToRgb } from '../utils/color';
  12. import { assign } from '../config';
  13. // Canvas 上下文类型(使用 any 以兼容小程序环境)
  14. export type CanvasContext = any;
  15. /**
  16. * 坐标点接口
  17. */
  18. export interface Point {
  19. x: number;
  20. y: number;
  21. }
  22. /**
  23. * 仪表盘选项接口
  24. */
  25. export interface GaugeOption {
  26. width?: number;
  27. labelOffset?: number;
  28. endAngle?: number;
  29. startAngle?: number;
  30. splitLine?: {
  31. splitNumber?: number;
  32. };
  33. endNumber?: number;
  34. startNumber?: number;
  35. formatter?: (val: number, index: number, opts: ChartOptions) => string;
  36. labelColor?: string;
  37. }
  38. /**
  39. * 雷达图选项接口
  40. */
  41. export interface RadarOption {
  42. labelPointShow?: boolean;
  43. labelPointColor?: string;
  44. labelPointRadius?: number;
  45. labelShow?: boolean;
  46. labelColor?: string;
  47. }
  48. /**
  49. * 图例数据项接口
  50. */
  51. export interface LegendItem {
  52. name: string;
  53. color: string;
  54. show?: boolean;
  55. legendShape?: string;
  56. legendText?: string;
  57. area?: number[];
  58. [key: string]: any;
  59. }
  60. /**
  61. * 图例数据接口
  62. */
  63. export interface LegendData {
  64. points: LegendItem[][];
  65. area: {
  66. start: Point;
  67. width: number;
  68. height: number;
  69. };
  70. widthArr: number[];
  71. heightArr: number[];
  72. }
  73. /**
  74. * 绘制X轴
  75. * @param categories - X轴分类数据
  76. * @param opts - 图表配置选项
  77. * @param config - uCharts配置对象
  78. * @param context - Canvas 渲染上下文
  79. */
  80. export function drawXAxis(
  81. categories: string[],
  82. opts: ChartOptions,
  83. config: UChartsConfig,
  84. context: CanvasContext
  85. ): void {
  86. let xAxisData = opts.chartData?.xAxisData;
  87. if (!xAxisData) return;
  88. let xAxisPoints = xAxisData.xAxisPoints;
  89. let startX = xAxisData.startX;
  90. let endX = xAxisData.endX;
  91. let eachSpacing = xAxisData.eachSpacing;
  92. let boundaryGap = 'center';
  93. if (opts.type == 'bar' || opts.type == 'line' || opts.type == 'area' || opts.type == 'scatter' || opts.type == 'bubble') {
  94. boundaryGap = opts.xAxis?.boundaryGap || 'center';
  95. }
  96. let startY = opts.height! - opts.area![2];
  97. let endY = opts.area![0];
  98. // 绘制滚动条
  99. if (opts.enableScroll && opts.xAxis?.scrollShow) {
  100. let scrollY = opts.height! - opts.area![2] + config.xAxisHeight;
  101. let scrollScreenWidth = endX - startX;
  102. let scrollTotalWidth = eachSpacing * (xAxisPoints.length - 1);
  103. if (opts.type == 'mount' && opts.extra?.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1) {
  104. let widthRatio = opts.extra.mount.widthRatio > 2 ? 2 : opts.extra.mount.widthRatio;
  105. scrollTotalWidth += (widthRatio - 1) * eachSpacing;
  106. }
  107. let scrollWidth = scrollScreenWidth * scrollScreenWidth / scrollTotalWidth;
  108. let scrollLeft = 0;
  109. if (opts._scrollDistance_) {
  110. scrollLeft = -opts._scrollDistance_ * (scrollScreenWidth) / scrollTotalWidth;
  111. }
  112. context.beginPath();
  113. context.setLineCap('round');
  114. context.setLineWidth(6 * opts.pix);
  115. context.setStrokeStyle(opts.xAxis.scrollBackgroundColor || "#EFEBEF");
  116. context.moveTo(startX, scrollY);
  117. context.lineTo(endX, scrollY);
  118. context.stroke();
  119. context.closePath();
  120. context.beginPath();
  121. context.setLineCap('round');
  122. context.setLineWidth(6 * opts.pix);
  123. context.setStrokeStyle(opts.xAxis.scrollColor || "#A6A6A6");
  124. context.moveTo(startX + scrollLeft, scrollY);
  125. context.lineTo(startX + scrollLeft + scrollWidth, scrollY);
  126. context.stroke();
  127. context.closePath();
  128. context.setLineCap('butt');
  129. }
  130. context.save();
  131. if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) {
  132. context.translate(opts._scrollDistance_, 0);
  133. }
  134. // 绘制X轴刻度线
  135. if (opts.xAxis?.calibration === true) {
  136. context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc");
  137. context.setLineCap('butt');
  138. context.setLineWidth(1 * opts.pix);
  139. xAxisPoints.forEach(function (item: number, index: number) {
  140. if (index > 0) {
  141. context.beginPath();
  142. context.moveTo(item - eachSpacing / 2, startY);
  143. context.lineTo(item - eachSpacing / 2, startY + 3 * opts.pix);
  144. context.closePath();
  145. context.stroke();
  146. }
  147. });
  148. }
  149. // 绘制X轴网格
  150. if (opts.xAxis && opts.xAxis.disableGrid !== true) {
  151. context.setStrokeStyle(opts.xAxis.gridColor || "#cccccc");
  152. context.setLineCap('butt');
  153. context.setLineWidth(1 * opts.pix);
  154. if (opts.xAxis.gridType == 'dash') {
  155. const dashLength = opts.xAxis.dashLength || 4;
  156. context.setLineDash([dashLength * opts.pix, dashLength * opts.pix]);
  157. }
  158. let gridEval = opts.xAxis.gridEval || 1;
  159. xAxisPoints.forEach(function (item: number, index: number) {
  160. if (index % gridEval == 0) {
  161. context.beginPath();
  162. context.moveTo(item, startY);
  163. context.lineTo(item, endY);
  164. context.stroke();
  165. }
  166. });
  167. context.setLineDash([]);
  168. }
  169. // 绘制X轴文案
  170. if (opts.xAxis && opts.xAxis.disabled !== true) {
  171. // 对X轴列表做抽稀处理
  172. // 默认全部显示X轴标签
  173. let maxXAxisListLength = categories.length;
  174. // 如果设置了X轴单屏数量
  175. if (opts.xAxis.labelCount) {
  176. // 如果设置X轴密度
  177. if (opts.xAxis.itemCount) {
  178. maxXAxisListLength = Math.ceil(categories.length / opts.xAxis.itemCount * opts.xAxis.labelCount);
  179. } else {
  180. maxXAxisListLength = opts.xAxis.labelCount;
  181. }
  182. maxXAxisListLength -= 1;
  183. }
  184. let ratio = Math.ceil(categories.length / maxXAxisListLength);
  185. let newCategories: string[] = [];
  186. let cgLength = categories.length;
  187. for (let i = 0; i < cgLength; i++) {
  188. if (i % ratio !== 0) {
  189. newCategories.push("");
  190. } else {
  191. newCategories.push(categories[i]);
  192. }
  193. }
  194. newCategories[cgLength - 1] = categories[cgLength - 1];
  195. let xAxisFontSize = (opts.xAxis!.fontSize || config.fontSize) * opts.pix;
  196. if (config._xAxisTextAngle_ === 0) {
  197. newCategories.forEach(function (item, index) {
  198. let xitem = opts.xAxis!.formatter ? opts.xAxis!.formatter!(item, index, opts) : item;
  199. let offset = -measureText(String(xitem), xAxisFontSize, context) / 2;
  200. if (boundaryGap == 'center') {
  201. offset += eachSpacing / 2;
  202. }
  203. let scrollHeight = 0;
  204. if (opts.xAxis!.scrollShow) {
  205. scrollHeight = 6 * opts.pix;
  206. }
  207. // 如果在主视图区域内
  208. let _scrollDistance_ = opts._scrollDistance_ || 0;
  209. let truePoints = boundaryGap == 'center' ? xAxisPoints[index] + eachSpacing / 2 : xAxisPoints[index];
  210. if ((truePoints - Math.abs(_scrollDistance_)) >= (opts.area![3] - 1) && (truePoints - Math.abs(_scrollDistance_)) <= (opts.width! - opts.area![1] + 1)) {
  211. context.beginPath();
  212. context.setFontSize(xAxisFontSize);
  213. context.setFillStyle(opts.xAxis!.fontColor || opts.fontColor);
  214. const marginTop = opts.xAxis!.marginTop || 0;
  215. const lineHeight = opts.xAxis!.lineHeight || config.fontSize;
  216. const xAxisFontSizeVal = opts.xAxis!.fontSize || config.fontSize;
  217. context.fillText(String(xitem), xAxisPoints[index] + offset, startY + marginTop * opts.pix + (lineHeight - xAxisFontSizeVal) * opts.pix / 2 + xAxisFontSizeVal * opts.pix);
  218. context.closePath();
  219. context.stroke();
  220. }
  221. });
  222. } else {
  223. newCategories.forEach(function (item, index) {
  224. let xitem = opts.xAxis!.formatter ? opts.xAxis!.formatter!(item, index, opts) : item;
  225. // 如果在主视图区域内
  226. let _scrollDistance_ = opts._scrollDistance_ || 0;
  227. let truePoints = boundaryGap == 'center' ? xAxisPoints[index] + eachSpacing / 2 : xAxisPoints[index];
  228. if ((truePoints - Math.abs(_scrollDistance_)) >= (opts.area![3] - 1) && (truePoints - Math.abs(_scrollDistance_)) <= (opts.width! - opts.area![1] + 1)) {
  229. context.save();
  230. context.beginPath();
  231. context.setFontSize(xAxisFontSize);
  232. context.setFillStyle(opts.xAxis!.fontColor || opts.fontColor);
  233. let textWidth = measureText(String(xitem), xAxisFontSize, context);
  234. let offsetX = xAxisPoints[index];
  235. if (boundaryGap == 'center') {
  236. offsetX = xAxisPoints[index] + eachSpacing / 2;
  237. }
  238. let scrollHeight = 0;
  239. if (opts.xAxis!.scrollShow) {
  240. scrollHeight = 6 * opts.pix;
  241. }
  242. const marginTop = opts.xAxis!.marginTop || 0;
  243. let offsetY = startY + marginTop * opts.pix + xAxisFontSize - xAxisFontSize * Math.abs(Math.sin(config._xAxisTextAngle_!));
  244. const rotateAngle = opts.xAxis!.rotateAngle || 0;
  245. if (rotateAngle < 0) {
  246. offsetX -= xAxisFontSize / 2;
  247. textWidth = 0;
  248. } else {
  249. offsetX += xAxisFontSize / 2;
  250. textWidth = -textWidth;
  251. }
  252. context.translate(offsetX, offsetY);
  253. context.rotate(-1 * config._xAxisTextAngle_!);
  254. context.fillText(String(xitem), textWidth, 0);
  255. context.closePath();
  256. context.stroke();
  257. context.restore();
  258. }
  259. });
  260. }
  261. }
  262. context.restore();
  263. // 画X轴标题
  264. if (opts.xAxis && opts.xAxis.title) {
  265. context.beginPath();
  266. const titleFontSize = opts.xAxis!.titleFontSize || config.fontSize;
  267. context.setFontSize(titleFontSize * opts.pix);
  268. context.setFillStyle(opts.xAxis!.titleFontColor!);
  269. const titleOffsetX = opts.xAxis!.titleOffsetX || 0;
  270. const marginTop = opts.xAxis!.marginTop || 0;
  271. const lineHeight = opts.xAxis!.lineHeight || titleFontSize;
  272. const titleOffsetY = opts.xAxis!.titleOffsetY || 0;
  273. context.fillText(String(opts.xAxis.title), opts.width! - opts.area![1] + titleOffsetX * opts.pix, opts.height! - opts.area![2] + marginTop * opts.pix + (lineHeight - titleFontSize) * opts.pix / 2 + (titleFontSize + titleOffsetY) * opts.pix);
  274. context.closePath();
  275. context.stroke();
  276. }
  277. // 绘制X轴轴线
  278. if (opts.xAxis && opts.xAxis.axisLine) {
  279. context.beginPath();
  280. context.setStrokeStyle(opts.xAxis!.axisLineColor!);
  281. context.setLineWidth(1 * opts.pix);
  282. context.moveTo(startX, opts.height! - opts.area![2]);
  283. context.lineTo(endX, opts.height! - opts.area![2]);
  284. context.stroke();
  285. }
  286. }
  287. /**
  288. * 绘制Y轴网格
  289. * @param categories - Y轴分类数据
  290. * @param opts - 图表配置选项
  291. * @param config - uCharts配置对象
  292. * @param context - Canvas 渲染上下文
  293. */
  294. export function drawYAxisGrid(
  295. categories: string[],
  296. opts: ChartOptions,
  297. config: UChartsConfig,
  298. context: CanvasContext
  299. ): void {
  300. if (opts.yAxis?.disableGrid === true) {
  301. return;
  302. }
  303. let spacingValid = opts.height! - opts.area![0] - opts.area![2];
  304. let eachSpacing = spacingValid / (opts.yAxis!.splitNumber || 5);
  305. let startX = opts.area![3];
  306. let xAxisPoints = opts.chartData?.xAxisData?.xAxisPoints || [];
  307. let xAxiseachSpacing = opts.chartData?.xAxisData?.eachSpacing || 0;
  308. let TotalWidth = xAxiseachSpacing * (xAxisPoints.length - 1);
  309. if (opts.type == 'mount' && opts.extra?.mount && opts.extra.mount.widthRatio && opts.extra.mount.widthRatio > 1) {
  310. let widthRatio = opts.extra.mount.widthRatio > 2 ? 2 : opts.extra.mount.widthRatio;
  311. TotalWidth += (widthRatio - 1) * xAxiseachSpacing;
  312. }
  313. let endX = startX + TotalWidth;
  314. let points: number[] = [];
  315. let startY = 1;
  316. if (opts.xAxis!.axisLine === false) {
  317. startY = 0;
  318. }
  319. for (let i = startY; i < (opts.yAxis!.splitNumber || 5) + 1; i++) {
  320. points.push(opts.height! - opts.area![2] - eachSpacing * i);
  321. }
  322. context.save();
  323. if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) {
  324. context.translate(opts._scrollDistance_, 0);
  325. }
  326. if (opts.yAxis!.gridType == 'dash') {
  327. context.setLineDash([(opts.yAxis!.dashLength || 4) * opts.pix, (opts.yAxis!.dashLength || 4) * opts.pix]);
  328. }
  329. context.setStrokeStyle(opts.yAxis!.gridColor || '#cccccc');
  330. context.setLineWidth(1 * opts.pix);
  331. points.forEach(function (item, index) {
  332. context.beginPath();
  333. context.moveTo(startX, item);
  334. context.lineTo(endX, item);
  335. context.stroke();
  336. });
  337. context.setLineDash([]);
  338. context.restore();
  339. }
  340. /**
  341. * 绘制Y轴
  342. * @param series - 系列数据数组
  343. * @param opts - 图表配置选项
  344. * @param config - uCharts配置对象
  345. * @param context - Canvas 渲染上下文
  346. */
  347. export function drawYAxis(
  348. series: SeriesItem[],
  349. opts: ChartOptions,
  350. config: UChartsConfig,
  351. context: CanvasContext
  352. ): void {
  353. if (opts.yAxis?.disabled === true) {
  354. return;
  355. }
  356. let spacingValid = opts.height! - opts.area![0] - opts.area![2];
  357. let eachSpacing = spacingValid / (opts.yAxis!.splitNumber || 5);
  358. let startX = opts.area![3];
  359. let endX = opts.width! - opts.area![1];
  360. let endY = opts.height! - opts.area![2];
  361. // set YAxis background
  362. context.beginPath();
  363. context.setFillStyle(opts.background || '#ffffff');
  364. if (opts.enableScroll == true && opts.xAxis!.scrollPosition && opts.xAxis!.scrollPosition !== 'left') {
  365. context.fillRect(0, 0, startX, endY + 2 * opts.pix);
  366. }
  367. if (opts.enableScroll == true && opts.xAxis!.scrollPosition && opts.xAxis!.scrollPosition !== 'right') {
  368. context.fillRect(endX, 0, opts.width!, endY + 2 * opts.pix);
  369. }
  370. context.closePath();
  371. context.stroke();
  372. let tStartLeft = opts.area![3];
  373. let tStartRight = opts.width! - opts.area![1];
  374. let tStartCenter = opts.area![3] + (opts.width! - opts.area![1] - opts.area![3]) / 2;
  375. if (opts.yAxis!.data) {
  376. for (let i = 0; i < opts.yAxis!.data.length; i++) {
  377. let yData = opts.yAxis!.data[i];
  378. let points: number[] = [];
  379. if (yData.type === 'categories') {
  380. for (let j = 0; j <= (yData.categories?.length || 0); j++) {
  381. points.push(opts.area![0] + spacingValid / (yData.categories!.length) / 2 + spacingValid / (yData.categories!.length) * j);
  382. }
  383. } else {
  384. for (let j = 0; j <= (opts.yAxis!.splitNumber || 5); j++) {
  385. points.push(opts.area![0] + eachSpacing * j);
  386. }
  387. }
  388. if (yData.disabled !== true) {
  389. let rangesFormat = opts.chartData?.yAxisData?.rangesFormat?.[i] || [];
  390. let yAxisFontSize = yData.fontSize ? yData.fontSize * opts.pix : config.fontSize;
  391. let yAxisWidth = opts.chartData?.yAxisData?.yAxisWidth?.[i];
  392. if (!yAxisWidth) continue;
  393. let textAlign = yData.textAlign || "right";
  394. // 画Y轴刻度及文案
  395. rangesFormat.forEach(function (item: any, index: any) {
  396. let pos = points[index];
  397. context.beginPath();
  398. context.setFontSize(yAxisFontSize);
  399. context.setLineWidth(1 * opts.pix);
  400. context.setStrokeStyle(yData.axisLineColor || '#cccccc');
  401. context.setFillStyle(yData.fontColor || opts.fontColor);
  402. let tmpstrat = 0;
  403. let gapwidth = 4 * opts.pix;
  404. if (yAxisWidth.position == 'left') {
  405. // 画刻度线
  406. if (yData.calibration == true) {
  407. context.moveTo(tStartLeft, pos);
  408. context.lineTo(tStartLeft - 3 * opts.pix, pos);
  409. gapwidth += 3 * opts.pix;
  410. }
  411. // 画文字
  412. switch (textAlign) {
  413. case "left":
  414. context.setTextAlign('left');
  415. tmpstrat = tStartLeft - yAxisWidth.width;
  416. break;
  417. case "right":
  418. context.setTextAlign('right');
  419. tmpstrat = tStartLeft - gapwidth;
  420. break;
  421. default:
  422. context.setTextAlign('center');
  423. tmpstrat = tStartLeft - yAxisWidth.width / 2;
  424. }
  425. context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
  426. } else if (yAxisWidth.position == 'right') {
  427. // 画刻度线
  428. if (yData.calibration == true) {
  429. context.moveTo(tStartRight, pos);
  430. context.lineTo(tStartRight + 3 * opts.pix, pos);
  431. gapwidth += 3 * opts.pix;
  432. }
  433. switch (textAlign) {
  434. case "left":
  435. context.setTextAlign('left');
  436. tmpstrat = tStartRight + gapwidth;
  437. break;
  438. case "right":
  439. context.setTextAlign('right');
  440. tmpstrat = tStartRight + yAxisWidth.width;
  441. break;
  442. default:
  443. context.setTextAlign('center');
  444. tmpstrat = tStartRight + yAxisWidth.width / 2;
  445. }
  446. context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
  447. } else if (yAxisWidth.position == 'center') {
  448. // 画刻度线
  449. if (yData.calibration == true) {
  450. context.moveTo(tStartCenter, pos);
  451. context.lineTo(tStartCenter - 3 * opts.pix, pos);
  452. gapwidth += 3 * opts.pix;
  453. }
  454. // 画文字
  455. switch (textAlign) {
  456. case "left":
  457. context.setTextAlign('left');
  458. tmpstrat = tStartCenter - yAxisWidth.width;
  459. break;
  460. case "right":
  461. context.setTextAlign('right');
  462. tmpstrat = tStartCenter - gapwidth;
  463. break;
  464. default:
  465. context.setTextAlign('center');
  466. tmpstrat = tStartCenter - yAxisWidth.width / 2;
  467. }
  468. context.fillText(String(item), tmpstrat, pos + yAxisFontSize / 2 - 3 * opts.pix);
  469. }
  470. context.closePath();
  471. context.stroke();
  472. context.setTextAlign('left');
  473. });
  474. // 画Y轴轴线
  475. if (yData.axisLine !== false) {
  476. context.beginPath();
  477. context.setStrokeStyle(yData.axisLineColor || '#cccccc');
  478. context.setLineWidth(1 * opts.pix);
  479. if (yAxisWidth.position == 'left') {
  480. context.moveTo(tStartLeft, opts.height! - opts.area![2]);
  481. context.lineTo(tStartLeft, opts.area![0]);
  482. } else if (yAxisWidth.position == 'right') {
  483. context.moveTo(tStartRight, opts.height! - opts.area![2]);
  484. context.lineTo(tStartRight, opts.area![0]);
  485. } else if (yAxisWidth.position == 'center') {
  486. context.moveTo(tStartCenter, opts.height! - opts.area![2]);
  487. context.lineTo(tStartCenter, opts.area![0]);
  488. }
  489. context.stroke();
  490. }
  491. // 画Y轴标题
  492. if (opts.yAxis!.showTitle) {
  493. let titleFontSize = (yData.titleFontSize || config.fontSize) * opts.pix;
  494. let title = yData.title || '';
  495. context.beginPath();
  496. context.setFontSize(titleFontSize);
  497. context.setFillStyle(yData.titleFontColor || opts.fontColor);
  498. if (yAxisWidth.position == 'left') {
  499. context.fillText(title, tStartLeft - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area![0] - (10 - (yData.titleOffsetY || 0)) * opts.pix);
  500. } else if (yAxisWidth.position == 'right') {
  501. context.fillText(title, tStartRight - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area![0] - (10 - (yData.titleOffsetY || 0)) * opts.pix);
  502. } else if (yAxisWidth.position == 'center') {
  503. context.fillText(title, tStartCenter - measureText(title, titleFontSize, context) / 2 + (yData.titleOffsetX || 0), opts.area![0] - (10 - (yData.titleOffsetY || 0)) * opts.pix);
  504. }
  505. context.closePath();
  506. context.stroke();
  507. }
  508. if (yAxisWidth.position == 'left') {
  509. tStartLeft -= (yAxisWidth.width + (opts.yAxis!.padding || 0) * opts.pix);
  510. } else {
  511. tStartRight += yAxisWidth.width + (opts.yAxis!.padding || 0) * opts.pix;
  512. }
  513. }
  514. }
  515. }
  516. }
  517. /**
  518. * 绘制图例
  519. * @param series - 系列数据数组
  520. * @param opts - 图表配置选项
  521. * @param config - uCharts配置对象
  522. * @param context - Canvas 渲染上下文
  523. * @param chartData - 图表数据对象
  524. */
  525. export function drawLegend(
  526. series: SeriesItem[],
  527. opts: ChartOptions,
  528. config: UChartsConfig,
  529. context: CanvasContext,
  530. chartData: any
  531. ): void {
  532. if (opts.legend?.show === false) {
  533. return;
  534. }
  535. let legendData = chartData.legendData as LegendData;
  536. let legendList = legendData.points;
  537. let legendArea = legendData.area;
  538. let padding = (opts.legend.padding || 5) * opts.pix;
  539. let fontSize = (opts.legend.fontSize || 12) * opts.pix;
  540. let shapeWidth = 15 * opts.pix;
  541. let shapeRight = 5 * opts.pix;
  542. let itemGap = (opts.legend.itemGap || 10) * opts.pix;
  543. let lineHeight = Math.max((opts.legend.lineHeight || 15) * opts.pix, fontSize);
  544. // 画背景及边框
  545. context.beginPath();
  546. context.setLineWidth((opts.legend.borderWidth || 0) * opts.pix);
  547. context.setStrokeStyle(opts.legend.borderColor || '#cccccc');
  548. context.setFillStyle(opts.legend.backgroundColor || '#ffffff');
  549. context.moveTo(legendArea.start.x, legendArea.start.y);
  550. context.rect(legendArea.start.x, legendArea.start.y, legendArea.width, legendArea.height);
  551. context.closePath();
  552. context.fill();
  553. context.stroke();
  554. legendList.forEach(function (itemList, listIndex) {
  555. let width = 0;
  556. let height = 0;
  557. width = legendData.widthArr[listIndex];
  558. height = legendData.heightArr[listIndex];
  559. let startX = 0;
  560. let startY = 0;
  561. if (opts.legend.position == 'top' || opts.legend.position == 'bottom') {
  562. switch (opts.legend.float) {
  563. case 'left':
  564. startX = legendArea.start.x + padding;
  565. break;
  566. case 'right':
  567. startX = legendArea.start.x + legendArea.width - width;
  568. break;
  569. default:
  570. startX = legendArea.start.x + (legendArea.width - width) / 2;
  571. }
  572. startY = legendArea.start.y + padding + listIndex * lineHeight;
  573. } else {
  574. if (listIndex == 0) {
  575. width = 0;
  576. } else {
  577. width = legendData.widthArr[listIndex - 1];
  578. }
  579. startX = legendArea.start.x + padding + width;
  580. startY = legendArea.start.y + padding + (legendArea.height - height) / 2;
  581. }
  582. context.setFontSize(config.fontSize);
  583. for (let i = 0; i < itemList.length; i++) {
  584. let item = itemList[i];
  585. item.area = [0, 0, 0, 0];
  586. item.area[0] = startX;
  587. item.area[1] = startY;
  588. item.area[3] = startY + lineHeight;
  589. context.beginPath();
  590. context.setLineWidth(1 * opts.pix);
  591. context.setStrokeStyle(item.show !== false ? item.color : (opts.legend.hiddenColor || '#999999'));
  592. context.setFillStyle(item.show !== false ? item.color : (opts.legend.hiddenColor || '#999999'));
  593. switch (item.legendShape) {
  594. case 'line':
  595. context.moveTo(startX, startY + 0.5 * lineHeight - 2 * opts.pix);
  596. context.fillRect(startX, startY + 0.5 * lineHeight - 2 * opts.pix, 15 * opts.pix, 4 * opts.pix);
  597. break;
  598. case 'triangle':
  599. context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
  600. context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix);
  601. context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix);
  602. context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
  603. break;
  604. case 'diamond':
  605. context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
  606. context.lineTo(startX + 2.5 * opts.pix, startY + 0.5 * lineHeight);
  607. context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight + 5 * opts.pix);
  608. context.lineTo(startX + 12.5 * opts.pix, startY + 0.5 * lineHeight);
  609. context.lineTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
  610. break;
  611. case 'circle':
  612. context.moveTo(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight);
  613. context.arc(startX + 7.5 * opts.pix, startY + 0.5 * lineHeight, 5 * opts.pix, 0, 2 * Math.PI);
  614. break;
  615. case 'rect':
  616. context.moveTo(startX, startY + 0.5 * lineHeight - 5 * opts.pix);
  617. context.fillRect(startX, startY + 0.5 * lineHeight - 5 * opts.pix, 15 * opts.pix, 10 * opts.pix);
  618. break;
  619. case 'square':
  620. context.moveTo(startX + 5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix);
  621. context.fillRect(startX + 5 * opts.pix, startY + 0.5 * lineHeight - 5 * opts.pix, 10 * opts.pix, 10 * opts.pix);
  622. break;
  623. case 'none':
  624. break;
  625. default:
  626. context.moveTo(startX, startY + 0.5 * lineHeight - 5 * opts.pix);
  627. context.fillRect(startX, startY + 0.5 * lineHeight - 5 * opts.pix, 15 * opts.pix, 10 * opts.pix);
  628. }
  629. context.closePath();
  630. context.fill();
  631. context.stroke();
  632. startX += shapeWidth + shapeRight;
  633. let fontTrans = 0.5 * lineHeight + 0.5 * fontSize - 2;
  634. const legendText = item.legendText ? item.legendText : item.name;
  635. context.beginPath();
  636. context.setFontSize(fontSize);
  637. context.setFillStyle(item.show !== false ? (opts.legend.fontColor || '#666666') : (opts.legend.hiddenColor || '#999999'));
  638. context.fillText(legendText, startX, startY + fontTrans);
  639. context.closePath();
  640. context.stroke();
  641. if (opts.legend.position == 'top' || opts.legend.position == 'bottom') {
  642. startX += measureText(legendText, fontSize, context) + itemGap;
  643. item.area[2] = startX;
  644. } else {
  645. item.area[2] = startX + measureText(legendText, fontSize, context) + itemGap;
  646. ;
  647. startX -= shapeWidth + shapeRight;
  648. startY += lineHeight;
  649. }
  650. }
  651. });
  652. }
  653. /**
  654. * 绘制仪表盘标签
  655. * @param gaugeOption - 仪表盘选项
  656. * @param radius - 半径
  657. * @param centerPosition - 中心点坐标
  658. * @param opts - 图表配置选项
  659. * @param config - uCharts配置对象
  660. * @param context - Canvas 渲染上下文
  661. */
  662. export function drawGaugeLabel(
  663. gaugeOption: GaugeOption,
  664. radius: number,
  665. centerPosition: Point,
  666. opts: ChartOptions,
  667. config: UChartsConfig,
  668. context: CanvasContext
  669. ): void {
  670. radius -= (gaugeOption.width || 0) / 2 + (gaugeOption.labelOffset || 0) * opts.pix;
  671. radius = radius < 10 ? 10 : radius;
  672. let totalAngle: number;
  673. if ((gaugeOption.endAngle || 0) < (gaugeOption.startAngle || 0)) {
  674. totalAngle = 2 + (gaugeOption.endAngle || 0) - (gaugeOption.startAngle || 0);
  675. } else {
  676. totalAngle = (gaugeOption.startAngle || 0) - (gaugeOption.endAngle || 0);
  677. }
  678. let splitAngle = totalAngle / (gaugeOption.splitLine?.splitNumber || 5);
  679. let totalNumber = (gaugeOption.endNumber || 10) - (gaugeOption.startNumber || 0);
  680. let splitNumber = totalNumber / (gaugeOption.splitLine?.splitNumber || 5);
  681. let nowAngle = gaugeOption.startAngle || 0;
  682. let nowNumber = gaugeOption.startNumber || 0;
  683. for (let i = 0; i < (gaugeOption.splitLine?.splitNumber || 5) + 1; i++) {
  684. let pos = {
  685. x: radius * Math.cos(nowAngle * Math.PI),
  686. y: radius * Math.sin(nowAngle * Math.PI)
  687. };
  688. let labelText = gaugeOption.formatter ? gaugeOption.formatter(nowNumber, i, opts) : String(nowNumber);
  689. pos.x += centerPosition.x - measureText(labelText, config.fontSize, context) / 2;
  690. pos.y += centerPosition.y;
  691. let startX = pos.x;
  692. let startY = pos.y;
  693. context.beginPath();
  694. context.setFontSize(config.fontSize);
  695. context.setFillStyle(gaugeOption.labelColor || opts.fontColor);
  696. context.fillText(labelText, startX, startY + config.fontSize / 2);
  697. context.closePath();
  698. context.stroke();
  699. nowAngle += splitAngle;
  700. if (nowAngle >= 2) {
  701. nowAngle = nowAngle % 2;
  702. }
  703. nowNumber += splitNumber;
  704. }
  705. }
  706. /**
  707. * 绘制雷达图标签
  708. * @param angleList - 角度列表
  709. * @param radius - 半径
  710. * @param centerPosition - 中心点坐标
  711. * @param opts - 图表配置选项
  712. * @param config - uCharts配置对象
  713. * @param context - Canvas 渲染上下文
  714. */
  715. export function drawRadarLabel(
  716. angleList: number[],
  717. radius: number,
  718. centerPosition: Point,
  719. opts: ChartOptions,
  720. config: UChartsConfig,
  721. context: CanvasContext
  722. ): void {
  723. let radarOption = opts.extra?.radar || {};
  724. angleList.forEach(function (angle, index) {
  725. if (radarOption.labelPointShow === true && opts.categories && opts.categories[index] !== '') {
  726. let posPoint = {
  727. x: radius * Math.cos(angle),
  728. y: radius * Math.sin(angle)
  729. };
  730. let posPointAxis = convertCoordinateOrigin(posPoint.x, posPoint.y, centerPosition);
  731. context.setFillStyle(radarOption.labelPointColor || opts.fontColor);
  732. context.beginPath();
  733. context.arc(posPointAxis.x, posPointAxis.y, (radarOption.labelPointRadius || 3) * opts.pix, 0, 2 * Math.PI, false);
  734. context.closePath();
  735. context.fill();
  736. }
  737. if (radarOption.labelShow === true && opts.categories) {
  738. let pos = {
  739. x: (radius + config.radarLabelTextMargin * opts.pix) * Math.cos(angle),
  740. y: (radius + config.radarLabelTextMargin * opts.pix) * Math.sin(angle)
  741. };
  742. let posRelativeCanvas = convertCoordinateOrigin(pos.x, pos.y, centerPosition);
  743. let startX = posRelativeCanvas.x;
  744. let startY = posRelativeCanvas.y;
  745. if (Math.abs(pos.x) < 1e-10) {
  746. startX -= measureText(opts.categories[index] || '', config.fontSize, context) / 2;
  747. } else if (pos.x < 0) {
  748. startX -= measureText(opts.categories[index] || '', config.fontSize, context);
  749. }
  750. context.beginPath();
  751. context.setFontSize(config.fontSize);
  752. context.setFillStyle(radarOption.labelColor || opts.fontColor);
  753. context.fillText(opts.categories[index] || '', startX, startY + config.fontSize / 2);
  754. context.closePath();
  755. context.stroke();
  756. }
  757. });
  758. }