D3JS 实现绘制折线效果(svg)

 目录

功能效果

效果图

实现逻辑

1、创建一个SVG容器:

2、创建一个主分组:

3、添加背景:

4、定义缩放和平移行为:

5、应用缩放和平移行为:

 6、定义鼠标事件:

7、事件监听

完整代码


 D3.js绘图工具系列文章总提纲:【传送门】

功能效果

        通过按下鼠标左键开始绘制,移动鼠标更新路径坐标预览效果,再次点击后则将鼠标当前位置添加进路径集,移动鼠标更新路径坐标预览效果,双击鼠标左键则结束绘制;通过画布的平移(按住滑轮)与缩放(滚动滑轮)不影响画笔的绘制效果。

效果图

实现逻辑

 通过修改 mousedown、mousemove与mouseup事件实现。

1、创建一个SVG容器:

使用d3.js创建一个SVG容器(h5代码中须有相关id标签),用于呈现图表和图形;

let svg = reactive({});

// 生成画布的dom节点id为d3Canvas;须在html代码中添加
svg = d3
    .select('#d3Canvas')
    .append("svg")
    .attr("id", "svg")
    .attr("width", 500)
    .attr("height", 500)
    .attr("viewBox", "0 0 500 500");

2、创建一个主分组:

使用d3.js对所创建的SVG容器,添加一个g标签,用于控制整体缩放;不将事件直接绑定于svg上,是因为会出现bug:画面呈现出一闪一闪的,解决方案是将所有元素使用g标签进行包裹。

let g = reactive({});

g = svg.append("g").attr("id", "svg-grid");

3、添加背景:

用于让拖拽缩放效果更明显;

g.insert("rect", ":first-child")
    .attr("width", "100%")
    .attr("height", "100%")
    .attr("fill", "url(#grid)");

// 使用网格样式填充 SVG 元素背景
var pattern = svg.insert("defs", ":first-child")
    .append("pattern")
    .attr("id", "grid")
    .attr("width", 20)
    .attr("height", 20)
    .attr("patternUnits", "userSpaceOnUse");

// 添加网格线
pattern.append("path")
    .attr("d", "M 20 0 L 0 0 0 20")
    .attr("stroke", "lightgray")
    .attr("stroke-width", 0.5)
    .attr("fill", "none");

4、定义缩放和平移行为:

使用d3.zoom()函数创建一个缩放和平移行为,可以通过设置缩放比例和平移偏移量来控制缩放和平移的效果;

function zoomed(event, svg) {
    // 对g进行偏移赋值
    g.attr("transform", event.transform);
}
var zoom = d3
    .zoom()
    .scaleExtent([0.3, 10])
    .on("zoom", (event) => zoomed(event));

5、应用缩放和平移行为:

将缩放和平移行为应用到SVG容器上,使得图表可以响应鼠标事件。
注:阻止鼠标落下默认事件与鼠标双击事件。

g.call(zoom).on("dblclick.zoom", null).on("mousedown.zoom", null);

 6、定义鼠标事件:

 参数定义:

let isPanning = false;
let startX = 0, startY = 0;

// 折线画笔状态
let isPolygonalLineDrawing = false;
// 路径
let pathData = '';
let currentPath = null;

定义坐标转换方法:

function getTransformedCoordinate(event) {
    // 获取鼠标当前位置的坐标
    const [x, y] = d3.pointer(event);
    // 获取画布当前的平移和缩放值
    const transform = d3.zoomTransform(g.node());
    // 应用平移和缩放转换,得到变换后的坐标
    const transformedX = (x - transform.x) / transform.k;
    const transformedY = (y - transform.y) / transform.k;
    return [transformedX, transformedY];
}

定义获取路劲中最后一组坐标的方法:

function getLastSegment(pathData) {
    // 使用正则表达式匹配最后一个'L'后的字符串
    const regex = /L\s*([^L]*)$/;
    const match = pathData.match(regex);

    // 如果找到匹配项,则返回最后一个'L'后的字符串,否则返回空字符串
    if (match && match.length > 1) {
        return match[1].trim();
    } else {
        return '';
    }
}

定义绘制/更新折现路径方法:

