Legendenelemente der Gruppe D3
Wie kann ich den Kreis und den Text für jedes Legendenelement gruppieren?
Ich habe ein Problem mit enter
/ exit
. Ich kann die Gruppen ( g
) erstellen , aber die text
/ circle
Elemente nicht anhängen .
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))
Hier ist das aktuell gerenderte SVG-Markup:
<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>
Dies ist das gewünschte Markup:
<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>
Auf diese Weise kann ich einen Ereignis-Listener hinzufügen .legend-item
und das Datenattribut verwenden key
, um Querverweise mit der Zeilenreihe zu erstellen .
Hinweis: Der folgende Legendencode ist eine aktualisierte Version (6.2.0) von d3-legend . Ich habe den Code leicht angepasst.
Beispiel
Hier ist ein funktionierendes Beispiel für das, was ich bisher habe.
(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>
Antworten
Das Ergebnis von .enter()
ist eine Auswahl, die entsprechende datum
Werte hat, aber keine zugehörigen Knoten hat. Deshalb verschmelzen wir normalerweise erst im allerletzten Moment, nachdem wir angerufen haben .append("g")
.
g = li.selectAll("g")
.data(items);
g
.enter()
.append("g")
.merge(g);
Da Sie nicht g
zweimal anhängen können , müssen Sie ein g
Element und dann ein text
Element anhängen und dann das zurückgeben g
. Das würde ungefähr so aussehen:
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())
Da Sie jedoch d3 v6 verwenden, können Sie auch die neue verwenden .join()Funktionalität, in dem Sie auf die Funktionen Ihrer anwenden können .enter()
, .exit()
und (implizit) Update Teile der Auswahl, und das gibt das fusionierte Ergebnis:
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()
);
Das führt zu folgendem Snippet:
(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>