提问人:dsapprentice 提问时间:11/7/2023 更新时间:11/14/2023 访问量:62
在 D3 中条件填充径向图
Conditional filling of radial chart in D3
问:
当我想从同一个数组(rawdata2)中绘制两列(即 VALUE1 和 VALUE2)时,我可以计算负和正区域以及交叉点。
rawdata = [ { "DATE_TIME": "2018-10-31T23:20:00.000Z", "VALUE1": 102, "VALUE2": 118, "INSPECTION": 1, "STATE": 1, "minuteTime": 20 }, { "DATE_TIME": "2018-10-31T23:20:00.000Z", "VALUE1": 110, "VALUE2": 95, "INSPECTION": 1, "STATE": 0, "minuteTime": 20 }, { "DATE_TIME": "2018-10-31T23:40:00.000Z", "VALUE1": 153, "VALUE2": 117, "INSPECTION": 1, "STATE": 1, "minuteTime": 40 }, { "DATE_TIME": "2018-10-31T23:40:00.000Z", "VALUE1": 125, "VALUE2": 98, "INSPECTION": 1, "STATE": 0, "minuteTime": 40 }, { "DATE_TIME": "2018-11-01T00:00:00.000Z", "VALUE1": 112, "VALUE2": 117, "INSPECTION": 1, "STATE": 1, "minuteTime": 60 }, { "DATE_TIME": "2018-11-01T00:00:00.000Z", "VALUE1": 120, "VALUE2": 100, "INSPECTION": 1, "STATE": 0, "minuteTime": 60 }, { "DATE_TIME": "2018-11-01T03:20:00.000Z", "VALUE1": 147, "VALUE2": 108, "INSPECTION": 1, "STATE": 1, "minuteTime": 260 }, { "DATE_TIME": "2018-11-01T03:20:00.000Z", "VALUE1": 130, "VALUE2": 98, "INSPECTION": 1, "STATE": 0, "minuteTime": 260 }, { "DATE_TIME": "2018-11-01T06:30:00.000Z", "VALUE1": 152, "VALUE2": 111, "INSPECTION": 1, "STATE": 1, "minuteTime": 450 }, { "DATE_TIME": "2018-11-01T06:30:00.000Z", "VALUE1": 140, "VALUE2": 101, "INSPECTION": 1, "STATE": 0, "minuteTime": 450 }, { "DATE_TIME": "2018-11-01T08:00:00.000Z", "VALUE1": 112, "VALUE2": 101, "INSPECTION": 1, "STATE": 1, "minuteTime": 540 }, { "DATE_TIME": "2018-11-01T08:00:00.000Z", "VALUE1": 120, "VALUE2": 90, "INSPECTION": 1, "STATE": 0, "minuteTime": 540 }, { "DATE_TIME": "2018-11-01T11:00:00.000Z", "VALUE1": 122, "VALUE2": 100, "INSPECTION": 1, "STATE": 1, "minuteTime": 720 }, { "DATE_TIME": "2018-11-01T11:00:00.000Z", "VALUE1": 130, "VALUE2": 95, "INSPECTION": 1, "STATE": 0, "minuteTime": 720 }, { "DATE_TIME": "2018-11-01T15:00:00.000Z", "VALUE1": 112, "VALUE2": 101, "INSPECTION": 1, "STATE": 1, "minuteTime": 960 }, { "DATE_TIME": "2018-11-01T15:00:00.000Z", "VALUE1": 125, "VALUE2": 98, "INSPECTION": 1, "STATE": 0, "minuteTime": 960 }, { "DATE_TIME": "2018-11-01T17:20:00.000Z", "VALUE1": 147, "VALUE2": 108, "INSPECTION": 1, "STATE": 1, "minuteTime": 1100 }, { "DATE_TIME": "2018-11-01T17:20:00.000Z", "VALUE1": 129, "VALUE2": 102, "INSPECTION": 1, "STATE": 0, "minuteTime": 1100 }, { "DATE_TIME": "2018-11-01T20:59:00.000Z", "VALUE1": 151, "VALUE2": 102, "INSPECTION": 1, "STATE": 1, "minuteTime": 1319 }, { "DATE_TIME": "2018-11-01T20:59:00.000Z", "VALUE1": 132, "VALUE2": 90, "INSPECTION": 1, "STATE": 0, "minuteTime": 1319 } ]
const width = 954;
const height = width;
const margin = 5;
const innerRadius = width / 10;
const outerRadius = width / 4 - margin;
function checkLineIntersection(line1StartX, line1StartY, line1EndX, line1EndY, line2StartX, line2StartY, line2EndX, line2EndY) {
// if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
var denominator, a, b, numerator1, numerator2, result = {
x: null,
y: null,
onLine1: false,
onLine2: false
};
denominator = ((line2EndY - line2StartY) * (line1EndX - line1StartX)) - ((line2EndX - line2StartX) * (line1EndY - line1StartY));
if (denominator === 0) {
return result;
}
a = line1StartY - line2StartY;
b = line1StartX - line2StartX;
numerator1 = ((line2EndX - line2StartX) * a) - ((line2EndY - line2StartY) * b);
numerator2 = ((line1EndX - line1StartX) * a) - ((line1EndY - line1StartY) * b);
a = numerator1 / denominator;
b = numerator2 / denominator;
// if we cast these lines infinitely in both directions, they intersect here:
result.x = line1StartX + (a * (line1EndX - line1StartX));
result.y = line1StartY + (a * (line1EndY - line1StartY));
/*
// it is worth noting that this should be the same as:
x = line2StartX + (b * (line2EndX - line2StartX));
y = line2StartX + (b * (line2EndY - line2StartY));
*/
// if line1 is a segment and line2 is infinite, they intersect if:
if (a > 0 && a < 1) {
result.onLine1 = true;
}
// if line2 is a segment and line1 is infinite, they intersect if:
if (b > 0 && b < 1) {
result.onLine2 = true;
}
// if line1 and line2 are segments, they intersect if both of the above are true
return result;
}
// const width = 954;
// const height = width;
// const margin = 10;
// const innerRadius = width / 5;
// const outerRadius = width / 2 - margin;
let rawdata2 = rawdata.filter(d => d.INSPECTION == 1);
const minuteScale = d3.scaleLinear()
.domain([0, 1439]) // 0 to 23:59 (24 hours * 60 minutes without -1 or else change to 1439 //- 1)
.range([0, 2 * Math.PI]);
const minuteTimeValues = rawdata2.map(d => d.minuteTime);
const x = d3.scaleTime()
.domain([0, 1439]) // Set the domain using the extent of the "minuteTime" values
.range([0, 2 * Math.PI]); // Set the desired range
console.log(rawdata)
const minValue1 = d3.min(rawdata, d => d.VALUE1);
const minValue2 = d3.min(rawdata, d => d.VALUE2);
const maxValue1 = d3.max(rawdata, d => d.VALUE1);
const maxValue2 = d3.max(rawdata, d => d.VALUE2);
const yMin = minValue1 < minValue2 ? minValue1 : minValue2;
const yMax = maxValue1 > maxValue2 ? maxValue1 : maxValue2;
const y = d3.scaleLinear()
.domain([yMin, yMax])
.range([innerRadius, outerRadius]);
// Modify the xAxis generator to display only the hour and add padding
const xAxis = (g) => g
.attr("font-family", "sans-serif")
.attr("font-size", 7)
.call((g) => g.selectAll("g")
.data(d3.range(0, 24)) // create an array of hour values
.join("g")
.each((d, i) => d.id = `hour-${i}`)
.call((g) => g.append("path")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.2)
.attr("d", (d) => `
M${d3.pointRadial(x(d * 60), innerRadius)}
L${d3.pointRadial(x(d * 60), outerRadius)}
`))
.call((g) => g.append("path")
.attr("id", (d) => `hour-${d}`)
.datum((d) => [d * 60, (d + 1) * 60])
.attr("fill", "none")
.attr("d", ([a, b]) => `
M${d3.pointRadial(x(a), innerRadius-10)}
A${innerRadius},${innerRadius} 0,0,1 ${d3.pointRadial(x(b), innerRadius)}
`))
.call((g) => g.append("text")
.append("textPath")
.attr("startOffset", 6)
.attr("href", (d) => `#hour-${d}`)
.text((d) => d.toString().padStart(2, "0"))
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", (d) => {
const angle = x(d * 60);
const radius = innerRadius - 20; // reduce the radius to move the labels closer to the center
return `translate(${d3.pointRadial(x(d * 60), radius)})`;
})
)
);
const yAxis = g => g
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", 7)
.call(g => g.selectAll("g")
.data(y.ticks().reverse())
.join("g")
.attr("fill", "none")
.call(g => g.append("circle")
.attr("stroke", "#000")
.attr("stroke-opacity", 0.1)
.attr("r", y))
.call(g => g.append("text")
.attr("y", d => -y(d))
.attr("dy", "0.35em")
.attr("stroke", "#fff")
.attr("stroke-width", 5)
.attr("stroke-opacity", 0.6)
.text((x, i) => `${x.toFixed(0)}${i ? "" : ""}`)
.clone(true)
.attr("y", d => y(d))
.selectAll(function() { return [this, this.previousSibling]; })
.clone(true)
.attr("fill", "currentColor")
.attr("stroke", "none")));
const line = d3.lineRadial()
.curve(d3.curveLinear)
.angle(d => x(d.minuteTime));
const area = d3.areaRadial()
.curve(d3.curveLinear)
.angle(d => x(d.minuteTime))
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round");
svg.append("path")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 3)
.attr("d", line
.radius(d => y(d.VALUE1))
(rawdata2));
svg.append("path")
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 3)
.attr("d", line
.radius(d => y(d.VALUE2))
(rawdata2));
// +++++++++++++++++++++++++++++++++++ calc crossing points ++++++++++++++++++++++++++++++++
function findCrossingPoints(data) {
const crossingPoints = [];
for (let i = 1; i < data.length; i++) {
const d1 = data[i - 1];
const d2 = data[i];
if ((d1.VALUE1 - d1.VALUE2) * (d2.VALUE1 - d2.VALUE2) < 0) {
// There's a crossing point between the two data points
const t = (0 - (d1.VALUE1 - d1.VALUE2)) / ((d2.VALUE1 - d2.VALUE2) - (d1.VALUE1 - d1.VALUE2));
const crossingValue1 = d1.VALUE1 + t * (d2.VALUE1 - d1.VALUE1);
const crossingValue2 = d1.VALUE2 + t * (d2.VALUE2 - d1.VALUE2);
crossingPoints.push({ VALUE1: crossingValue1, VALUE2: crossingValue2 });
}
}
return crossingPoints;
}
const crossingPoints = findCrossingPoints(rawdata2);
function getX(d) {
return Number(line.radius(d => y(d.VALUE1))([d]).match('M(-?[0-9.]*),(-?[0-9.]*)')[1]);
}
function getY(d) {
return Number(line.radius(d => y(d.VALUE1))([d]).match('M(-?[0-9.]*),(-?[0-9.]*)')[2]);
}
function getX2(d) {
return Number(line.radius(d => y(d.VALUE2))([d]).match('M(-?[0-9.]*),(-?[0-9.]*)')[1]);
}
function getY2(d) {
return Number(line.radius(d => y(d.VALUE2))([d]).match('M(-?[0-9.]*),(-?[0-9.]*)')[2]);
}
// +++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++ ++++++++++++++++++++++++++++++++
// separate data in positive and negative value
let separated = rawdata2.reduce((a, b,index) => {
let sub = b.VALUE1 - b.VALUE2;
if (sub > 0) {
return {positive: [...a.positive, {...b,index}], negative: [...a.negative]};
}
return {positive: [...a.positive], negative: [...a.negative, {...b,index}]};
}, {positive: [], negative: []})
let positiveSets = [];
let previous = undefined
separated.positive.forEach((e)=>{
//if not contiguous
if (!previous || previous.index + 1 !== e.index) {
// create a new set
positiveSets.push([e]);
} else {
// append value to previous set
positiveSets[positiveSets.length - 1] = [...positiveSets[positiveSets.length - 1], e]
}
previous = e;
})
//same for negatives
let negativeSets = [];
previous = undefined
separated.negative.forEach((e)=>{
if (!previous || previous.index + 1 !== e.index) {
negativeSets.push([e]);
} else {
negativeSets[negativeSets.length - 1] = [...negativeSets[negativeSets.length - 1], e]
}
previous = e;
})
negativeSets =negativeSets.map(s=>{
let first = s[0];
let last = s[s.length-1];
let next;
let prev;
if (first.index > 0){
prev = rawdata2[first.index-1]
}else{
prev = rawdata2[rawdata2.length-1]
}
if(last.index === rawdata2.length-1){
next = rawdata2[0];
}else{
next = rawdata2[last.index +1];
}
let w = checkLineIntersection(getX(prev),getY(prev),getX(first),getY(first),getX2(prev),getY2(prev),getX2(first),getY2(first))
let n = checkLineIntersection(getX(last),getY(last),getX(next),getY(next),getX2(last),getY2(last),getX2(next),getY2(next))
return [{x1:w.x,y1:w.y,x2:w.x,y2:w.y},...s.map(w=>({x1:getX(w),y1:getY(w),x2:getX2(w),y2:getY2(w)})),{x1:n.x,y1:n.y,x2:n.x,y2:n.y}]
})
positiveSets =positiveSets.map(s=>{
let first = s[0];
let last = s[s.length-1];
let next;
let prev;
if (first.index > 0){
prev = rawdata2[first.index-1]
}else{
prev = rawdata2[rawdata2.length-1]
}
if(last.index === rawdata2.length-1){
next = rawdata2[0];
}else{
next = rawdata2[last.index +1];
}
let w = checkLineIntersection(getX(prev),getY(prev),getX(first),getY(first),getX2(prev),getY2(prev),getX2(first),getY2(first))
let n = checkLineIntersection(getX(last),getY(last),getX(next),getY(next),getX2(last),getY2(last),getX2(next),getY2(next))
return [{x1:w.x,y1:w.y,x2:w.x,y2:w.y},...s.map(w=>({x1:getX(w),y1:getY(w),x2:getX2(w),y2:getY2(w)})),{x1:n.x,y1:n.y,x2:n.x,y2:n.y}]
})
const negG = svg.selectAll('.negative').data(negativeSets)
negG.enter().append('path')
.attr('class','negative') // "negative" color
.attr('fill','lightblue')
.attr("d", (d)=>d3.area().x0(d => d.x1).x1(d => d.x2).y0(d => d.y1).y1(d => d.y2)(d));
const posG = svg.selectAll('.positive').data(positiveSets)
posG.enter().append('path')
.attr('class','positive') // "negative" color
.attr('fill','darkblue')
.attr("d", (d)=>d3.area().x0(d => d.x1).x1(d => d.x2).y0(d => d.y1).y1(d => d.y2)(d));
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);
// console.log(negG)
const container = d3.select("body").append("div");
container.node().appendChild(svg.node());
#csvdata {
display: none;
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<h1>D3 CSV Example</h1>
我想可视化另一个带有 STATE 列(二进制值)的数据框。我想可视化具有 0 个 STATE 列值的 VALUE1 列和具有 1 个 STATE 列值的 VALUE1 列。我可以为每个数组创建两个数组。
let rawdata3 = rawdata.filter(d => d.STATE== 0);
let rawdata4 = rawdata.filter(d => d.STATE== 1);
但是,我不确定如何更新交叉点部分和负/正区域部分 I 我从两个不同的数组中可视化 VALUE1?
答: 暂无答案
评论