function updatePolygonalLinePath(x, y) {
    let currentPolygonalLineData = '';
    // 截取最后一个L后的字符
    if (isPolygonalLineDrawing) {
        // 若点击位置相同,则不绘制
        const lastSegment = getLastSegment(pathData);
        if (lastSegment == `${x} ${y}`) return;
        // 折线路径
        pathData += `${pathData ? ' L' : 'M'} ${x} ${y}`;
        currentPolygonalLineData = pathData;
    } else {
        currentPolygonalLineData = pathData + `${pathData ? ' L' : 'M'} ${x} ${y}`;
    }
    if (currentPath) {
        currentPath.attr('d', currentPolygonalLineData);
    } else {
        currentPath = g.append('path')
            .attr('id', 'path')
            .attr('d', currentPolygonalLineData)
            .attr('stroke', 'red')
            .attr('fill', 'none')
            .attr('stroke-width', 2);
    }
}

平移鼠标落下事件(mousedown):

function panMouseDown(event) {
    if (event.button !== 1) return; // 中键按下
    console.log('中键按下');
    isPanning = true;
    [startX, startY] = d3.pointer(event);
}

平移鼠标移动事件(mousemove):

function panMouseMove(event) {
    if (isPanning) {
        event.preventDefault();
        const [x, y] = d3.pointer(event);
        const transform = d3.zoomTransform(g.node()); // 获取当前变换

        const transformedX = (x - startX) / transform.k;
        const transformedY = (y - startY) / transform.k;
        zoom.translateBy(g, transformedX, transformedY); // 使用偏移量进行平移
        startX = x; // 更新起始位置
        startY = y;
    }
}

 平移鼠标抬起事件(mouseup):

function panMouseUp() {
    if (isPanning) {
        isPanning = false;
    }
}

折线鼠标落下事件(mousedown):

function polygonalLineMouseDown(event) {
    if (event.button !== 0) return; // 检查左键
    console.log('左键按下');
    isPolygonalLineDrawing = true;
    const [x, y] = getTransformedCoordinate(event);
    updatePolygonalLinePath(x, y);
}

折线鼠标移动事件(mousemove):

function polygonalLineMouseMove(event) {
    if (!isPolygonalLineDrawing && currentPath) {
        // 获取缩放及平移后的坐标
        const [x, y] = getTransformedCoordinate(event);
        updatePolygonalLinePath(x, y);
    }
}

折线鼠标抬起事件(mouseup):

function polygonalLineMouseUp(event) {
    if (isPolygonalLineDrawing) {
        isPolygonalLineDrawing = false;
    }
}

折线鼠标双击事件(dblclick):

function polygonalLineDbClick(event) {
    isPolygonalLineDrawing = false;
    pathData = '';
    currentPath = null;
    console.log('结束绘制');
}

7、事件监听

svg.on("mousedown", function (event) {
    panMouseDown(event);
    polygonalLineMouseDown(event);
});

svg.on("mousemove", function (event) {
    panMouseMove(event);
    polygonalLineMouseMove(event);
});

svg.on("mouseup", function (event) {
    panMouseUp(event);
    polygonalLineMouseUp();
});

svg.on("dblclick", function (event) {
    polygonalLineDbClick()
});

完整代码

let svg = reactive({});

// 生成画布的dom节点id为d3Canvas;须在html代码中添加
svg = d3
    .select('#d3Canvas')
    .append("svg")
    .attr("id", "svg")
    .attr("width", 500)
    .attr("height", 500)
    .attr("viewBox", "0 0 500 500");

let g = reactive({});

g = svg.append("g").attr("id", "svg-grid");

g.insert("rect", ":first-child")
    .attr("width", "100%")
    .attr("height", "100%")
    .attr("fill", "url(#grid)");

// 使用网格样式填充 SVG 元素背景
var pattern = svg.insert("defs", ":first-child")
    .append("pattern")
    .attr("id", "grid")
    .attr("width", 20)
    .attr("height", 20)
    .attr("patternUnits", "userSpaceOnUse");

// 添加网格线
pattern.append("path")
    .attr("d", "M 20 0 L 0 0 0 20")
    .attr("stroke", "lightgray")
    .attr("stroke-width", 0.5)
    .attr("fill", "none");

function zoomed(event, svg) {
    // 对g进行偏移赋值
    g.attr("transform", event.transform);
}

var zoom = d3
    .zoom()
    .scaleExtent([0.3, 10])
    .on("zoom", (event) => zoomed(event));

g.call(zoom).on("dblclick.zoom", null).on("mousedown.zoom", null);

let isPanning = false;
let startX = 0, startY = 0;

