demo01
v1无接口无坐标轴测试
TSAreaLine.qml
javascript
复制代码
import QtQuick 2.12
Item {
id: lineChart
// ===================== 外部可配置属性 ======================
property real xMin: 0
property real xMax: 10
property real yMin: 0
property real yMax: 100
property string xLabel: "时间"
property string xUnit: "秒"
property string yLabel: "数值"
property string yUnit: ""
property int xTicks: 10
property int yTicks: 10
property int textSize: 12
property bool dynamicMargin: true
property real baseMargin: 40
property var lineConfigs: []
// 全局基础透明度提高到0.9
property real fillOpacityBase: 0.9
// 底部更透(0.1),顶部不透(1.0)
property real topOpacityRatio: 1.0
property real bottomOpacityRatio: 0.1
// 新增:顶部颜色亮度提升比例(0.1=10%)
property real topColorBrightnessRatio: 0.1
// 默认填充样式(仅保留颜色,透明度由垂直渐变控制)
property var defaultFillStyle: {
"gradientStart": "#00e5ff",
"gradientEnd": "#7c4dff",
"gradientDirection": "vertical"
}
property color lineStrokeColor: "#ffffff"
property real lineStrokeWidth: 1.5
property int pointRadius: 3
// ===================== 内部属性 ======================
property color axisColor: "#333333"
property color gridColor: "#e0e0e0"
property real actualLeftMargin: baseMargin
property real actualRightMargin: 20
property real actualTopMargin: 20
property real actualBottomMargin: baseMargin
// ===================== 外部接口 ======================
function setAxisConfig(config) {
lineChart.xMin = config.xMin !== undefined ? config.xMin : lineChart.xMin;
lineChart.xMax = config.xMax !== undefined ? config.xMax : lineChart.xMax;
lineChart.yMin = config.yMin !== undefined ? config.yMin : lineChart.yMin;
lineChart.yMax = config.yMax !== undefined ? config.yMax : lineChart.yMax;
lineChart.xLabel = config.xLabel || lineChart.xLabel;
lineChart.xUnit = config.xUnit || lineChart.xUnit;
lineChart.yLabel = config.yLabel || lineChart.yLabel;
lineChart.yUnit = config.yUnit || lineChart.yUnit;
lineChart.xTicks = config.xTicks !== undefined ? config.xTicks : lineChart.xTicks;
lineChart.yTicks = config.yTicks !== undefined ? config.yTicks : lineChart.yTicks;
updateChart();
}
function setLineConfigs(configs) {
lineChart.lineConfigs = configs || [];
updateChart();
}
function setAllLineFillStyles(style) {
for (let i = 0; i < lineConfigs.length; i++) {
lineConfigs[i].fillStyle = Object.assign({}, lineConfigs[i].fillStyle || {}, style);
}
updateChart();
}
function setLineFillStyle(index, style) {
if (index >= 0 && index < lineConfigs.length) {
lineConfigs[index].fillStyle = Object.assign({}, lineConfigs[index].fillStyle || {}, style);
updateChart();
}
}
function setAllLineFillOpacity(opacity) {
lineChart.fillOpacityBase = Math.max(0, Math.min(1, opacity));
updateChart();
}
function setVerticalOpacityRatio(topRatio, bottomRatio) {
lineChart.topOpacityRatio = Math.max(0, Math.min(1, topRatio));
lineChart.bottomOpacityRatio = Math.max(0, Math.min(1, bottomRatio));
updateChart();
}
// 新增:设置顶部颜色亮度
function setTopColorBrightness(ratio) {
lineChart.topColorBrightnessRatio = Math.max(-1, Math.min(1, ratio));
updateChart();
}
function updateChart() {
calculateDynamicMargin();
chartCanvas.requestPaint();
}
// ===================== 内部工具函数 ======================
function calculateDynamicMargin() {
if (!dynamicMargin) {
actualLeftMargin = baseMargin;
actualBottomMargin = baseMargin;
return;
}
let ctx = chartCanvas.getContext("2d");
ctx.font = textSize + "px Arial";
let maxYTickTextWidth = ctx.measureText(yMax.toString()).width;
let yLabelWithUnit = yLabel + (yUnit ? "(" + yUnit + ")" : "");
let yLabelWidth = ctx.measureText(yLabelWithUnit).width;
actualLeftMargin = baseMargin + maxYTickTextWidth + yLabelWidth/2 + 10;
let maxXTickTextHeight = textSize + 5;
let xLabelWithUnit = xLabel + (xUnit ? "(" + xUnit + ")" : "");
let xLabelHeight = textSize + 5;
actualBottomMargin = baseMargin + maxXTickTextHeight + xLabelHeight + 5;
let maxMargin = Math.min(width/4, height/4);
actualLeftMargin = Math.min(actualLeftMargin, maxMargin);
actualBottomMargin = Math.min(actualBottomMargin, maxMargin);
}
function dataToCanvasX(x) {
let xRange = xMax - xMin;
let canvasWidth = chartCanvas.width - actualLeftMargin - actualRightMargin;
return actualLeftMargin + (x - xMin) * (canvasWidth / xRange);
}
function dataToCanvasY(y) {
let yRange = yMax - yMin;
let canvasHeight = chartCanvas.height - actualTopMargin - actualBottomMargin;
return chartCanvas.height - actualBottomMargin - (y - yMin) * (canvasHeight / yRange);
}
// 工具函数:十六进制颜色转RGBA
function hexToRgba(hex, alpha) {
hex = hex.replace(/^#/, '');
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
// 新增:调整颜色亮度(核心!实现顶部变亮)
function adjustColorBrightness(hex, brightness) {
hex = hex.replace(/^#/, '');
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
// 调整亮度:brightness>0变亮,<0变暗,范围-1到1
r = Math.round(Math.min(255, Math.max(0, r + r * brightness)));
g = Math.round(Math.min(255, Math.max(0, g + g * brightness)));
b = Math.round(Math.min(255, Math.max(0, b + b * brightness)));
// 转回十六进制
let toHex = (n) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// ===================== Canvas绘制(核心修改)=====================
Canvas {
id: chartCanvas
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
onPaint: {
let ctx = getContext("2d")
ctx.reset()
let w = width
let h = height
// 清空画布
ctx.fillStyle = "#1e1e1e"
ctx.fillRect(0, 0, w, h)
if (xMax <= xMin || yMax <= yMin) {
return;
}
if (lineConfigs.length > 0) {
// 逆序绘制折线
for (let lineIdx = lineConfigs.length - 1; lineIdx >= 0; lineIdx--) {
let lineCfg = lineConfigs[lineIdx]
let dataPoints = lineCfg.dataPoints || []
if (dataPoints.length < 2) continue;
// 获取填充样式
let fillStyle = lineCfg.fillStyle || lineChart.defaultFillStyle;
let gradientStart = fillStyle.gradientStart || lineChart.defaultFillStyle.gradientStart;
let gradientEnd = fillStyle.gradientEnd || lineChart.defaultFillStyle.gradientEnd;
let gradientDirection = fillStyle.gradientDirection || lineChart.defaultFillStyle.gradientDirection;
// ========== 核心修改1:顶部颜色变亮10% ==========
let brightenedTopColor = adjustColorBrightness(gradientEnd, lineChart.topColorBrightnessRatio);
// ========== 核心修改2:创建渐变(透明度+亮度) ==========
let gradient;
let fillTopY = dataToCanvasY(yMax);
let fillBottomY = dataToCanvasY(yMin);
if (gradientDirection === "horizontal") {
gradient = ctx.createLinearGradient(0, 0, w, 0);
gradient.addColorStop(0, hexToRgba(gradientStart, lineChart.fillOpacityBase));
gradient.addColorStop(1, hexToRgba(brightenedTopColor, lineChart.fillOpacityBase));
} else {
// 垂直渐变:底部更透(0.1),顶部不透(1.0)+变亮
gradient = ctx.createLinearGradient(0, fillBottomY, 0, fillTopY);
// 底部:原颜色 + 低透明度(0.9*0.1=0.09)
let bottomAlpha = lineChart.fillOpacityBase * lineChart.bottomOpacityRatio;
gradient.addColorStop(0, hexToRgba(gradientStart, bottomAlpha));
// 顶部:亮10%的颜色 + 高透明度(0.9*1.0=0.9)
let topAlpha = lineChart.fillOpacityBase * lineChart.topOpacityRatio;
gradient.addColorStop(1, hexToRgba(brightenedTopColor, topAlpha));
}
// 绘制填充区域
ctx.beginPath();
let firstPoint = dataPoints[0];
let firstCanvasX = dataToCanvasX(firstPoint.x);
let firstCanvasY = dataToCanvasY(firstPoint.y);
ctx.moveTo(firstCanvasX, firstCanvasY);
// 绘制折线
for (let pointIdx = 1; pointIdx < dataPoints.length; pointIdx++) {
let point = dataPoints[pointIdx];
let canvasX = dataToCanvasX(point.x);
let canvasY = dataToCanvasY(point.y);
ctx.lineTo(canvasX, canvasY);
}
// 闭合路径
let lastPoint = dataPoints[dataPoints.length - 1];
let lastCanvasX = dataToCanvasX(lastPoint.x);
let xAxisY = dataToCanvasY(yMin);
ctx.lineTo(lastCanvasX, xAxisY);
ctx.lineTo(firstCanvasX, xAxisY);
ctx.closePath();
// 填充渐变(透明度+亮度已融入)
ctx.fillStyle = gradient;
ctx.fill();
// 绘制折线描边
ctx.beginPath();
ctx.moveTo(firstCanvasX, firstCanvasY);
for (let pointIdx = 1; pointIdx < dataPoints.length; pointIdx++) {
let point = dataPoints[pointIdx];
let canvasX = dataToCanvasX(point.x);
let canvasY = dataToCanvasY(point.y);
ctx.lineTo(canvasX, canvasY);
}
ctx.strokeStyle = lineCfg.lineColor || lineChart.lineStrokeColor;
ctx.lineWidth = lineCfg.lineWidth || lineChart.lineStrokeWidth;
ctx.stroke();
// 绘制数据点
ctx.fillStyle = lineCfg.lineColor || lineChart.lineStrokeColor;
for (let pointIdx = 0; pointIdx < dataPoints.length; pointIdx++) {
let point = dataPoints[pointIdx];
let canvasX = dataToCanvasX(point.x);
let canvasY = dataToCanvasY(point.y);
ctx.beginPath();
ctx.arc(canvasX, canvasY, pointRadius, 0, Math.PI*2);
ctx.fill();
}
}
}
}
}
// 组件初始化
Component.onCompleted: {
updateChart();
}
}
main.qml
javascript
复制代码
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Window 2.15
Window {
id: mainWindow
width: 1355
height: 519
title: "科技风渐变折线图示例"
visible: true
color: "#1e1e1e"
ColumnLayout {
anchors.fill: parent
spacing: 20
TSAreaLine {
id: myLineChart
Layout.fillWidth: true
Layout.fillHeight: true
textSize: 14
dynamicMargin: false
baseMargin: 0
// 初始参数更新:基础透明度0.9,底部0.1,顶部亮度+0.1
fillOpacityBase: 0.9
topOpacityRatio: 1.0
bottomOpacityRatio: 0.1
topColorBrightnessRatio: 0.1 // 顶部变亮10%
defaultFillStyle: {
"gradientStart": "#4dd0e1",
"gradientEnd": "#006064",
"gradientDirection": "vertical"
}
lineStrokeColor: "#80deea"
lineStrokeWidth: 1.5
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 10
Button {
text: "显示科技风渐变折线"
onClicked: {
myLineChart.setAxisConfig({
xMin: 0,
xMax: 28,
yMin: 0,
yMax: 160,
xLabel: "",
xUnit: "",
yLabel: "",
yUnit: "",
xTicks: 0,
yTicks: 0
});
myLineChart.setLineConfigs([
{
dataPoints: [
{x:0,y:10}, {x:1,y:5}, {x:2,y:0}, {x:3,y:5},
{x:4,y:3}, {x:5,y:8}, {x:6,y:12}, {x:7,y:15},
{x:8,y:10}, {x:9,y:18}, {x:10,y:12}, {x:11,y:20},
{x:12,y:8}, {x:13,y:5}, {x:14,y:15}, {x:15,y:20},
{x:16,y:25}, {x:17,y:20}, {x:18,y:30}, {x:19,y:35},
{x:20,y:55}, {x:21,y:30}, {x:22,y:20}, {x:23,y:15},
{x:24,y:35}, {x:25,y:45}, {x:26,y:40}, {x:27,y:38},
{x:28,y:15}
],
fillStyle: {
"gradientStart": "#80deea",
"gradientEnd": "#004d40"
}
},
{
dataPoints: [
{x:0,y:15}, {x:1,y:0}, {x:2,y:5}, {x:3,y:10},
{x:4,y:18}, {x:5,y:22}, {x:6,y:15}, {x:7,y:35},
{x:8,y:40}, {x:9,y:10}, {x:10,y:40}, {x:11,y:20},
{x:12,y:35}, {x:13,y:35}, {x:14,y:30}, {x:15,y:35},
{x:16,y:40}, {x:17,y:80}, {x:18,y:145}, {x:19,y:135},
{x:20,y:105}, {x:21,y:110}, {x:22,y:60}, {x:23,y:45},
{x:24,y:50}, {x:25,y:40}, {x:26,y:42}, {x:27,y:10},
{x:28,y:5}
],
fillStyle: {
"gradientStart": "#4dd0e1",
"gradientEnd": "#006064"
}
}
]);
}
}
Button {
text: "清空折线"
onClicked: {
myLineChart.setLineConfigs([]);
}
}
Button {
text: "统一修改填充样式"
onClicked: {
myLineChart.setAllLineFillStyles({
"gradientStart": "#ff9800",
"gradientEnd": "#e64a19"
});
}
}
Button {
text: "修改第一条折线样式"
onClicked: {
myLineChart.setLineFillStyle(0, {
"gradientStart": "#4caf50",
"gradientEnd": "#1b5e20"
});
}
}
Button {
text: "提高基础透明度(0.9)"
onClicked: {
myLineChart.setAllLineFillOpacity(0.9);
}
}
Button {
text: "降低基础透明度(0.5)"
onClicked: {
myLineChart.setAllLineFillOpacity(0.5);
}
}
Button {
text: "底部更透(0.1)"
onClicked: {
myLineChart.setVerticalOpacityRatio(1.0, 0.1);
}
}
Button {
text: "底部稍透(0.5)"
onClicked: {
myLineChart.setVerticalOpacityRatio(1.0, 0.5);
}
}
// 新增:调整顶部亮度按钮
Button {
text: "顶部更亮(+20%)"
onClicked: {
myLineChart.setTopColorBrightness(0.2);
}
}
Button {
text: "顶部默认亮度"
onClicked: {
myLineChart.setTopColorBrightness(0.1);
}
}
}
}
}
v2坐标轴+标签+测试接口
TSAreaLine.qml
javascript
复制代码
import QtQuick 2.12
Item {
id: lineChart
// ===================== 外部可配置属性 ======================
// 基础坐标轴配置
property real xMin: 0
property real xMax: 10
property real yMin: 0
property real yMax: 100
property string xLabel: "时间"
property string xUnit: "秒"
property string yLabel: "数值"
property string yUnit: ""
property int xTicks: 10
property int yTicks: 10
property int textSize: 12
property bool dynamicMargin: true
property real baseMargin: 40
property var lineConfigs: []
// 渐变与透明度
property real fillOpacityBase: 0.9
property real topOpacityRatio: 1.0
property real bottomOpacityRatio: 0.1
property real topColorBrightnessRatio: 0.1
property var defaultFillStyle: {
"gradientStart": "#00e5ff",
"gradientEnd": "#7c4dff",
"gradientDirection": "vertical"
}
property color lineStrokeColor: "#ffffff"
property real lineStrokeWidth: 1.5
property int pointRadius: 3
// 横轴自定义标签
property var xTickLabels: []
// ===================== 新增:标签相关全局配置 ======================
property bool showLabel: true // 全局控制是否显示标签(核心新增)
property var defaultLabelOffset: [0, -15] // 标签在拐点正上方的偏移(x=0水平居中,y=-15向上偏移)
property real labelOffsetStep: 8 // 标签错开步长(防遮挡)
property bool enableLabelStagger: true // 是否开启标签自动错开
// ===================== 内部属性 ======================
property color axisColor: "#333333"
property color gridColor: "#e0e0e0"
property real actualLeftMargin: baseMargin
property real actualRightMargin: 20
property real actualTopMargin: 20
property real actualBottomMargin: baseMargin
// ===================== 外部接口(扩展标签配置)=====================
function setAxisConfig(config) {
// 原有坐标轴配置赋值
lineChart.xMin = config.xMin !== undefined ? config.xMin : lineChart.xMin;
lineChart.xMax = config.xMax !== undefined ? config.xMax : lineChart.xMax;
lineChart.yMin = config.yMin !== undefined ? config.yMin : lineChart.yMin;
lineChart.yMax = config.yMax !== undefined ? config.yMax : lineChart.yMax;
lineChart.xLabel = config.xLabel || lineChart.xLabel;
lineChart.xUnit = config.xUnit || lineChart.xUnit;
lineChart.yLabel = config.yLabel || lineChart.yLabel;
lineChart.yUnit = config.yUnit || lineChart.yUnit;
lineChart.xTicks = config.xTicks !== undefined ? config.xTicks : lineChart.xTicks;
lineChart.yTicks = config.yTicks !== undefined ? config.yTicks : lineChart.yTicks;
lineChart.xTickLabels = config.xTickLabels || lineChart.xTickLabels;
// 新增:标签全局配置赋值
lineChart.showLabel = config.showLabel !== undefined ? config.showLabel : lineChart.showLabel;
lineChart.defaultLabelOffset = config.defaultLabelOffset || lineChart.defaultLabelOffset;
lineChart.enableLabelStagger = config.enableLabelStagger !== undefined ? config.enableLabelStagger : lineChart.enableLabelStagger;
lineChart.labelOffsetStep = config.labelOffsetStep || lineChart.labelOffsetStep;
updateChart();
}
function setLineConfigs(configs) {
lineChart.lineConfigs = configs || [];
updateChart();
}
// 新增:全局控制是否显示标签(核心接口)
function setShowLabel(show) {
lineChart.showLabel = show;
updateChart();
}
// 新增:单独控制指定折线是否显示标签
function setLineShowLabel(lineIndex, show) {
if (lineChart.lineConfigs[lineIndex]) {
lineChart.lineConfigs[lineIndex].showLabel = show;
updateChart();
}
}
// 保留:单独设置标签偏移(如需微调正上方位置)
function setLineLabelOffset(lineIndex, offset) {
if (lineChart.lineConfigs[lineIndex]) {
lineChart.lineConfigs[lineIndex].labelOffset = offset;
updateChart();
}
}
function updateChart() {
calculateDynamicMargin();
chartCanvas.requestPaint();
}
// ===================== 内部工具函数(扩展标签相关)=====================
function calculateDynamicMargin() {
if (!dynamicMargin) {
actualLeftMargin = baseMargin;
actualBottomMargin = baseMargin;
return;
}
let ctx = chartCanvas.getContext("2d");
ctx.font = textSize + "px Arial";
let maxYTickTextWidth = ctx.measureText(yMax.toString()).width;
let yLabelWithUnit = yLabel + (yUnit ? "(" + yUnit + ")" : "");
let yLabelWidth = ctx.measureText(yLabelWithUnit).width;
actualLeftMargin = baseMargin + maxYTickTextWidth + yLabelWidth/2 + 10;
let maxXTickTextHeight = textSize + 5;
let xLabelWithUnit = xLabel + (xUnit ? "(" + xUnit + ")" : "");
let xLabelHeight = textSize + 5;
actualBottomMargin = baseMargin + maxXTickTextHeight + xLabelHeight + 5;
let maxMargin = Math.min(width/4, height/4);
actualLeftMargin = Math.min(actualLeftMargin, maxMargin);
actualBottomMargin = Math.min(actualBottomMargin, maxMargin);
}
function dataToCanvasX(x) {
let xRange = xMax - xMin;
let canvasWidth = chartCanvas.width - actualLeftMargin - actualRightMargin;
return actualLeftMargin + (x - xMin) * (canvasWidth / xRange);
}
function dataToCanvasY(y) {
let yRange = yMax - yMin;
let canvasHeight = chartCanvas.height - actualTopMargin - actualBottomMargin;
return chartCanvas.height - actualBottomMargin - (y - yMin) * (canvasHeight / yRange);
}
function hexToRgba(hex, alpha) {
hex = hex.replace(/^#/, '');
if (hex.length === 3) {
hex = hex.split('').map(c => c + c).join('');
}
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
let op = Math.max(0, Math.min(1, alpha || 1));
return `rgba(${r}, ${g}, ${b}, ${op})`;
}
function adjustColorBrightness(hex, brightness) {
hex = hex.replace(/^#/, '');
let r = parseInt(hex.substring(0, 2), 16);
let g = parseInt(hex.substring(2, 4), 16);
let b = parseInt(hex.substring(4, 6), 16);
r = Math.round(Math.min(255, Math.max(0, r + r * brightness)));
g = Math.round(Math.min(255, Math.max(0, g + g * brightness)));
b = Math.round(Math.min(255, Math.max(0, b + b * brightness)));
let toHex = (n) => n.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// 核心修改:强制标签在拐点正上方,移除位置配置,简化计算
function calculateLabelPosition(params) {
let {ctx, x, y, value, lineIdx, pointIdx, lineCfg} = params;
let labelText = value.toFixed(1);
let textWidth = ctx.measureText(labelText).width + 6;
let textHeight = textSize + 4;
// 固定标签位置:拐点正上方,水平居中
let customOffset = lineCfg.labelOffset || lineChart.defaultLabelOffset;
let staggerStep = lineChart.labelOffsetStep;
let labelX = x; // 水平居中于拐点
let labelY = y + customOffset[1];// 垂直向上偏移(默认-15)
let textAlign = "center"; // 文字水平居中
let textBaseline = "bottom"; // 文字底部对齐
// 自动错开防遮挡(仅水平微调,保持垂直在正上方)
if (lineChart.enableLabelStagger) {
let staggerX = (lineIdx % 3) * staggerStep - staggerStep;
labelX += staggerX; // 仅水平错开,不改变垂直位置
}
// 边界检测:确保标签不超出画布
let canvasWidth = chartCanvas.width;
let canvasHeight = chartCanvas.height;
if (labelX - textWidth/2 < actualLeftMargin) {
labelX = actualLeftMargin + textWidth/2 + 5;
}
if (labelX + textWidth/2 > canvasWidth - actualRightMargin) {
labelX = canvasWidth - actualRightMargin - textWidth/2 - 5;
}
if (labelY - textHeight < actualTopMargin) {
labelY = actualTopMargin + textHeight + 5;
textBaseline = "top";
}
return {
x: labelX,
y: labelY,
textWidth: textWidth,
textHeight: textHeight,
textAlign: textAlign,
textBaseline: textBaseline,
labelText: labelText
};
}
// 绘制自定义数据点
function drawPoint(ctx, x, y, type, color, size) {
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
switch(type) {
case "diamond":
ctx.beginPath();
ctx.moveTo(x, y - size);
ctx.lineTo(x + size, y);
ctx.lineTo(x, y + size);
ctx.lineTo(x - size, y);
ctx.closePath();
ctx.fill();
break;
case "square":
ctx.fillRect(x - size, y - size, size*2, size*2);
break;
case "triangle":
ctx.beginPath();
ctx.moveTo(x, y - size);
ctx.lineTo(x + size, y + size);
ctx.lineTo(x - size, y + size);
ctx.closePath();
ctx.fill();
break;
case "cross":
ctx.beginPath();
ctx.moveTo(x - size, y - size);
ctx.lineTo(x + size, y + size);
ctx.moveTo(x + size, y - size);
ctx.lineTo(x - size, y + size);
ctx.stroke();
break;
default:
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI*2);
ctx.fill();
}
}
// 核心修改:移除标签背景,仅绘制文字,固定在正上方
function drawPointLabel(ctx, x, y, value, color, lineIdx, pointIdx, lineCfg) {
let labelInfo = calculateLabelPosition({
ctx: ctx,
x: x,
y: y,
value: value,
lineIdx: lineIdx,
pointIdx: pointIdx,
lineCfg: lineCfg
});
// 移除所有背景绘制逻辑,仅保留文字绘制
ctx.fillStyle = "#ffffff"; // 固定白色文字
ctx.font = (textSize - 1) + "px Arial";
ctx.textAlign = labelInfo.textAlign;
ctx.textBaseline = labelInfo.textBaseline;
ctx.fillText(labelInfo.labelText, labelInfo.x, labelInfo.y);
}
// ===================== Canvas 绘制(整合标签+保留网格样式)=====================
Canvas {
id: chartCanvas
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
onPaint: {
let ctx = getContext("2d")
ctx.reset()
let w = width
let h = height
ctx.fillStyle = "#1e1e1e"
ctx.fillRect(0, 0, w, h)
if (xMax <= xMin || yMax <= yMin) return;
// 网格横线:最下面的横线(i=0)设为白色,其他保持原颜色
let yStep = (yMax - yMin) / yTicks
for (let i = 0; i <= yTicks; i++) {
let y = yMin + i * yStep
let cy = dataToCanvasY(y)
ctx.beginPath()
if (i === 0) {
ctx.strokeStyle = "#ffffff" // 最下面的横线设为白色
} else {
ctx.strokeStyle = gridColor
}
ctx.moveTo(actualLeftMargin, cy)
ctx.lineTo(w - actualRightMargin, cy)
ctx.stroke()
}
// 网格竖线:最左边的竖线(i=0)设为白色,其他设为透明
let xStep = (xMax - xMin) / xTicks
for (let i = 0; i <= xTicks; i++) {
let x = xMin + i * xStep
let cx = dataToCanvasX(x)
ctx.beginPath()
if (i === 0) {
ctx.strokeStyle = "#ffffff" // 最左边的竖线设为白色
} else {
ctx.strokeStyle = "rgba(0, 0, 0, 0)" // 其他竖线透明
}
ctx.moveTo(cx, actualTopMargin)
ctx.lineTo(cx, h - actualBottomMargin)
ctx.stroke()
}
// 坐标轴
ctx.strokeStyle = axisColor
ctx.lineWidth = 1.5
ctx.beginPath()
ctx.moveTo(actualLeftMargin, h - actualBottomMargin)
ctx.lineTo(w - actualRightMargin, h - actualBottomMargin)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(actualLeftMargin, actualTopMargin)
ctx.lineTo(actualLeftMargin, h - actualBottomMargin)
ctx.stroke()
// X 刻度标签(支持自定义)
ctx.fillStyle = "#fff"
ctx.font = textSize + "px Arial"
ctx.textAlign = "center"
ctx.textBaseline = "top"
for (let i = 0; i <= xTicks; i++) {
let x = xMin + i * xStep
let cx = dataToCanvasX(x)
let txt = xTickLabels[i] ?? x.toFixed(0)
ctx.fillText(txt, cx, h - actualBottomMargin + 8)
}
// Y 刻度
ctx.textAlign = "right"
ctx.textBaseline = "middle"
for (let i = 0; i <= yTicks; i++) {
let y = yMin + i * yStep
let cy = dataToCanvasY(y)
ctx.fillText(y.toFixed(0), actualLeftMargin - 8, cy)
}
// 折线 + 数据点 + 标签(核心扩展)
if (lineConfigs.length > 0) {
for (let lineIdx = lineConfigs.length - 1; lineIdx >= 0; lineIdx--) {
let lineCfg = lineConfigs[lineIdx]
let data = lineCfg.dataPoints || []
if (data.length < 2) continue;
let fs = lineCfg.fillStyle || defaultFillStyle
let gStart = fs.gradientStart || defaultFillStyle.gradientStart
let gEnd = fs.gradientEnd || defaultFillStyle.gradientEnd
let dir = fs.gradientDirection || defaultFillStyle.gradientDirection
let brightEnd = adjustColorBrightness(gEnd, topColorBrightnessRatio)
let bottomY = dataToCanvasY(yMin)
let topY = dataToCanvasY(yMax)
let grad = ctx.createLinearGradient(0, bottomY, 0, topY)
grad.addColorStop(0, hexToRgba(gStart, fillOpacityBase * bottomOpacityRatio))
grad.addColorStop(1, hexToRgba(brightEnd, fillOpacityBase * topOpacityRatio))
// 填充区域
ctx.beginPath()
let first = data[0]
let fx = dataToCanvasX(first.x)
let fy = dataToCanvasY(first.y)
ctx.moveTo(fx, fy)
for (let p of data.slice(1)) {
ctx.lineTo(dataToCanvasX(p.x), dataToCanvasY(p.y))
}
ctx.lineTo(dataToCanvasX(data[data.length-1].x), bottomY)
ctx.lineTo(fx, bottomY)
ctx.closePath()
ctx.fillStyle = grad
ctx.fill()
// 折线描边
ctx.beginPath()
ctx.moveTo(fx, fy)
for (let p of data.slice(1)) {
ctx.lineTo(dataToCanvasX(p.x), dataToCanvasY(p.y))
}
ctx.strokeStyle = lineCfg.lineColor || lineStrokeColor
ctx.lineWidth = lineStrokeWidth
ctx.stroke()
// 数据点 + 数值标签(核心修改)
let lineColor = lineCfg.lineColor || lineStrokeColor;
for (let pointIdx = 0; pointIdx < data.length; pointIdx++) {
let p = data[pointIdx];
let cx = dataToCanvasX(p.x);
let cy = dataToCanvasY(p.y);
// 绘制自定义数据点
drawPoint(ctx, cx, cy, lineCfg.pointType || "circle", lineColor, pointRadius);
// 绘制标签:全局开关 + 单条折线开关双重控制
if (lineChart.showLabel && lineCfg.showLabel !== false) {
drawPointLabel(ctx, cx, cy, p.y, lineColor, lineIdx, pointIdx, lineCfg);
}
}
// 折线名称标签
if (lineCfg.lineName) {
ctx.font = textSize + "px Arial"
ctx.textAlign = "left"
ctx.textBaseline = "top"
let nameX = actualLeftMargin + 10 + (lineIdx % 3) * 120
let nameY = actualTopMargin + 10 + Math.floor(lineIdx / 3) * 20
nameX = Math.min(nameX, w - actualRightMargin - 10)
nameY = Math.min(nameY, h - actualBottomMargin - 20)
let lineName = lineCfg.lineName.length > 10 ? lineCfg.lineName.substring(0, 10) + "..." : lineCfg.lineName
ctx.fillStyle = lineColor
ctx.fillRect(nameX, nameY + 2, 8, 8)
ctx.fillStyle = "#ffffff"
ctx.fillText(lineName, nameX + 12, nameY)
}
}
}
}
}
Component.onCompleted: updateChart()
}
main.qml
javascript
复制代码
// ===================== 主窗口(测试标签控制接口)=====================
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Window 2.15
Window {
width: 1200
height: 600
title: "科技风折线图(标签固定正上方)"
visible: true
color: "#1e1e1e"
ColumnLayout {
anchors.fill: parent
spacing: 20
TSAreaLine {
id: chart
Layout.fillWidth: true
Layout.fillHeight: true
textSize: 14
dynamicMargin: false
baseMargin: 40
fillOpacityBase: 0.9
topOpacityRatio: 1.0
bottomOpacityRatio: 0.1
topColorBrightnessRatio: 0.1
// 全局标签配置:默认显示,正上方偏移
showLabel: true
defaultLabelOffset: [0, -15] // x=0水平居中,y=-15向上偏移
enableLabelStagger: true
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 20
Button {
text: "显示数据"
onClicked: {
chart.setAxisConfig({
xMin: 0,
xMax: 12,
yMin: 0,
yMax: 160,
xTicks: 12,
yTicks: 8,
xTickLabels: ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月",""],
// 全局标签配置
showLabel: true,
defaultLabelOffset: [0, -20], // 自定义正上方偏移量
enableLabelStagger: true
})
chart.setLineConfigs([
{
lineName: "数据1",
pointType: "circle",
showLabel: true, // 单独控制该折线显示标签
dataPoints: [
{x:0,y:20},{x:1,y:40},{x:2,y:35},{x:3,y:60},{x:4,y:70},{x:5,y:90},
{x:6,y:80},{x:7,y:100},{x:8,y:110},{x:9,y:95},{x:10,y:80},{x:11,y:60},{x:12,y:45}
],
fillStyle: { gradientStart: "#80deea", gradientEnd: "#004d40" },
lineColor: "#80deea"
},
{
lineName: "数据2",
pointType: "diamond",
showLabel: true, // 单独控制该折线显示标签
dataPoints: [
{x:0,y:10},{x:1,y:25},{x:2,y:55},{x:3,y:45},{x:4,y:65},{x:5,y:85},
{x:6,y:120},{x:7,y:140},{x:8,y:125},{x:9,y:100},{x:10,y:75},{x:11,y:50},{x:12,y:30}
],
fillStyle: { gradientStart: "#4dd0e1", gradientEnd: "#006064" },
lineColor: "#4dd0e1"
}
])
}
}
Button {
text: "显示所有标签"
onClicked: {
chart.setShowLabel(true); // 全局显示所有标签
}
}
Button {
text: "隐藏所有标签"
onClicked: {
chart.setShowLabel(false); // 全局隐藏所有标签
}
}
Button {
text: "仅隐藏数据2标签"
onClicked: {
chart.setLineShowLabel(1, false); // 单独隐藏第二条折线的标签
}
}
Button {
text: "恢复数据2标签"
onClicked: {
chart.setLineShowLabel(1, true); // 单独显示第二条折线的标签
}
}
}
}
}
demo02
TSAreaLine.qml
javascript
复制代码
import QtQuick 2.12
Item {
id: lineChart
// ===================== 外部可配置属性(核心,新增/优化)=====================
// 坐标轴范围(外部设置)
property real xMin: 0 // X轴最小值
property real xMax: 10 // X轴最大值
property real yMin: 0 // Y轴最小值
property real yMax: 100 // Y轴最大值
// 坐标轴标签+单位(外部设置,解决单位配置需求)
property string xLabel: "时间" // X轴标签
property string xUnit: "秒" // X轴单位(新增:外部可设置横轴单位)
property string yLabel: "数值" // Y轴标签
property string yUnit: "" // Y轴单位(可选,外部可设置)
// 刻度数量(控制网格密度)
property int xTicks: 10 // X轴刻度数
property int yTicks: 10 // Y轴刻度数
// 文字配置(新增:解决文字显示不全)
property int textSize: 12 // 基础文字大小,外部可调整
property bool dynamicMargin: true // 动态计算边距(默认开启,保证文字全显示)
property real baseMargin: 40 // 基础边距,外部可调整
// 折线配置数组(外部设置,支持任意条数折线,默认空数组)
// 格式:[{lineColor: "#4CAF50", lineWidth: 2, dataPoints: [{x:0,y:0}, ...], lineName: "折线1"}, ...]
property var lineConfigs: []
// 科技风渐变配置(新增)
property color gradientStart: "#00e5ff" // 渐变起始色(青色)
property color gradientEnd: "#7c4dff" // 渐变结束色(紫色)
property color lineStrokeColor: "#ffffff" // 折线描边色(白色)
property real lineStrokeWidth: 1.5 // 折线描边宽度
// ===================== 内部属性(优化文字显示)=====================
property color axisColor: "#333333" // 坐标轴颜色
property color gridColor: "#e0e0e0" // 网格线颜色
property int pointRadius: 3 // 数据点半径
// 动态计算的实际边距(根据文字自动调整)
property real actualLeftMargin: baseMargin
property real actualRightMargin: 20
property real actualTopMargin: 20
property real actualBottomMargin: baseMargin
// ===================== 外部友好接口(新增:简化配置)=====================
/**
* 外部一键配置坐标轴(范围+标签+单位)
* @param {Object} config - 配置对象
* config.xMin: X轴最小值
* config.xMax: X轴最大值
* config.yMin: Y轴最小值
* config.yMax: Y轴最大值
* config.xLabel: X轴标签(如"时间")
* config.xUnit: X轴单位(如"秒")
* config.yLabel: Y轴标签(如"温度")
* config.yUnit: Y轴单位(如"℃")
* config.xTicks: X轴刻度数
* config.yTicks: Y轴刻度数
*/
function setAxisConfig(config) {
// 赋值坐标轴范围
lineChart.xMin = config.xMin !== undefined ? config.xMin : lineChart.xMin;
lineChart.xMax = config.xMax !== undefined ? config.xMax : lineChart.xMax;
lineChart.yMin = config.yMin !== undefined ? config.yMin : lineChart.yMin;
lineChart.yMax = config.yMax !== undefined ? config.yMax : lineChart.yMax;
// 赋值标签+单位
lineChart.xLabel = config.xLabel || lineChart.xLabel;
lineChart.xUnit = config.xUnit || lineChart.xUnit;
lineChart.yLabel = config.yLabel || lineChart.yLabel;
lineChart.yUnit = config.yUnit || lineChart.yUnit;
// 赋值刻度数
lineChart.xTicks = config.xTicks !== undefined ? config.xTicks : lineChart.xTicks;
lineChart.yTicks = config.yTicks !== undefined ? config.yTicks : lineChart.yTicks;
// 刷新图表
updateChart();
}
/**
* 外部设置折线配置
* @param {Array} configs - 折线配置数组(格式同lineConfigs)
*/
function setLineConfigs(configs) {
lineChart.lineConfigs = configs || [];
updateChart();
}
/**
* 刷新折线图(外部修改配置后调用此接口即可更新显示)
*/
function updateChart() {
// 动态计算边距(即使无折线,也要计算边距保证坐标轴文字显示)
calculateDynamicMargin();
// 触发重绘
chartCanvas.requestPaint()
}
// ===================== 内部工具函数(核心:动态计算边距,解决文字截断)=====================
/**
* 动态计算边距:根据文字长度调整左/底部边距,保证文字不截断
*/
function calculateDynamicMargin() {
if (!dynamicMargin) {
// 关闭动态边距:使用基础边距
actualLeftMargin = baseMargin;
actualBottomMargin = baseMargin;
return;
}
// 1. 计算左侧边距(适配Y轴刻度文字、Y轴标签+单位)
let ctx = chartCanvas.getContext("2d");
ctx.font = textSize + "px Arial";
// 最大Y轴刻度文字宽度(比如"150"比"50"宽)
let maxYTickTextWidth = ctx.measureText(yMax.toString()).width;
// Y轴标签+单位宽度(旋转后占左侧空间)
let yLabelWithUnit = yLabel + (yUnit ? "(" + yUnit + ")" : "");
let yLabelWidth = ctx.measureText(yLabelWithUnit).width;
// 左侧边距 = 基础边距 + 最大刻度文字宽度 + 标签宽度/2(旋转后) + 预留空间
actualLeftMargin = baseMargin + maxYTickTextWidth + yLabelWidth/2 + 10;
// 2. 计算底部边距(适配X轴刻度文字、X轴标签+单位)
let maxXTickTextHeight = textSize + 5; // 刻度文字高度+预留
let xLabelWithUnit = xLabel + (xUnit ? "(" + xUnit + ")" : "");
let xLabelHeight = textSize + 5; // 标签高度+预留
// 底部边距 = 基础边距 + 刻度文字高度 + 标签高度 + 预留空间
actualBottomMargin = baseMargin + maxXTickTextHeight + xLabelHeight + 5;
// 3. 限制边距不超过画布1/4(避免边距过大)
let maxMargin = Math.min(width/4, height/4);
actualLeftMargin = Math.min(actualLeftMargin, maxMargin);
actualBottomMargin = Math.min(actualBottomMargin, maxMargin);
}
/**
* 将数据X值转换为Canvas像素X坐标(适配动态边距)
*/
function dataToCanvasX(x) {
let xRange = xMax - xMin;
let canvasWidth = chartCanvas.width - actualLeftMargin - actualRightMargin;
return actualLeftMargin + (x - xMin) * (canvasWidth / xRange);
}
/**
* 将数据Y值转换为Canvas像素Y坐标(适配动态边距,Canvas Y轴向下,需反转)
*/
function dataToCanvasY(y) {
let yRange = yMax - yMin;
let canvasHeight = chartCanvas.height - actualTopMargin - actualBottomMargin;
return chartCanvas.height - actualBottomMargin - (y - yMin) * (canvasHeight / yRange);
}
// ===================== 折线图绘制Canvas(核心修改:分离基础图层和折线图层)=====================
Canvas {
id: chartCanvas
anchors.fill: parent
antialiasing: true // 抗锯齿,保证线条平滑
renderTarget: Canvas.FramebufferObject
onPaint: {
let ctx = getContext("2d")
ctx.reset()
let w = width
let h = height
// 1. 清空画布(科技风深色背景)
ctx.fillStyle = "#1e1e1e"
ctx.fillRect(0, 0, w, h)
// 边界保护:仅判断坐标轴范围是否有效(移除lineConfigs.length === 0的判断)
if (xMax <= xMin || yMax <= yMin) {
return;
}
// 6. 绘制科技风渐变折线和填充区域(仅当lineConfigs有数据时执行)
if (lineConfigs.length > 0) { // 新增判断:仅当有折线配置时才绘制
for (let lineIdx = 0; lineIdx < lineConfigs.length; lineIdx++) {
let lineCfg = lineConfigs[lineIdx]
let dataPoints = lineCfg.dataPoints || []
if (dataPoints.length < 2) continue // 至少2个点才绘制折线
// 创建线性渐变(从青色到紫色)
let gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, gradientStart);
gradient.addColorStop(1, gradientEnd);
// 绘制渐变填充区域
ctx.beginPath();
// 移动到第一个数据点
let firstPoint = dataPoints[0];
let firstCanvasX = dataToCanvasX(firstPoint.x);
let firstCanvasY = dataToCanvasY(firstPoint.y);
ctx.moveTo(firstCanvasX, firstCanvasY);
// 绘制折线
for (let pointIdx = 1; pointIdx < dataPoints.length; pointIdx++) {
let point = dataPoints[pointIdx];
let canvasX = dataToCanvasX(point.x);
let canvasY = dataToCanvasY(point.y);
ctx.lineTo(canvasX, canvasY);
}
// 闭合路径到X轴底部,形成填充区域
let lastPoint = dataPoints[dataPoints.length - 1];
let lastCanvasX = dataToCanvasX(lastPoint.x);
let xAxisY = dataToCanvasY(yMin); // X轴的Y坐标
ctx.lineTo(lastCanvasX, xAxisY);
ctx.lineTo(firstCanvasX, xAxisY);
ctx.closePath();
// 填充渐变
ctx.fillStyle = gradient;
ctx.fill();
// 绘制白色折线描边
ctx.beginPath();
ctx.moveTo(firstCanvasX, firstCanvasY);
for (let pointIdx = 1; pointIdx < dataPoints.length; pointIdx++) {
let point = dataPoints[pointIdx];
let canvasX = dataToCanvasX(point.x);
let canvasY = dataToCanvasY(point.y);
ctx.lineTo(canvasX, canvasY);
}
ctx.strokeStyle = lineStrokeColor;
ctx.lineWidth = lineStrokeWidth;
ctx.stroke();
// 绘制数据点(白色小圆点)
ctx.fillStyle = lineStrokeColor;
for (let pointIdx = 0; pointIdx < dataPoints.length; pointIdx++) {
let point = dataPoints[pointIdx];
let canvasX = dataToCanvasX(point.x);
let canvasY = dataToCanvasY(point.y);
ctx.beginPath();
ctx.arc(canvasX, canvasY, pointRadius, 0, Math.PI*2);
ctx.fill();
}
}
}
}
}
// 组件初始化时触发一次绘制(即使无折线,也绘制基础图层)
Component.onCompleted: {
updateChart()
}
}
main.qml
javascript
复制代码
//main.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtQuick.Window 2.15
Window {
id: mainWindow
width: 800
height: 600
title: "科技风渐变折线图示例"
visible: true
color: "#1e1e1e" // 窗口背景色与图表一致
ColumnLayout {
anchors.fill: parent
spacing: 20
//padding: 10
// 1. 折线图组件(核心):实例化时默认显示坐标轴+背景,无折线
TSAreaLine {
id: myLineChart
Layout.fillWidth: true
Layout.fillHeight: true
// 初始配置(可被外部随时修改)
textSize: 14 // 调大文字,测试是否显示全
dynamicMargin: false // 关闭动态边距,隐藏坐标轴
baseMargin: 0 // 基础边距设为0,隐藏坐标轴
// lineConfigs默认空数组,无需额外配置
gradientStart: "#00e5ff"
gradientEnd: "#7c4dff"
lineStrokeColor: "#ffffff"
lineStrokeWidth: 1.5
}
// 2. 外部控制按钮(演示新增接口调用)
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 10
// 按钮1:显示科技风渐变折线图
Button {
text: "显示科技风渐变折线"
onClicked: {
// 调用新增接口:一键配置坐标轴(数值范围+横轴单位)
myLineChart.setAxisConfig({
xMin: 0,
xMax: 10,
yMin: 0,
yMax: 100,
xLabel: "",
xUnit: "",
yLabel: "",
yUnit: "",
xTicks: 0,
yTicks: 0
});
// 调用接口设置折线配置
myLineChart.setLineConfigs([
{
dataPoints: [
{x:0,y:10}, {x:1,y:12}, {x:2,y:8}, {x:3,y:15},
{x:4,y:30}, {x:5,y:25}, {x:6,y:28}, {x:7,y:40},
{x:8,y:60}, {x:9,y:80}, {x:10,y:95}
]
}
]);
}
}
// 按钮2:清空折线图(仅隐藏折线,保留坐标轴+背景)
Button {
text: "清空折线"
onClicked: {
myLineChart.setLineConfigs([]);
}
}
}
}
}