그룹 D3 전설 아이템
Nov 19 2020
각 범례 항목의 원과 텍스트를 어떻게 그룹화 할 수 있습니까?
enter
/에 문제가 exit
있습니다. 그룹 ( g
)을 만들 수 있지만 text
/ circle
요소를 추가 할 수는 없습니다 .
li.selectAll("g")
.data(items, ([key]) => key)
.call((d) => d.enter().append("text")
.attr("y", (d, i) => (i * 1.25) + "em")
.attr("x", "1em")
.text(([key]) => key))
.call((d) => d.exit().remove())
.call((d) => d.enter().append("circle")
.attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
.attr("cx", 0)
.attr("r", "0.4em")
.style("fill", ([key, value]) => value.color))
현재 렌더링 된 SVG 마크 업은 다음과 같습니다.
<g class="legend-items">
<text y="0em" x="1em">Amanda</text>
<text y="1.25em" x="1em">Linda</text>
<text y="2.5em" x="1em">Dorothy</text>
<text y="3.75em" x="1em">Betty</text>
<text y="5em" x="1em">Helen</text>
<text y="6.25em" x="1em">Patricia</text>
<text y="7.5em" x="1em">Jessica</text>
<text y="8.75em" x="1em">Ashley</text>
<text y="10em" x="1em">Deborah</text>
<circle cy="-0.25em" cx="0" r="0.4em" style="fill: rgb(228, 26, 28);"></circle>
<circle cy="1em" cx="0" r="0.4em" style="fill: rgb(247, 129, 191);"></circle>
<circle cy="2.25em" cx="0" r="0.4em" style="fill: rgb(255, 127, 0);"></circle>
<circle cy="3.5em" cx="0" r="0.4em" style="fill: rgb(77, 175, 74);"></circle>
<circle cy="4.75em" cx="0" r="0.4em" style="fill: rgb(255, 255, 51);"></circle>
<circle cy="6em" cx="0" r="0.4em" style="fill: rgb(153, 153, 153);"></circle>
<circle cy="7.25em" cx="0" r="0.4em" style="fill: rgb(166, 86, 40);"></circle>
<circle cy="8.5em" cx="0" r="0.4em" style="fill: rgb(55, 126, 184);"></circle>
<circle cy="9.75em" cx="0" r="0.4em" style="fill: rgb(152, 78, 163);"></circle>
</g>
다음은 원하는 마크 업입니다.
<g class="legend-items">
<g class="legend-item" data-key="Amanda">
<text y="0em" x="1em">Amanda</text>
<circle cy="-0.25em" cx="0" r="0.4em" style="fill: rgb(228, 26, 28);"></circle>
</g>
<g class="legend-item" data-key="Linda">
<text y="1.25em" x="1em">Linda</text>
<circle cy="1em" cx="0" r="0.4em" style="fill: rgb(247, 129, 191);"></circle>
</g>
<g class="legend-item" data-key="Dorothy">
<text y="2.5em" x="1em">Dorothy</text>
<circle cy="2.25em" cx="0" r="0.4em" style="fill: rgb(255, 127, 0);"></circle>
</g>
<g class="legend-item" data-key="Betty">
<text y="3.75em" x="1em">Betty</text>
<circle cy="3.5em" cx="0" r="0.4em" style="fill: rgb(77, 175, 74);"></circle>
</g>
<g class="legend-item" data-key="Helen">
<text y="5em" x="1em">Helen</text>
<circle cy="4.75em" cx="0" r="0.4em" style="fill: rgb(255, 255, 51);"></circle>
</g>
<g class="legend-item" data-key="Patricia">
<text y="6.25em" x="1em">Patricia</text>
<circle cy="6em" cx="0" r="0.4em" style="fill: rgb(153, 153, 153);"></circle>
</g>
<g class="legend-item" data-key="Jessica">
<text y="7.5em" x="1em">Jessica</text>
<circle cy="7.25em" cx="0" r="0.4em" style="fill: rgb(166, 86, 40);"></circle>
</g>
<g class="legend-item" data-key="Ashley">
<text y="8.75em" x="1em">Ashley</text>
<circle cy="8.5em" cx="0" r="0.4em" style="fill: rgb(55, 126, 184);"></circle>
</g>
<g class="legend-item" data-key="Deborah">
<text y="10em" x="1em">Deborah</text>
<circle cy="9.75em" cx="0" r="0.4em" style="fill: rgb(152, 78, 163);"></circle>
</g>
</g>
이렇게하면 이벤트 리스너를 추가 .legend-item
하고 데이터 속성 key
을 사용하여 라인 시리즈와 상호 참조 할 수 있습니다 .
참고 : 아래의 범례 코드 는 d3-legend 의 업데이트 된 버전 (6.2.0)입니다 . 코드를 약간 수정했습니다.
예
여기 내가 지금까지 가지고있는 작업의 예가 있습니다.
(function() {
d3.legend = function(g) {
g.each(function() {
var g = d3.select(this),
items = {},
svg = d3.select(g.property("nearestViewportElement")),
legendPadding = g.attr("data-style-padding") || 5,
lb = g.selectAll(".legend-box").data([true]),
li = g.selectAll(".legend-items").data([true]);
lb = lb.enter().append("rect").classed("legend-box", true).merge(lb);
li = li.enter().append("g").classed("legend-items", true).merge(li);
svg.selectAll("[data-legend]").each(function() {
var self = d3.select(this);
items[self.attr("data-legend")] = {
pos: self.attr("data-legend-pos") || this.getBBox().y,
color: self.attr("data-legend-color") ||
self.style("stroke") ||
self.style("fill")
};
});
items = Object.entries(items)
.sort(([keyA, valA], [keyB, valB]) => valA.pos - valB.pos);
li.selectAll("text")
.data(items, ([key]) => key)
.call((d) => d.enter().append("text")
.attr("y", (d, i) => (i * 1.25) + "em")
.attr("x", "1em")
.text(([key]) => key))
.call((d) => d.exit().remove())
li.selectAll("circle")
.data(items, ([key]) => key)
.call((d) => d.enter().append("circle")
.attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
.attr("cx", 0)
.attr("r", "0.4em")
.style("fill", ([key, value]) => value.color))
// Reposition and resize the box
var lbbox = li.node().getBBox();
lb.attr("x", lbbox.x - legendPadding)
.attr("y", lbbox.y - legendPadding)
.attr("height", lbbox.height + 2 * legendPadding)
.attr("width", lbbox.width + 2 * legendPadding);
});
return g;
};
})();
const apiUrl = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv";
const palette = [
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999"
];
const chartConfig = {
groupingKey: "name",
xField: "year",
yField: "n"
};
const mapperFn = ({ year, n, prop, ...rest }) => ({
...rest,
year: parseInt(year, 10),
n: parseFloat(n),
prop: parseFloat(prop)
});
const { groupingKey, xField, yField } = chartConfig;
const margin = { top: 10, right: 120, bottom: 20, left: 60 },
width = 400 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
d3.csv(apiUrl).then((jsonData) => {
const data = jsonData.map(mapperFn);
const ref = document.querySelector('.d3-chart > svg');
const svgElement = d3.select(ref);
// Empty the children
svgElement.selectAll("*").remove();
// Inner-SVG
const svgInner = svgElement
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Group the data: I want to draw one line per group
const groupedData = [ ...d3.group(data, (d) => d[groupingKey]) ]
.map(([key, values]) => ({ key, values }));
const halfway = Math.floor(groupedData.length / 2);
const dataLeft = groupedData.slice(0, halfway);
const dataRight = groupedData.slice(halfway);
// Add X axis --> it is a date format
var xScale = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d[xField]))
.range([0, width]);
svgInner
.append("g")
.attr("class", "d3-axis d3-axis-bottom")
.attr("transform", `translate(0, ${height})`) .call( d3 .axisBottom(xScale) .ticks(2) .tickFormat((year) => year.toString()) ); // Add Y axis var yScaleLeft = d3 .scaleLinear() .domain([ 0, d3.max( dataLeft.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); var yScaleRight = d3 .scaleLinear() .domain([ 0, d3.max( dataRight.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .call(d3.axisLeft(yScaleLeft)); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .attr("transform", `translate(${width}, 0)`)
.call(d3.axisRight(yScaleRight));
// Color palette
var res = groupedData.map((d) => d.key).sort(); // list of group names
var color = d3.scaleOrdinal().domain(res).range(palette);
svgInner
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Draw the line
svgInner
.selectAll(".line")
.data(dataLeft)
.enter()
.append("path")
.attr("fill", "none")
.attr("data-legend", (d) => d.key)
.attr("stroke", (d) => color(d.key))
.attr("stroke-width", 1.5)
.attr("d", function(d) {
return d3
.line()
.x((d) => xScale(d[xField]))
.y((d) => yScaleLeft(+d[yField]))(d.values);
});
svgInner
.selectAll(".line")
.data(dataRight)
.enter()
.append("path")
.attr("fill", "none")
.attr("data-legend", (d) => d.key)
.attr("stroke", (d) => color(d.key))
.attr("stroke-width", 1.5)
.attr("d", function(d) {
return d3
.line()
.x((d) => xScale(d[xField]))
.y((d) => yScaleRight(+d[yField]))(d.values);
});
let legend = svgInner
.append("g")
.attr("class", "legend")
.attr("transform", "translate(280,20)")
.style("font-size", "12px")
.call(d3.legend);
});
:root {
--chart-background: #272727;
--chart-foreground: #d7d7d7;
}
.d3-chart {
background: var(--chart-background);
}
.d3-chart .d3-axis line,
.d3-chart .d3-axis path {
stroke: var(--chart-foreground);
}
.d3-chart .d3-axis text {
fill: var(--chart-foreground);
}
.d3-chart .legend-box {
fill: none;
}
.d3-chart .legend .legend-items text {
fill: var(--chart-foreground);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div class="d3-chart">
<svg width="400px" height="200px" />
</div>
답변
2 RubenHelsloot Nov 19 2020 at 22:22
결과 .enter()
는 해당 datum
값이 있지만 관련된 노드가없는 선택입니다 . 그렇기 때문에 우리는 일반적으로 를 호출 한 후 마지막 순간에만 병합합니다 .append("g")
.
g = li.selectAll("g")
.data(items);
g
.enter()
.append("g")
.merge(g);
g
두 번 추가 할 수 없기 때문에 g
요소 를 추가 한 다음 요소 를 추가 한 text
다음 g
. 이것은 다음과 같이 보일 것입니다.
li.selectAll("g")
.data(items, ([key]) => key)
.call((d) => {
const g = d.enter()
.append("g");
g.append("text")
.attr("y", (d, i) => (i * 1.25) + "em")
.attr("x", "1em")
.text(([key]) => key);
g.append("circle")
.attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
.attr("cx", 0)
.attr("r", "0.4em")
.style("fill", ([key, value]) => value.color)
return g;
})
.call((d) => d.exit().remove())
당신이 D3 버전 6을 사용하고 있기 때문에, 당신은 또한 새로운 사용할 수 있습니다 .join()당신은 당신에 기능을 적용 할 수있는, 기능을 .enter()
, .exit()
그리고 선택 (암시) 업데이트 부품 및 병합 된 결과를 반환합니다 :
li.selectAll("g")
.data(items, ([key]) => key)
.join(
enter => {
const g = enter.append("g");
g.append("text")
.attr("y", (d, i) => (i * 1.25) + "em")
.attr("x", "1em")
.text(([key]) => key);
g.append("circle")
.attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
.attr("cx", 0)
.attr("r", "0.4em")
.style("fill", ([key, value]) => value.color);
return g;
},
update => update,
exit => exit.remove()
);
다음 스 니펫으로 이어집니다.
(function() {
d3.legend = function(g) {
g.each(function() {
var g = d3.select(this),
items = {},
svg = d3.select(g.property("nearestViewportElement")),
legendPadding = g.attr("data-style-padding") || 5,
lb = g.selectAll(".legend-box").data([true]),
li = g.selectAll(".legend-items").data([true]);
lb = lb.enter().append("rect").classed("legend-box", true).merge(lb);
li = li.enter().append("g").classed("legend-items", true).merge(li);
svg.selectAll("[data-legend]").each(function() {
var self = d3.select(this);
items[self.attr("data-legend")] = {
pos: self.attr("data-legend-pos") || this.getBBox().y,
color: self.attr("data-legend-color") ||
self.style("stroke") ||
self.style("fill")
};
});
items = Object.entries(items)
.sort(([keyA, valA], [keyB, valB]) => valA.pos - valB.pos);
li.selectAll("g")
.data(items, ([key]) => key)
.join(
enter => {
const g = enter.append("g");
g.append("text")
.attr("y", (d, i) => (i * 1.25) + "em")
.attr("x", "1em")
.text(([key]) => key);
g.append("circle")
.attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
.attr("cx", 0)
.attr("r", "0.4em")
.style("fill", ([key, value]) => value.color);
return g;
},
update => update,
exit => exit.remove()
);
// Reposition and resize the box
var lbbox = li.node().getBBox();
lb.attr("x", lbbox.x - legendPadding)
.attr("y", lbbox.y - legendPadding)
.attr("height", lbbox.height + 2 * legendPadding)
.attr("width", lbbox.width + 2 * legendPadding);
});
return g;
};
})();
const apiUrl = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv";
const palette = [
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999"
];
const chartConfig = {
groupingKey: "name",
xField: "year",
yField: "n"
};
const mapperFn = ({ year, n, prop, ...rest }) => ({
...rest,
year: parseInt(year, 10),
n: parseFloat(n),
prop: parseFloat(prop)
});
const { groupingKey, xField, yField } = chartConfig;
const margin = { top: 10, right: 120, bottom: 20, left: 60 },
width = 400 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
d3.csv(apiUrl).then((jsonData) => {
const data = jsonData.map(mapperFn);
const ref = document.querySelector('.d3-chart > svg');
const svgElement = d3.select(ref);
// Empty the children
svgElement.selectAll("*").remove();
// Inner-SVG
const svgInner = svgElement
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Group the data: I want to draw one line per group
const groupedData = [ ...d3.group(data, (d) => d[groupingKey]) ]
.map(([key, values]) => ({ key, values }));
const halfway = Math.floor(groupedData.length / 2);
const dataLeft = groupedData.slice(0, halfway);
const dataRight = groupedData.slice(halfway);
// Add X axis --> it is a date format
var xScale = d3
.scaleLinear()
.domain(d3.extent(data, (d) => d[xField]))
.range([0, width]);
svgInner
.append("g")
.attr("class", "d3-axis d3-axis-bottom")
.attr("transform", `translate(0, ${height})`) .call( d3 .axisBottom(xScale) .ticks(2) .tickFormat((year) => year.toString()) ); // Add Y axis var yScaleLeft = d3 .scaleLinear() .domain([ 0, d3.max( dataLeft.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); var yScaleRight = d3 .scaleLinear() .domain([ 0, d3.max( dataRight.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .call(d3.axisLeft(yScaleLeft)); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .attr("transform", `translate(${width}, 0)`)
.call(d3.axisRight(yScaleRight));
// Color palette
var res = groupedData.map((d) => d.key).sort(); // list of group names
var color = d3.scaleOrdinal().domain(res).range(palette);
svgInner
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// Draw the line
svgInner
.selectAll(".line")
.data(dataLeft)
.enter()
.append("path")
.attr("fill", "none")
.attr("data-legend", (d) => d.key)
.attr("stroke", (d) => color(d.key))
.attr("stroke-width", 1.5)
.attr("d", function(d) {
return d3
.line()
.x((d) => xScale(d[xField]))
.y((d) => yScaleLeft(+d[yField]))(d.values);
});
svgInner
.selectAll(".line")
.data(dataRight)
.enter()
.append("path")
.attr("fill", "none")
.attr("data-legend", (d) => d.key)
.attr("stroke", (d) => color(d.key))
.attr("stroke-width", 1.5)
.attr("d", function(d) {
return d3
.line()
.x((d) => xScale(d[xField]))
.y((d) => yScaleRight(+d[yField]))(d.values);
});
let legend = svgInner
.append("g")
.attr("class", "legend")
.attr("transform", "translate(280,20)")
.style("font-size", "12px")
.call(d3.legend);
});
:root {
--chart-background: #272727;
--chart-foreground: #d7d7d7;
}
.d3-chart {
background: var(--chart-background);
}
.d3-chart .d3-axis line,
.d3-chart .d3-axis path {
stroke: var(--chart-foreground);
}
.d3-chart .d3-axis text {
fill: var(--chart-foreground);
}
.d3-chart .legend-box {
fill: none;
}
.d3-chart .legend .legend-items text {
fill: var(--chart-foreground);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div class="d3-chart">
<svg width="400px" height="200px" />
</div>