function panMouseDown(event) {
    if (event.button !== 1) return; // 中键按下
    console.log('中键按下');
    // event.preventDefault(); // 阻止默认行为
    isPanning = true;
    [startX, startY] = d3.pointer(event);
}

function panMouseMove(event) {
    if (isPanning) {
        event.preventDefault();
        const [x, y] = d3.pointer(event);
        const transform = d3.zoomTransform(g.node()); // 获取当前变换

        const transformedX = (x - startX) / transform.k;
        const transformedY = (y - startY) / transform.k;
        zoom.translateBy(g, transformedX, transformedY); // 使用偏移量进行平移
        startX = x; // 更新起始位置
        startY = y;
    }
}

function panMouseUp() {
    if (isPanning) {
        isPanning = false;
    }
}

// 折线画笔状态
let isPolygonalLineDrawing = false;
// 路径
let pathData = '';
let currentPath = null;

// 获取转换坐标
function getTransformedCoordinate(event) {
    // 获取鼠标当前位置的坐标
    const [x, y] = d3.pointer(event);
    // 获取画布当前的平移和缩放值
    const transform = d3.zoomTransform(g.node());
    // 应用平移和缩放转换,得到变换后的坐标
    const transformedX = (x - transform.x) / transform.k;
    const transformedY = (y - transform.y) / transform.k;
    return [transformedX, transformedY];
}

// 获取最后一个'L'后的坐标
function getLastSegment(pathData) {
    // 使用正则表达式匹配最后一个'L'后的字符串
    const regex = /L\s*([^L]*)$/;
    const match = pathData.match(regex);

    // 如果找到匹配项,则返回最后一个'L'后的字符串,否则返回空字符串
    if (match && match.length > 1) {
        return match[1].trim();
    } else {
        return '';
    }
}

// 绘制/更新折线路径
function updatePolygonalLinePath(x, y) {
    let currentPolygonalLineData = '';
    // 截取最后一个L后的字符
    if (isPolygonalLineDrawing) {
        // 若点击位置相同,则不绘制
        const lastSegment = getLastSegment(pathData);
        if (lastSegment == `${x} ${y}`) return;
        // 折线路径
        pathData += `${pathData ? ' L' : 'M'} ${x} ${y}`;
        currentPolygonalLineData = pathData;
    } else {
        currentPolygonalLineData = pathData + `${pathData ? ' L' : 'M'} ${x} ${y}`;
    }
    if (currentPath) {
        currentPath.attr('d', currentPolygonalLineData);
    } else {
        currentPath = g.append('path')
            .attr('id', 'path')
            .attr('d', currentPolygonalLineData)
            .attr('stroke', 'red')
            .attr('fill', 'none')
            .attr('stroke-width', 2);
    }
}

// 折线 - 监听事件
function polygonalLineMouseDown(event) {
    if (event.button !== 0) return; // 检查左键
    console.log('左键按下');
    isPolygonalLineDrawing = true;
    const [x, y] = getTransformedCoordinate(event);
    updatePolygonalLinePath(x, y);
}

function polygonalLineMouseMove(event) {
    if (!isPolygonalLineDrawing && currentPath) {
        // 获取缩放及平移后的坐标
        const [x, y] = getTransformedCoordinate(event);
        updatePolygonalLinePath(x, y);
    }
}

function polygonalLineMouseUp(event) {
    if (isPolygonalLineDrawing) {
        isPolygonalLineDrawing = false;
    }
}

function polygonalLineDbClick(event) {
    isPolygonalLineDrawing = false;
    pathData = '';
    currentPath = null;
    console.log('结束绘制');
}

svg.on("mousedown", function (event) {
    panMouseDown(event);
    polygonalLineMouseDown(event);
});

svg.on("mousemove", function (event) {
    panMouseMove(event);
    polygonalLineMouseMove(event);
});

svg.on("mouseup", function (event) {
    panMouseUp(event);
    polygonalLineMouseUp();
});

svg.on("dblclick", function (event) {
    polygonalLineDbClick()
});

感谢阅读!在您离开之前,不妨留下您的足迹:

👍 点个赞,让我知道您喜欢这篇文章!

📝 有任何想法、建议或问题?欢迎在下方评论区分享您的想法!

🔁 分享给您的朋友们,让更多人受益!

💌 喜欢我的内容?立即订阅我的博客,获取更多精彩内容!

谢谢您的支持与参与,让我们一起创造更好的内容体验!

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值