How can I group the circle and text for each legend item?
I am having an issue with enter/exit. I can create the groups (g), but I cannot append the text/circle elements.
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))
Here is the current rendered 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>
This is the desired 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>
This way I can add an event listener to .legend-item and use the data attribute key to cross-reference with the line-series.
Note: The legend code below is an updated version (6.2.0) of d3-legend. I tweaked the code slightly.
Example
Here is a working example of what I have so far.
(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>
The result of .enter() is a selection that has corresponding datum values, but does not have any nodes related to it. That is why we normally only merge at the very last moment, after having called .append("g").
g = li.selectAll("g")
.data(items);
g
.enter()
.append("g")
.merge(g);
Because you can't append g twice, you would need to append a g element, then a text element, and then return the g. This would look something like this:
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())
However, since you're using d3 v6, you can also use the new .join() functionality, where you can apply functions to your .enter(), .exit() and (implicit) update parts of the selection, and which returns the merged result:
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()
);
That leads to the following 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>
Related
Really struggling with this. I've tried a lot of hacks to get the labels to render correctly on my force directed d3 graph. Here is a stackBlitz
You will notice in all other browsers except IE11 renders ok.
In IE:
As you can see in IE11 the first <tspan> isn't anchored to the middle. I've tried a combination of things with and without the x attribute, dx manipulation. I have lined them up via the dx attribute but as each set of labels have different lengths the math isn't the same for each.
The svg structure is as follows:
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
<tspan class="edgelabels" dx="0">label one</tspan>
<tspan class="edgelabels" x="0" dy="22" dx="0">label two</tspan>
<tspan class="edgelabels" x="0" dy="22" dx="0">label three</tspan>
</textPath>
</text>
You can see above in this particular implementation I intentionally left out the x attribute for the first tspan.
This is the code that renders the above:
const edgelabels = zoomContainer
.selectAll('.edgelabel')
.data(links)
.enter()
.append('text')
.attr('class', 'edgelabel')
.style('text-anchor', 'middle')
.attr('id', function (d, i) {
return 'edgelabel' + i;
})
.attr('font-size', 10);
edgelabels
.append('textPath')
.attr('xlink:href', function (d, i) {
return '#edgepath' + i;
})
.style('cursor', 'pointer')
.attr('dominant-baseline', 'middle')
.attr('startOffset', '50%')
.selectAll('div.textPath')
.data(function (d, i) {
return d.label;
})
.enter()
.append('tspan')
.attr('class', 'edgelabels')
.text(function (d, i) {
return console.log(d), d;
})
.attr('x', function (d, i) {
if (i > 0) {
return 0;
}
})
.attr('dy', function (d, i) {
if (i > 0) {
return 22;
}
})
.attr('dx', 0);
Has anybody else had this issue, can you see anything wrong? Is there anything else I could try to get a consistent dx attribute for each set of labels? Could I restructure the code to get a better result?
Complete file code:
import { Injectable } from '#angular/core';
import * as d3 from 'd3';
#Injectable({
providedIn: 'root',
})
export class DirectedGraphExperimentService {
constructor() {}
/** A method to bind a zoom behaviour to the svg g element */
public applyZoomableBehaviour(svgElement, containerElement) {
let svg, container, zoomed, zoom;
svg = d3.select(svgElement);
container = d3.select(containerElement);
zoomed = () => {
const transform = d3.event.transform;
container.attr(
'transform',
'translate(' +
transform.x +
',' +
transform.y +
') scale(' +
transform.k +
')'
);
};
zoom = d3.zoom().scaleExtent([0.5, 1]).on('zoom', zoomed);
svg.call(zoom).style('cursor', 'move');
}
private clearView(svg) {
return svg.selectAll('*').remove();
}
private ticked(link, node, edgepaths, edgelabels) {
link
.attr('x1', function (d) {
return d.source.x;
})
.attr('y1', function (d) {
return d.source.y;
})
.attr('x2', function (d) {
return d.target.x;
})
.attr('y2', function (d) {
return d.target.y;
});
node.attr('transform', function (d) {
return 'translate(' + d.x + ', ' + d.y + ')';
});
edgepaths.attr('d', function (d) {
return (
'M ' +
d.source.x +
' ' +
d.source.y +
' L ' +
d.target.x +
' ' +
d.target.y
);
});
edgelabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
let bbox = this.getBBox();
let rx = bbox.x + bbox.width / 2;
let ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
} else {
return 'rotate(0)';
}
});
}
private dragended(d3, d, simulation) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
private initDefinitions(svg) {
svg
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 27)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 8)
.attr('markerHeight', 8)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
return svg;
}
private forceSimulation(d3, { width, height }) {
return d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id(function (d) {
return d.id;
})
.distance(500)
.strength(2)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
}
private _update(d3, svg, data) {
let { links, nodes } = data;
// this.clearView(svg); // removes everything!
this.initDefinitions(svg);
const simulation = this.forceSimulation(d3, {
width: +svg.attr('width'),
height: +svg.attr('height'),
});
const zoomContainer = d3.select('svg g');
const link = zoomContainer
.selectAll('.link')
.data(links)
.enter()
.append('line')
.style('stroke', '#999')
.style('stroke-opacity', '.6')
.style('stroke-width', '2px')
.attr('class', 'link')
.attr('marker-end', 'url(#arrowhead)');
link.append('title').text(function (d) {
return d.label;
});
const edgepaths = zoomContainer
.selectAll('.edgepath')
.data(links)
.enter()
.append('path')
.attr('class', 'edgepath')
.attr('fill-opacity', 0)
.attr('stroke-opacity', 0)
.attr('id', function (d, i) {
return 'edgepath' + i;
});
const edgelabels = zoomContainer
.selectAll('.edgelabel')
.data(links)
.enter()
.append('text')
.attr('class', 'edgelabel')
.style('text-anchor', 'middle')
.attr('id', function (d, i) {
return 'edgelabel' + i;
})
.attr('font-size', 10);
edgelabels
.append('textPath')
.attr('xlink:href', function (d, i) {
return '#edgepath' + i;
})
.style('cursor', 'pointer')
.attr('dominant-baseline', 'middle')
.attr('startOffset', '50%')
.selectAll('div.textPath')
.data(function (d, i) {
return d.label;
})
.enter()
.append('tspan')
.attr('class', 'edgelabels')
.text(function (d, i) {
return console.log(d), d;
})
.attr('x', function (d, i) {
if (i > 0) {
return 0;
}
})
.attr('dy', function (d, i) {
if (i > 0) {
return 22;
}
})
.attr('dx', 0);
svg.selectAll('.edgelabel').on('click', function () {
// arrow function will produce this = undefined
d3.selectAll('.edgelabel').style('fill', '#999');
d3.select(this).style('fill', 'blue');
});
const node = zoomContainer
.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.call(
d3
.drag()
.on('start', (d) => this.dragended(d3, d, simulation))
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', (d) => this.dragended(d3, d, simulation))
);
node
.append('circle')
.style('fill', '#fff')
.style('cursor', 'pointer')
.style('fill-opacity', '1')
.style('stroke-opacity', '0.5')
.attr('id', (d, i) => d.id)
.attr('r', 28);
svg.selectAll('circle').on('click', function () {
// arrow function will produce this = undefined
d3.selectAll('.nodeText').style('fill', 'black');
d3.select(this.parentNode).selectAll('.nodeText').style('fill', 'blue');
});
node
.append('image')
.attr('xlink:href', 'https://github.com/favicon.ico')
.attr('x', -10)
.attr('y', -40)
.attr('width', 16)
.attr('class', 'image')
.attr('height', 16);
node
.append('svg:foreignObject')
.attr('class', 'delete-icon')
.html('<div style="color:green;padding-left: 50px;">remove</div>')
.text('delete');
const nodeText = node
.data(nodes)
.append('text')
.style('text-anchor', 'middle')
.style('cursor', 'pointer')
.attr('dy', -3)
.attr('y', -25)
.attr('class', 'nodeText')
.attr('id', 'nodeText');
nodeText
.selectAll('tspan')
.data((d, i) => d.label)
.enter()
.append('tspan')
.attr('class', 'nodeTextTspan')
.text((d) => d)
.style('font-size', '12px')
.attr('x', -10)
.attr('dx', 10)
.attr('dy', 22);
svg.selectAll('.nodeText').on('click', function () {
d3.selectAll('.nodeText').style('fill', 'black');
d3.select(this).style('fill', 'blue');
});
node.append('title').text(function (d) {
return d.id;
});
simulation.nodes(nodes).on('tick', () => {
this.ticked(link, node, edgepaths, edgelabels);
});
simulation.force('link').links(links);
}
public update(data, element) {
const svg = d3.select(element);
return this._update(d3, svg, data);
}
}
UPDATE:
IE11 does not like <tspans> inside of <textPaths> therefore:
I've implemented some of #herrstrietzel solution into my new demo. The <tspans> are now directly inside of the <text> elements and each <text> has a dynamic x/y coordinates to lay it on the <path> as we no longer have <textPaths> to do this for us. I've had to do a lot of the rework inside of the ticked() method to gain access to the latest x/y coords as you do not get these outside. One issue remains:
// gets the coordinates of the edgepaths to use for the <text> positioning
edgepaths.each(function (d) {
source_x = d.source.x;
source_y = d.source.y;
target_x = d.target.x;
target_y = d.target.y;
});
let p1 = { x: source_x, y: source_y };
let p2 = { x: target_x, y: target_y };
// centers the <text> on the path
let textAnchor = this.interpolatedPoint(p1, p2, 0.5);
// Adds the x/y attribute to the <text> elements
edgelabels.attr('x', textAnchor.x).attr('y', textAnchor.y)
The text elements share the same coordinates and therefore share the same path... not sure why it hasn't adjusted inside the loop. The index of the first label that's already there should get the first index coordinate, but for some reason they are all just getting one set of path coordinates. p1 and p2 always give me the last coordinates in the array of edges.
Ok I figured it out. Here is the rendered SVG that works - IE11 doesn't like tspan's inside textPaths, so you have to separate each label into its own text + textPath element with an appropriate dy attribute on the text element.
<svg _ngcontent-c1="" height="600" width="900" style="cursor: move;"><g _ngcontent-c1="" ng-reflect-zoomable-of="[object SVGSVGElement]">
<line class="link" marker-end="url(#arrowhead)" style="stroke: rgb(153, 153, 153); stroke-opacity: 0.6; stroke-width: 2px;" x1="609.1621195478509" y1="362.0472615717732" x2="120.80347004842467" y2="254.41514023225977">
<title>label one,label two,label three</title></line>
<line class="link" marker-end="url(#arrowhead)" style="stroke: rgb(153, 153, 153); stroke-opacity: 0.6; stroke-width: 2px;" x1="620.0342472996117" y1="283.53757565928413" x2="120.80347004842467" y2="254.41514023225977">
<title>really Long Link text,tom</title>
</line>
<path class="edgepath" fill-opacity="0" stroke-opacity="0" id="edgepath0" d="M 609.1621195478509 362.0472615717732 L 120.80347004842467 254.41514023225977"></path><path class="edgepath" fill-opacity="0" stroke-opacity="0" id="edgepath1" d="M 620.0342472996117 283.53757565928413 L 120.80347004842467 254.41514023225977"></path>
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.5402526855469 287.623779296875)" dy="0">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
label one
</textPath>
</text>
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.5402526855469 287.623779296875)" dy="22">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
label two
</textPath>
</text>
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.5402526855469 287.623779296875)" dy="44">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
label three
</textPath>
</text>
<text class="edgelabel" id="edgelabel1" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.2359619140625 260.2969512939453)"><textPath xlink:href="#edgepath1" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;"><tspan class="edgelabels" dx="0">really Long Link text</tspan><tspan class="edgelabels" x="0" dy="22" dx="0">tom</tspan></textPath></text><g class="node" transform="translate(609.1621195478509, 362.0472615717732)"><circle id="5678" r="28" style="fill: rgb(255, 255, 255); cursor: pointer; fill-opacity: 1; stroke-opacity: 0.5;"></circle><image xlink:href="https://github.com/favicon.ico" x="-10" y="-40" width="16" class="image" height="16"></image><foreignObject class="delete-icon">delete</foreignObject><text dy="-3" y="-25" class="nodeText" id="nodeText" style="text-anchor: middle; cursor: pointer;"><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Red</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">BMW</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Fast</tspan></text><title>5678</title></g><g class="node" transform="translate(120.80347004842467, 254.41514023225977)"><circle id="2494EA62" r="28" style="fill: rgb(255, 255, 255); cursor: pointer; fill-opacity: 1; stroke-opacity: 0.5;"></circle><image xlink:href="https://github.com/favicon.ico" x="-10" y="-40" width="16" class="image" height="16"></image><foreignObject class="delete-icon">delete</foreignObject><text dy="-3" y="-25" class="nodeText" id="nodeText" style="text-anchor: middle; cursor: pointer;"><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Cool</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Brown</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Ford Cortina</tspan></text><title>2494EA62</title></g><g class="node" transform="translate(620.0342472996117, 283.53757565928413)"><circle id="123" r="28" style="fill: rgb(255, 255, 255); cursor: pointer; fill-opacity: 1; stroke-opacity: 0.5;"></circle><image xlink:href="https://github.com/favicon.ico" x="-10" y="-40" width="16" class="image" height="16"></image><foreignObject class="delete-icon">delete</foreignObject><text dy="-3" y="-25" class="nodeText" id="nodeText" style="text-anchor: middle; cursor: pointer;"><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Aston</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Martin</tspan></text><title>123</title></g></g><defs><marker id="arrowhead" viewBox="-0 -5 10 10" refX="27" refY="0" orient="auto" markerWidth="8" markerHeight="8" xoverflow="visible"><path d="M 0,-5 L 10 ,0 L 0,5" fill="#999" style="stroke: none;"></path></marker></defs></svg>
Not a proper d3.js solution, but should be able to adapt/include it in your script.
Calculate centered text x/y coordinates and rotation angles according to line/graph
We're using <text> elements placed at the middle of the line/graph replacing <textpath> usage.
Get centered text anchor coordinates replacing startOffset="50%"
Since you know the line's starting and end points, we can easily interpolate a mid point like so:
let p1 = {x:x1, y:y1};
let p2 = {x:x2, y:y2};
let textAnchor = {x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2};
The textAnchor point coordinates will become the <text> elements new x and y atribute values.
Get rotation angle
let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;
This variable is used so set the transform rotate property according to the line's drawing angle.
// example coordinates - should come from your data object
let x1 = line1.x1.baseVal.value;
let x2 = line1.x2.baseVal.value;
let y1 = line1.y1.baseVal.value;
let y2 = line1.y2.baseVal.value;
let p1 = {x:x1, y:y1};
let p2 = {x:x2, y:y2};
/**
* calculte mid point on line
* use LERP helper if you need to place the mid point at other positions:
* e.g t=0.75 = 75%
*/
let textAnchor = interpolatedPoint(p1, p2, 0.5);
// use this for simple centered alignment
//let textAnchor = {x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2};
// move parent <text> element to mid point
textLabel.setAttribute('x', textAnchor.x)
textLabel.setAttribute('y', textAnchor.y);
// add x and dy attributes to tspan for pdeudo-multiline appearance
let lineHeight = parseFloat(window.getComputedStyle(textLabel).fontSize)*1.2;
setTspanMultiline(textLabel, lineHeight)
// rotate according to line center point
let angle = getAngle(p1, p2);
// svg attribute transform
textLabel.setAttribute('transform', `rotate(${angle} ${textAnchor.x} ${textAnchor.y})`)
// css style equivalent
//textLabel.style.transform = `translate(${textAnchor.x}px, ${textAnchor.y}px) rotate(${angle}deg) translate(-${textAnchor.x}px, -${textAnchor.y}px)`;
function setTspanMultiline(textEl, dy){
let tspans = textEl.querySelectorAll('tspan');
let totalDy = (tspans.length-1)*dy;
let x= +textEl.getAttribute('x');
tspans.forEach((tspan, i)=>{
tspan.setAttribute('x', x);
if(i>0){
tspan.setAttribute('dy', dy);
}
})
textEl.setAttribute('dy', totalDy/-2 )
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
// get angle helper
function getAngle(p1, p2) {
let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;
return angle;
}
svg{
width:50em;
border:1px solid #ccc;
overflow:visible;
}
text{
font-size:10px;
}
<svg id="svg" viewBox="0 0 500 100">
<line id="line1" x1="0" x2="100%" y1="25%" y2="75%" stroke="#ccc" stroke-width="1" />
<text x="0" id="textLabel" text-anchor="middle" dominant-baseline="middle">
<tspan class="edgelabels">line one</tspan>
<tspan class="edgelabels">line two long</tspan>
<tspan class="edgelabels">line three</tspan>
<tspan class="edgelabels">line four</tspan>
</text>
</svg>
In the above example I'm using the setTspanMultiline(textEl, dy) helper function to generate the correct x, y and dy values.
<text x="250" id="textLabel" text-anchor="middle" dominant-baseline="middle" y="50" dy="-18" transform="rotate(5.710593137499643 250 50)">
<tspan class="edgelabels" x="250">line one</tspan>
<tspan class="edgelabels" x="250" dy="12">line two long</tspan>
<tspan class="edgelabels" x="250" dy="12">line three</tspan>
<tspan class="edgelabels" x="250" dy="12">line four</tspan>
</text>
Basically all <tspan> inherit the x value from the parent <text> elements.
The relative dy will emulate the line height.
The parent <text> dy value will vertically center the text around the line and is calculated like so:
(Total number of tspans - 1) * dy
let tspans = textEl.querySelectorAll('tspan');
let totalDy = (tspans.length-1)*dy;
here im facing some small issue line dot points are not placed according to the x-axis points
i dont know why dot points are moved 1cm before the actual values of x-points
issue-1 how can we match the dots exact with the x-axis points
issue-2 how can i avoid the zero postion grid line(i dont want to show grid line if value is zero 0 on y-axis)
i want to align like this
but i got like this
here i want to align the months vales and dot points position has to be same when we see x-axis to the dots postions
const convertRemToPixels = (rem) => {
return (
rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
);
};
const data = [
{ x: "Jan", y: "0.03" },
{ x: "Feb", y: "5" },
{ x: "Mar", y: "10" },
{ x: "Apr", y: "6" },
{ x: "May", y: "11" },
{ x: "Jun", y: "2" },
];
const margin = { top: 10, right: 30, bottom: 30, left: 60 },
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select("#mainChart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.select("#mainGroup")
.attr("transform", `translate(${margin.left},${margin.top})`);
let tooltip = d3
.select("#tooltipContainer")
.style("position", "absolute")
.style("top", 0)
.style("left", 0)
.style("display", "none");
// grid function
function make_y_gridlines() {
return d3.axisLeft(y).ticks(6);
}
// Add X axis --> it is a date format
const x = d3.scaleBand().range([0, width]);
const y = d3
.scaleLinear()
.domain([0, d3.max(data.map((e) => parseInt(e.y)))])
.range([height, 0]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y).ticks(6);
x.domain(
data.map(function (d) {
console.log(d);
return d.x;
})
).padding([0.2]);
svg
.select("#XAxis")
.transition()
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.style("color", "#a4a4a4");
svg
.select("#XAxis")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", ".6em")
.attr("dy", "1em")
.style("font-size", ".8rem")
.attr("transform", "rotate(0)")
.style("font-family", '"Roboto", sans-serif');
// Add Y axis
svg
.select("#YAxis")
.transition()
.call(yAxis)
.style("color", "#a4a4a4")
.style("font-size", ".7rem");
svg
.select("#linePath")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#14c884")
.transition()
.attr(
"d",
d3
.line()
.x(function (d) {
console.log(d);
return x(d.x);
})
.y(function (d) {
return y(d.y);
})
.curve(d3.curveCatmullRom.alpha(1.1))
);
const circle = d3.select("#LineDots");
circle
.selectAll(".point")
.data(data)
.join("circle")
.on("mouseover", (e, i) => {
d3.select(e.target).transition().attr("r", 4);
tooltip.transition().duration(0).style("display", "block");
tooltip
.html(`<div>${i.x} : <span>${i.y}</span></div>`)
.style("left", e.pageX + convertRemToPixels(-1.6) + "px")
.style("top", e.pageY - convertRemToPixels(2) + "px");
})
.on("mouseout", (e) => {
d3.select(e.target).transition().attr("r", 2);
tooltip.transition().duration(0);
tooltip
.style("left", "0px")
.style("top", "0px")
.style("display", "none");
})
.transition()
.attr("class", "point")
.attr("stroke", "#14c884")
.attr("fill", function (d, i) {
return "#14c884";
})
.attr("cx", function (d, i) {
return x(d.x);
})
.attr("cy", function (d, i) {
console.log(d.y);
return y(d.y);
})
.attr("r", function (d, i) {
return 2;
})
.style("opacity", 1);
d3.select("#Grid")
.transition()
.call(make_y_gridlines().tickSize(-width).tickFormat(""))
.attr("id", "gridSystem");
#gridSystem line{
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
stroke-dasharray: 2 2;
stroke-width : .05rem ;
}
#gridSystem path {
stroke-width: 0;
}
/* tooltip */
.barTitle{
text-align: center;
font-weight: bolder;
padding: .2em 0;
font-size: .8rem;
color: black;
}
#tooltipContainer div span{
color: #536876;
font-weight: bold;
}
.bar{
border-top-left-radius: 1em !important;
}
#tooltipContainer {
line-height: 1.1;
font-weight: bold;
padding: .6em 1em .6em 1em;
background:white;
color: #9cb3c3;
border-radius: .4em;
font-weight: 600;
box-shadow: 0em 0em .5em rgb(165, 163, 163);
font-size: .6rem;
font-family: 'Roboto', sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
<div id="tooltipContainer"></div>
<svg id="mainChart">
<g id="mainGroup">
<g id="XAxis"></g>
<g id="YAxis"></g>
<g id="Grid"></g>
<path id="linePath"></path>
<g id="LineDots"></g>
</g>
</svg>
here is the codepan source code for fixing https://codepen.io/codingdarci/pen/ZERpNOG
can anyone please give me some suggetion how to figure it out and how to fix this issue
thanks advance
Regarding the position of the circles, you want a point scale instead of a band scale, because band scales have an associated bandwidth:
const x = d3.scalePoint().range([0, width]);
For the y axis, simply remove the ticks for zero:
axis.selectAll(".tick")
.filter(d => d === 0)
.remove();
Here is your code with those changes:
const convertRemToPixels = (rem) => {
return (
rem * parseFloat(getComputedStyle(document.documentElement).fontSize)
);
};
const data = [{
x: "Jan",
y: "0.03"
},
{
x: "Feb",
y: "5"
},
{
x: "Mar",
y: "10"
},
{
x: "Apr",
y: "6"
},
{
x: "May",
y: "11"
},
{
x: "Jun",
y: "2"
},
];
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 60
},
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = d3
.select("#mainChart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.select("#mainGroup")
.attr("transform", `translate(${margin.left},${margin.top})`);
let tooltip = d3
.select("#tooltipContainer")
.style("position", "absolute")
.style("top", 0)
.style("left", 0)
.style("display", "none");
// grid function
function make_y_gridlines() {
return d3.axisLeft(y).ticks(6);
}
// Add X axis --> it is a date format
const x = d3.scalePoint().range([0, width]);
const y = d3
.scaleLinear()
.domain([0, d3.max(data.map((e) => parseInt(e.y)))])
.range([height, 0]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y).ticks(6);
x.domain(
data.map(function(d) {
console.log(d);
return d.x;
})
).padding([0.2]);
svg
.select("#XAxis")
.transition()
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.style("color", "#a4a4a4");
svg
.select("#XAxis")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", ".6em")
.attr("dy", "1em")
.style("font-size", ".8rem")
.attr("transform", "rotate(0)")
.style("font-family", '"Roboto", sans-serif');
// Add Y axis
svg
.select("#YAxis")
.transition()
.call(yAxis)
.style("color", "#a4a4a4")
.style("font-size", ".7rem")
.selectAll(".tick")
.filter(d => d === 0)
.remove();
svg
.select("#linePath")
.datum(data)
.attr("fill", "none")
.attr("stroke", "#14c884")
.transition()
.attr(
"d",
d3
.line()
.x(function(d) {
console.log(d);
return x(d.x);
})
.y(function(d) {
return y(d.y);
})
.curve(d3.curveCatmullRom.alpha(1.1))
);
const circle = d3.select("#LineDots");
circle
.selectAll(".point")
.data(data)
.join("circle")
.on("mouseover", (e, i) => {
d3.select(e.target).transition().attr("r", 4);
tooltip.transition().duration(0).style("display", "block");
tooltip
.html(`<div>${i.x} : <span>${i.y}</span></div>`)
.style("left", e.pageX + convertRemToPixels(-1.6) + "px")
.style("top", e.pageY - convertRemToPixels(2) + "px");
})
.on("mouseout", (e) => {
d3.select(e.target).transition().attr("r", 2);
tooltip.transition().duration(0);
tooltip
.style("left", "0px")
.style("top", "0px")
.style("display", "none");
})
.transition()
.attr("class", "point")
.attr("stroke", "#14c884")
.attr("fill", function(d, i) {
return "#14c884";
})
.attr("cx", function(d, i) {
return x(d.x);
})
.attr("cy", function(d, i) {
console.log(d.y);
return y(d.y);
})
.attr("r", function(d, i) {
return 2;
})
.style("opacity", 1);
d3.select("#Grid")
.transition()
.call(make_y_gridlines().tickSize(-width).tickFormat(""))
.attr("id", "gridSystem")
.selectAll(".tick")
.filter(d => d === 0)
.remove();
#gridSystem line {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
stroke-dasharray: 2 2;
stroke-width: .05rem;
}
#gridSystem path {
stroke-width: 0;
}
/* tooltip */
.barTitle {
text-align: center;
font-weight: bolder;
padding: .2em 0;
font-size: .8rem;
color: black;
}
#tooltipContainer div span {
color: #536876;
font-weight: bold;
}
.bar {
border-top-left-radius: 1em !important;
}
#tooltipContainer {
line-height: 1.1;
font-weight: bold;
padding: .6em 1em .6em 1em;
background: white;
color: #9cb3c3;
border-radius: .4em;
font-weight: 600;
box-shadow: 0em 0em .5em rgb(165, 163, 163);
font-size: .6rem;
font-family: 'Roboto', sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
<div id="tooltipContainer"></div>
<svg id="mainChart">
<g id="mainGroup">
<g id="XAxis"></g>
<g id="YAxis"></g>
<g id="Grid"></g>
<path id="linePath"></path>
<g id="LineDots"></g>
</g>
</svg>
After following up from this question Insert text inside Circle in D3 chart
My nodes are sticking to the center. I am not sure which property is directing my nodes and their x and y coordinates. I recently chnaged my code to add a g layer to the circles so that i can append text along with shape.
DATA
https://api.myjson.com/bins/hwtj0
UPDATED CODE
async function d3function() {
d3.selectAll("svg > *").remove();
const svg = d3.select("svg");
file = document.getElementById("selectFile").value;
console.log("File: " + file)
var width = 900
var height = 900
svg.style("width", width + 'px').style("height", height + 'px');
data = (await fetch(file)).json()
d3.json(file).then(function(data) {
const links = data.links.map(d => Object.create(d));
const nodes = data.nodes.map(d => Object.create(d));
console.log(links.length);
console.log(nodes.length);
const simulation = forceSimulation(nodes, links).on("tick", ticked);
var categorical = [
{ "name" : "schemeAccent", "n": 8},
{ "name" : "schemeDark2", "n": 8},
]
// var colorScale = d3.scaleOrdinal(d3[categorical[6].name])
var color = d3.scaleOrdinal(d3[categorical[1].name]);
var drag = simulation => {
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", d => Math.sqrt(d.value));
// link.append("title").text(d => d.value);
// var circles = svg.append("g")
// .attr("stroke", "#fff")
// .attr("stroke-width", 1.5)
// .selectAll(".circle")
// .data(nodes)
// const node = circles.enter().append("circle")
// .attr("r", 5)
// .attr("fill", d => color(d.group))
// .call(drag(simulation));
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circles")
.data(nodes)
.enter()
.append("g")
.classed('circles', true)
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
node.append("circle")
.classed('circle', true)
.attr("r", 5)
.attr("fill", d => color(d.group))
.call(drag(simulation));
node
.append("text")
.classed('circleText', true)
.attr('dy', '0.35em')
.attr('dx', 5)
.text(d => "Node: " + d.id);
node.append("title").text(d => "Node: " + d.id);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
}
});
}
function forceSimulation(nodes, links) {
return d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter());
}
UPDATED OUTPUT
EXPECTED OUTPUT
UPDATED HTML
<g stroke="#fff" stroke-width="1.5">
<g class="circle" cx="-35.89111508769784" cy="131.13965804447696">
<circle class="circle" r="5" fill="#1b9e77"></circle>
<text class="circleText" dy="0.35em" dx="5">Node: 0</text>
<title>Node: 0</title>
</g>
<g class="circle" cx="70.97799024729613" cy="-195.71408429254427">
<circle class="circle" r="5" fill="#d95f02"></circle>
<text class="circleText" dy="0.35em" dx="5">Node: 3</text>
<title>Node: 3</title>
</g>
[....]
</g>
You have to adapt your code slightly as it currently assumes that you're working with circle elements, where you specify the centres using cx and cy, but you are now using g elements, which use standard x and y coordinates.
First, remove the transform from the g element (that's a leftover from my demo code):
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll(".circles") // note - should be .circles!
.data(nodes)
.enter()
.append("g")
.classed('circles', true)
and in the ticked() function, change the node updating code into a transform that works on g elements (which don't have cx or cy):
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')' )
Demo:
var json = {"nodes":[{"id":"0","group":0},{"id":"1","group":1},{"id":"2","group":2},{"id":"3","group":3},{"id":"4","group":4},{"id":"5","group":5},{"id":"6","group":6},{"id":"7","group":7},{"id":"8","group":8},{"id":"9","group":9},{"id":"10","group":10},{"id":"11","group":11},{"id":"12","group":12},{"id":"13","group":13},{"id":"14","group":14},{"id":"15","group":15},{"id":"16","group":16},{"id":"17","group":17},{"id":"18","group":18},{"id":"19","group":19}],"links":[{"source":"0","target":"1","value":1},{"source":"0","target":"18","value":1},{"source":"0","target":"10","value":1},{"source":"0","target":"12","value":1},{"source":"0","target":"5","value":1},{"source":"0","target":"8","value":1},{"source":"1","target":"0","value":1},{"source":"1","target":"9","value":1},{"source":"1","target":"4","value":1},{"source":"2","target":"4","value":1},{"source":"2","target":"17","value":1},{"source":"2","target":"13","value":1},{"source":"2","target":"15","value":1},{"source":"3","target":"6","value":1},{"source":"4","target":"14","value":1},{"source":"4","target":"2","value":1},{"source":"4","target":"5","value":1},{"source":"4","target":"19","value":1},{"source":"4","target":"1","value":1},{"source":"5","target":"4","value":1},{"source":"5","target":"0","value":1},{"source":"6","target":"3","value":1},{"source":"7","target":"18","value":1},{"source":"7","target":"16","value":1},{"source":"8","target":"0","value":1},{"source":"9","target":"1","value":1},{"source":"10","target":"0","value":1},{"source":"10","target":"15","value":1},{"source":"12","target":"0","value":1},{"source":"13","target":"15","value":1},{"source":"13","target":"2","value":1},{"source":"14","target":"4","value":1},{"source":"15","target":"13","value":1},{"source":"15","target":"10","value":1},{"source":"15","target":"2","value":1},{"source":"16","target":"7","value":1},{"source":"17","target":"2","value":1},{"source":"18","target":"0","value":1},{"source":"18","target":"7","value":1},{"source":"19","target":"4","value":1},{"source":"19","target":"4","value":1}]};
d3.selectAll("svg > *").remove();
const svg = d3.select("svg");
var width = 900
var height = 900
svg.style("width", width + 'px').style("height", height + 'px');
const links = json.links.map(d => Object.create(d));
const nodes = json.nodes.map(d => Object.create(d));
const simulation = forceSimulation(nodes, links).on("tick", ticked);
var categorical = [
{
"name": "schemeAccent",
"n": 8
},
{
"name": "schemeDark2",
"n": 8
}, ]
var color = d3.scaleOrdinal(d3[categorical[1].name]);
var drag = simulation => {
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
const link = svg.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", d => Math.sqrt(d.value));
const node = svg.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll(".circles")
.data(nodes)
.enter()
.append("g")
.classed('circles', true)
.call(drag(simulation))
// .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
const circle = node.append("circle")
.classed('circle', true)
.attr("r", 5)
.attr("fill", d => color(d.group))
node
.append("text")
.classed('circleText', true)
.attr('dy', '0.35em')
.attr('dx', 5)
.text(d => "Node: " + d.id);
node.append("title").text(d => "Node: " + d.id);
function ticked() {
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')')
}
function forceSimulation(nodes, links) {
return d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter());
}
.circleText { fill: black; stroke: none }
<script src="//d3js.org/d3.v5.js"></script>
<svg></svg>
Here is the code:
//Circle Data Set
var circleData = [
{ "cx": 20, "cy": 20, "radius": 20, "color" : "green" },
{ "cx": 70, "cy": 70, "radius": 20, "color" : "purple" }];
//Create the SVG Viewport
var svgContainer = d3.select("#svgContainer")
.attr("width",200)
.attr("height",200);
//Add the SVG Text Element to the svgContainer
var text = svgContainer.selectAll("text")
.data(circleData)
.enter()
.append("text");
var circles = svgContainer.selectAll("circle")
.data(circleData)
.enter()
.append("circle")
.attr("cx", function(d) {return d.cx})
.attr("cy", function(d) {return d.cy})
.attr("r", function(d) {return d.radius})
.attr("fill", function(d) {return d.color})
//Add SVG Text Element Attributes
var textLabels = text
.attr("x", function(d) { return d.cx; })
.attr("y", function(d) { return d.cy; })
.text( function (d) { return "( " + d.cx + ", " + d.cy +" )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
http://jsfiddle.net/kindlychung/jrsxLfpg/1/
It seems d3 always renders text first, which means the text is partly hidden behind the circles:
<svg id="svgContainer" width="200" height="200">
<text x="20" y="20" font-family="sans-serif" font-size="20px" fill="red">( 20, 20 )</text>
<text x="70" y="70" font-family="sans-serif" font-size="20px" fill="red">( 70, 70 )</text>
<circle cx="20" cy="20" r="20" fill="green"></circle>
<circle cx="70" cy="70" r="20" fill="purple"></circle></svg>
How can I adjust this?
You just need to draw your text nodes after drawing your circles.
//Circle Data Set
var circleData = [
{ "cx": 20, "cy": 20, "radius": 20, "color" : "green" },
{ "cx": 70, "cy": 70, "radius": 20, "color" : "purple" }];
//Create the SVG Viewport
var svgContainer = d3.select("#svgContainer")
.attr("width",200)
.attr("height",200);
// draw your circles and any other graphic elements first!
var circles = svgContainer.selectAll("circle")
.data(circleData)
.enter()
.append("circle")
.attr("cx", function(d) {return d.cx})
.attr("cy", function(d) {return d.cy})
.attr("r", function(d) {return d.radius})
.attr("fill", function(d) {return d.color})
// These will now be appended AFTER the circles
// When you use `append` it will add text nodes to end
// of svgContainer
var text = svgContainer.selectAll("text")
.data(circleData)
.enter()
.append("text");
// Here you are editing the pre-existing `text` nodes that you added above.
// Note that you don't use `append` here.
// Instead, you are modifying the d3 selection stored in `text`
var textLabels = text
.attr("x", function(d) { return d.cx; })
.attr("y", function(d) { return d.cy; })
.text( function (d) { return "( " + d.cx + ", " + d.cy +" )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
Here is an edited version of your code:
http://jsfiddle.net/jrsxLfpg/2/
I have the following structure:
[
{ 'length': 10, attributes: [1,2,3] },
{ 'length': 7, attributes: [1,3,4,5] },
{ 'length': 12, attributes: [3,5,7,9,10] },
]
and I am doing the following:
x = d3.scale.linear().domain([0, maxHeight]).range([50, w]),
y = d3.scale.linear().domain([0, maxHeight]).range([h, 20]);
z = d3.scale.linear().domain([0, maxHeight]).range([0, h - 20]);
var chart = svg.selectAll("g.chart")
.data(items)
.enter()
.append("svg:g")
.attr("class", "chart");
chart.append("svg:rect")
.attr("fill", 'darkblue')
.attr("class", 'data')
.attr("x", function(d, i) { return x(i+1); })
.attr("y", function(d, i) { return bottom - z(d['length']) + 15 })
.attr("width", 4)
.attr("height", function(d, i) { return z(d['length']) - z(d['min']); })
What I would like to do is add circles on each of these rectangles which corresponds to the attributes in my structure. Basically, (for one 'item'}, I should see something like this:
<g class="chart">
<rect fill="darkblue" class="data" x="626.1538461538462" y="15" width="6" height="530"></rect>
<circle cx="626.1538461538462" cy="(y1)" r="5" style="fill: #ffff00; stroke: #808080;"></circle>
<circle cx="626.1538461538462" cy="(y2)" r="5" style="fill: #ffff00; stroke: #808080;"></circle>
<circle cx="626.1538461538462" cy="(y3)" r="5" style="fill: #ffff00; stroke: #808080;"></circle>
</g>
The only thing I can think of is looping over the attributes and adding them element by element:
for (z=0; z< 3; ++z)
{
chart.append("svg:circle")
.data(items[z]['attributes'])
.style("fill", 'yellow')
.style("stroke", "gray")
.attr("cx", function(d, i) { return x(i+1); })
.attr("cy", function(d, i)
{
console.log(d);
return bottom - 15;
})
.attr("r", 5);
}
Is there a better way to do this?
You can created a nested selection instead of looping:
chart.selectAll("svg:circle")
.data(function(item) { return item.attributes; })
.enter()
.append("svg:circle")
.style("fill", 'yellow')
.style("stroke", "gray")
.attr("cx", function(d, i) { return x(i+1); })
.attr("cy", function(d, i)
{
console.log(d);
return bottom - 15;
})
.attr("r", 5);
Example:
To keep the cx the same for each parent rect, you can pass the parent_idx through
chart.selectAll("svg:circle")
.data(function(item, parent_idx) {
return item.attributes.map(function (attr_val) {
return { attr_val: attr_val, parent_idx: parent_idx };
});
})
.enter()
.append("svg:circle")
.style("fill", 'yellow')
.style("stroke", "gray")
.attr("cx", function(d, i) { return x(d.parent_idx); })
.attr("cy", function(d, i)
{
return y(d.attr_val);
})
.attr("r", 5);
You can use nested selections. The primary selection will create the groups, each group will have a data item bound to it.
var data = [
{name: 'A', items: [1, 2]},
{name: 'B', items: [2, 3, 4]}
];
var cScale = d3.scale.category10()
.domain(d3.range(10));
var grp = svg.selectAll('g.main')
.data(data)
.enter()
.append('g')
.attr('class', 'main')
.attr('transform', function(d, i) {
return 'translate(0,' + i * 20 + ')';
});
Then, you can create a nested selection, passing an accessor function to the data method. I have an example with rect elements, but with circles is the same:
grp.selectAll('rect')
.data(function(d) { return d.items; })
.enter()
.append('rect')
.attr('x', function(d) { return 10 * d; })
.attr('width', 8)
.attr('height', 10)
.attr('fill', function(d) { return cScale(d); });
You may found the article Nested Selections useful. I wrote a small jsfiddle too: http://jsfiddle.net/pnavarrc/h2YVd/