英文:
How to apply conditional filling in D3?
问题
// ++++++++++++++++++++++++++++++++++++++++ 这是常量变量,用于定义可视化的尺寸。width 和 height 变量定义了包含可视化的 SVG 元素的总体尺寸。margin 变量用于设置 SVG 元素和图表之间的边距。innerRadius 和 outerRadius 变量定义了用于表示数据的两个圆的半径。++++++++++++++++++++++++ */
const width = 954;
const height = width;
const margin = 10;
const innerRadius = width / 5;
const outerRadius = width / 2 - margin;
/* ++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++ +++++++++++++++++++++++++++++*/
// ++++++++++++++++++++++++++++++++++++++++ 步骤 1) 解析 CSV 文件 ++++++++++++++++++++++++++++
d3.csv("synthetic_data.csv", function(d) {
// 解析 CSV 数据并返回一个具有所需属性的 JavaScript 对象
return {
DATE_TIME: new Date(d.DATE_TIME),
VALUE1: +d.VALUE1,
VALUE2: +d.VALUE2,
INSPECTION: +d.INSPECTION,
};
}).then(function(rawdata) {
// 将 DATE_TIME 列转换为分钟精确时间
rawdata.forEach(function(d) {
const hour = d.DATE_TIME.getHours();
const minute = d.DATE_TIME.getMinutes();
d.minuteTime = hour * 60 + minute;
});
let rawdata2 = rawdata.filter(d => d.INSPECTION == 1);
const minuteScale = d3.scaleLinear()
.domain([0, 1439]) // 0 到 23:59 (24 小时 * 60 分钟,不减去 -1,否则改为 1439 - 1)
.range([0, 2 * Math.PI]);
const minuteTimeValues = rawdata2.map(d => d.minuteTime);
const x = d3.scaleTime()
.domain([0, 1439]) // 使用“minuteTime”值的范围设置域
.range([0, 2 * Math.PI]); // 设置所需的范围
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]);
// 修改 xAxis 生成器以仅显示小时并添加填充
const xAxis = (g) => g
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.call((g) => g.selectAll("g")
.data(d3.range(0, 24)) // 创建一个小时值数组
.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; // 减小半径以将标签移近中心
return `translate(${d3.pointRadial(x(d * 60), radius)})`;
})
)
);
const yAxis = g => g
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.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.2)
.attr("r", y))
.call(g => g.append("text")
.attr("y", d => -y(d))
.attr("dy", "0.35em")
.attr("stroke", "#fff")
.attr("stroke-width", 5)
.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.curveLinearClosed)
.angle(d => x(d.minuteTime));
const area = d3.areaRadial()
.curve(d3.curveLinearClosed)
.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
<details>
<summary>英文:</summary>
My CSV file looks like this
```csv
DATE_TIME,ID,HOUR,MINUTE,VALUE1,VALUE2,TIME,INSPECTION,seconds
2018-11-01 00:00:00,1,0,0,131.8403388480975,163.40706930229611,00:00:00,1,0
2018-11-01 00:20:00,1,0,20,128.66143180880016,182.4166850946821,00:20:00,1,20
2018-11-01 00:40:00,1,0,40,157.9838672230082,177.12826879097582,00:40:00,1,40
2018-11-01 01:00:00,1,1,0,154.74491894762372,177.02056153432287,01:00:00,1,60
2018-11-01 01:20:00,1,1,20,145.17425322202857,170.28572211898428,01:20:00,1,80
2018-11-01 01:40:00,1,1,40,86.33692934233684,161.306233500133004,01:40:00,1,100
2018-11-01 02:00:00,1,2,0,157.70472270832147,175.57007397898963,02:00:00,1,120
2018-11-01 02:20:00,1,2,20,92.04378773072385,152.758855696098315,02:20:00,1,140
2018-11-01 02:40:00,1,2,40,94.91482763887929,164.34713115356462,02:40:00,1,160
2018-11-01 03:00:00,1,3,0,150.58752716935703,152.573995003487525,03:00:00,1,180
2018-11-01 03:20:00,1,3,20,147.64763286003245,181.7346317353725,03:20:00,1,200
2018-11-01 03:40:00,1,3,40,152.00311766772614,169.43438782829136,03:40:00,1,220
2018-11-01 04:00:00,1,4,0,90.67568616871652,165.94364405219294,04:00:00,1,240
2018-11-01 04:20:00,1,4,20,127.6260776280666,154.41327889693226,04:20:00,1,260
2018-11-01 04:40:00,1,4,40,88.35193693321801,86.7514997924824,04:40:00,1,280
2018-11-01 05:00:00,1,5,0,99.44723697225662,61.81705113015809,05:00:00,1,300
2018-11-01 05:20:00,1,5,20,153.28490595395064,70.71671056760238,05:20:00,1,320
2018-11-01 05:40:00,1,5,40,139.53124376248127,86.96902617467873,05:40:00,1,340
2018-11-01 06:00:00,1,6,0,144.5960580946409,53.46091762212306,06:00:00,1,360
2018-11-01 06:20:00,1,6,20,110.05611978038519,73.32550566412914,06:20:00,1,380
2018-11-01 06:40:00,1,6,40,151.21482961729612,50.858838256056316,06:40:00,1,400
2018-11-01 07:00:00,1,7,0,127.88766918064593,52.45266678876003,07:00:00,1,420
2018-11-01 07:20:00,1,7,20,148.72830141013,60.496196118302194,07:20:00,1,440
2018-11-01 07:40:00,1,7,40,83.62908148257569,86.54612924908034,07:40:00,1,460
2018-11-01 08:00:00,1,8,0,113.49475177931873,59.15439348393694,08:00:00,1,480
2018-11-01 08:20:00,1,8,20,139.37377082538475,86.1347823168292,08:20:00,1,500
2018-11-01 08:40:00,1,8,40,104.5216263376223,86.92624219265554,08:40:00,1,520
2018-11-01 09:00:00,1,9,0,152.55680056900144,60.938994790632144,09:00:00,1,540
2018-11-01 09:20:00,1,9,20,140.38896796205978,56.19504976107255,09:20:00,1,560
2018-11-01 09:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 10:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 11:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 12:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 13:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 14:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 15:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 16:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 17:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 18:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 19:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 20:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 21:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 22:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 23:40:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
2018-11-01 23:59:00,1,9,40,135.79579769996855,62.50941443441722,09:40:00,1,580
I would like to apply conditional filling so that the area where VALUE1 value bigger than VALUE2 is blue, and VALUE2 value bigger than VALUE1 is gray. But, I end up seeing only gray color.
Here is my complete code:
<!--
This is an HTML document that includes a D3.js script. It starts with the head section where the document's title, character encoding, and D3.js library are included. Then, the document's body starts with a header h1 tag, followed by a script tag where the D3.js code is written.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>D3 CSV Example</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<h1>D3 CSV Example</h1>
<script>
/* ++++++++++++++++++++++++ These are constant variables that define the dimensions of the visualization. The width and height variables define the overall dimensions of the SVG element that will contain the visualization. The margin variable is used to set a margin between the SVG element and the chart. The innerRadius and outerRadius variables define the radii of the two circles that will be used to represent the data. ++++++++++++++++ */
const width = 954;
const height = width;
const margin = 10;
const innerRadius = width / 5;
const outerRadius = width / 2 - margin;
/* ++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++ +++++++++++++++++++++++++++++*/
// ++++++++++++++++++++++++++++++++++++++++ Step 1) Parsing the CSV file ++++++++++++++++++++++++++++
d3.csv("synthetic_data.csv", function(d) {
// Parse the CSV data and return a JavaScript object
// with the desired properties
return {
DATE_TIME: new Date(d.DATE_TIME),
VALUE1: +d.VALUE1,
VALUE2: +d.VALUE2,
INSPECTION: +d.INSPECTION,
};
}).then(function(rawdata) {
// Convert DATE_TIME column to minute-precise time
rawdata.forEach(function(d) {
const hour = d.DATE_TIME.getHours();
const minute = d.DATE_TIME.getMinutes();
d.minuteTime = hour * 60 + minute;
});
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", 10)
.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", 10)
.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.2)
.attr("r", y))
.call(g => g.append("text")
.attr("y", d => -y(d))
.attr("dy", "0.35em")
.attr("stroke", "#fff")
.attr("stroke-width", 5)
.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.curveLinearClosed)
.angle(d => x(d.minuteTime));
const area = d3.areaRadial()
.curve(d3.curveLinearClosed)
.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));
svg.append("path")
.attr("fill", function(_, i) {
const d = rawdata2[i];
console.log(d.VALUE1);
if (d && d.VALUE1 > d.VALUE2) {
return "blue";
} else {
return "gray";
}
})
.attr("stroke", "none")
.attr("d", area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
(rawdata2));
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);
const container = d3.select("body").append("div");
container.node().appendChild(svg.node());
});
</script>
</body>
</html>
How can I fix this? My expectation is to see blue area color from beginning of label 0 to until middle of label 4 and label 5. How can I achieve this? I want to fill the area in blue where the value of VALUE1 is greater than the value of VALUE2, and in gray where the value of VALUE2 is greater than the value of VALUE1.
EDIT: Based on answer of @pernifloss, I changed conditional filling by defininig two areas as following:
const positiveData = rawdata2.filter(d => d.VALUE1 > d.VALUE2);
const negativeData = rawdata2.filter(d => d.VALUE2 > d.VALUE1);
svg.selectAll(".positive-area")
.data([positiveData])
.enter()
.append("path")
.attr("class", "positive-area")
.attr("fill", "blue")
.attr("stroke", "none")
.attr("d", area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
);
svg.selectAll(".negative-area")
.data([negativeData])
.enter()
.append("path")
.attr("class", "negative-area")
.attr("fill", "gray")
.attr("stroke", "none")
.attr("d", area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
);
However, I ended up with this plot.
How can I make apply conditional filling, while keeping circular shape and circular areas? I do not want to have rectangular like areas.
答案1
得分: 1
你不能真正拥有一条带有多种颜色的路径。
我唯一看到的解决方法是创建多个区域。
将数据分为多组连续的“正”或“负”区域,然后为每个数据集绘制一个区域。
[编辑] 这是迄今为止我的代码,结果在https://imgur.com/FH6Xim7...
// 将数据分为正值和负值
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: [] })
const positiveSets = [];
let previous = undefined
separated.positive.forEach((e) => {
// 如果不连续
if (!previous || previous.index + 1 !== e.index) {
// 创建一个新的集合
positiveSets.push([e]);
} else {
// 将值附加到前一个集合
positiveSets[positiveSets.length - 1] = [...positiveSets[positiveSets.length - 1], e]
}
previous = e;
})
// 负值同样如此
const 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;
})
我的区域函数:
const area = d3.areaRadial()
.curve(d3.curveLinear)
.angle(d => x(d.minuteTime))
然后将所有区域附加到图形:
const posG = svg.selectAll('.positive').data(positiveSets)
posG.enter().append('path')
.attr('class', 'positive')
.attr('fill', 'blue') // "positive" 颜色
.attr("d", (d) => area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
(d));
const negG = svg.selectAll('.negative').data(negativeSets)
negG.enter().append('path')
.attr('class', 'negative') // "negative" 颜色
.attr('fill', 'red')
.attr("d", (d) => area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
(d));
英文:
You can't really have one path with multiple colors.
The only solution I see is to make multiple areas.
Separate your data in multiple set of contiguous “positive” or “negative” area. Then draw an area for each dataset.
[edit] here is my code so far, the result https://imgur.com/FH6Xim7 ...
// 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: []})
const 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
const 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;
})
My area function:
const area = d3.areaRadial()
.curve(d3.curveLinear)
.angle(d => x(d.minuteTime))
en then append all area to graph :
const posG = svg.selectAll('.positive').data(positiveSets)
posG.enter().append('path')
.attr('class','positive')
.attr('fill','blue') // "positive" color
.attr("d", (d)=>area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
(d));
const negG = svg.selectAll('.negative').data(negativeSets)
negG.enter().append('path')
.attr('class','negative') // "negative" color
.attr('fill','red')
.attr("d", (d)=>area
.innerRadius(d => y(d.VALUE1))
.outerRadius(d => y(d.VALUE2))
(d));
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论