un stacking a stacker area chart in d3 - javascript

Hello I have this code that I use to create a stacked area chart:
updateArea(yOffset, data = [], categories) {
const parseTime = this.parseTime;
const xScale = this.getScale(yOffset, data, categories).date;
const yScale = this.getScale(yOffset, data, categories).y;
const area = d3.area()
.curve(d3.curveCardinal)
.x(d => xScale(parseTime(d.data.date)))
.y0(d => yScale(d[0] || 0))
.y1(d => yScale(d[1] || 0));
const stack = d3.stack()
.keys(categories)
.order(d3.stackOrderReverse)
.offset(d3.stackOffsetNone);
if (data.length > 0) {
const stackContainer = this.vis.append('g')
.attr('class', 'stack');
const layer = stackContainer.selectAll('.layer')
.data(stack(data))
.enter()
.append('g')
.attr('class', 'layer');
layer.append('path')
.attr('class', 'area')
.style('fill', (d, i) => d3.schemeCategory20[i])
.attr('d', area);
}
const legend = this.vis.append('g')
.attr('class', 'legend');
legend.selectAll('.legend-item')
.data(stack(data))
.enter()
.append('circle')
.attr('r', 5)
.attr('cx', 20)
.attr('cy', (d, i) => yOffset + 20 + i * 12)
.attr('stroke', 'none')
.attr('fill', (d, i) => d3.schemeCategory20[i]);
legend.selectAll('.legend-item')
.data(stack(data))
.enter()
.append('text')
.attr('class', 'legend-item')
.attr('x', 30)
.attr('y', (d, i) => yOffset + 24 + i * 12)
.text(d => d.key);
}
I want to un stack the stacked areas so that they overlap and I can then make the areas opacity .3 or something.
When I try and do this:
.data(data)
.enter()
.append('g')
.attr('class', 'layer');
None of the areas show up. So just trying to figure out why!!
Thanks!

I just ended up doing this:
layer.append('path')
.attr('class', 'area')
.style('fill', 'transparent')
.style('stroke', (d, i) => d3.schemeCategory20[i])
.style('stroke-width', 1)
.attr('d', area);
and that seemed to work. Thanks everyone for the help!

Related

How do I access double-nested arrays for multi-line chart in d3.js?

I've exhausted the extent of my logical capabilities in my multi-line chart trying to 1. add color, 2. add a tooltip, and 3. add mouseover animation to double-nested groups in d3.
Sample data:
state,year,lvalue,wvalue
GA,1980,0.8,value1
GA,1980,0.5,value2
GA,1981,0.3,value1
GA,1981,0.1,value2
Here's the group:
var nest = d3.groups(data,
d => d.state, d => d['lvalue'])
Where I'm currently running into issues with the colors and tooltip:
// Function to create the initial graph
var initialGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "legisGroups")
.attr('stroke', ([key, ]) => myColor(key))
var initialPath = selectLegisGroups.selectAll(".line")
.data(([, values]) => values)
.enter()
.append("path")
initialPath
.attr('d', (d) => valueLine(Array.from(d.values())[1]))
.attr("class", "line")
.attr('fill', 'none')
.attr('stroke-width', 1.5)
.on('mouseover', function () {
d3.select(this)
.transition()
.duration(500)
.attr('stroke-width',3)
})
.on('mouseout', function () {
d3.select(this)
.transition()
.duration(500)
.attr('stroke-width',2)
})
.append('title') // Tooltip
.text(function (d) { return d[1].state +
'\nlvalue: ' + d['lvalue']})
I've tried dozens of iterations for both and have had no luck and I can't seem to figure out how to properly pull what I need out of the arrays. I want the colors to be based off lvalue and the tooltips to show the state and lvalue. Any help is very much appreciated.
EDIT: I've solved part of the problem, my CSS had attributes for the line class that were not being overridden, so I just created a different class (line2) not included in the CSS.
I still can't get the colors to separate and now the mouseover function applies to all of my lines regardless of the lvalue so I'm still not quite where I want to be.
Full code in case it's helpful:
// Variables
var margin = { top: 50, right: 50, bottom: 50, left: 50 }
var h = 500 - margin.top - margin.bottom
var w = 700 - margin.left - margin.right
var formatYear = d3.format('d')
var formatPercent = d3.format('.0%')
d3.csv('15/data3.csv').then(function (data) {
// format the data
data.forEach(function(d) {
d.year = +d.year;
d.state = d.state;
d.wvalue = +d.wvalue;
d["lvalue"] = d["lvalue"]
});
// Scales
var x = d3.scaleLinear()
.range([0,w])
var y = d3.scaleLinear()
.range([h,0])
y.domain([
d3.min([0,d3.min(data,function (d) { return d.wvalue })]),
d3.max([0,d3.max(data,function (d) { return d.wvalue })])
]);
x.domain([1968, 2016])
// Define the line
var valueLine = d3.line()
.x(function(d) { return x(d.year); })
.y(function(d) { return y(d.wvalue); })
// Create the svg canvas in the "d3block" div
var svg = d3.select("#d3block")
.append("svg")
.style("width", w + margin.left + margin.right + "px")
.style("height", h + margin.top + margin.bottom + "px")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")")
.attr("class", "svg");
//add reference lines
svg.append('line')
.style("stroke", "red")
.style("stroke-width", 0.5)
.attr("x1", x(1968))
.attr("y1", y(0.08))
.attr("x2", x(2016))
.attr("y2", y(0.08))
.attr("class", "line")
svg.append('line')
.style("stroke", "gray")
.style("stroke-width", 0.5)
.attr("x1", x(1968))
.attr("y1", y(0.00))
.attr("x2", x(2016))
.attr("y2", y(0.00))
.attr("class", "line")
svg.append('line')
.style("stroke", "red")
.style("stroke-width", 0.5)
.attr("x1", x(1968))
.attr("y1", y(-0.08))
.attr("x2", x(2016))
.attr("y2", y(-0.08))
.attr("class", "line")
//nest variable
var nest = d3.groups(data,
d => d.state, d => d['lvalue'])
var myColor = d3.scaleOrdinal()
.domain(["lvalue1", "lvalue2", "lvalue3"])
.range(["#884E17", "#0D0FAD", "#0DAD1C"]);
// X-axis
var xAxis = d3.axisBottom()
.scale(x)
.tickFormat(formatYear)
.tickValues([1968, 1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000, 2004, 2008, 2012, 2016])
// Y-axis
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(formatPercent)
.ticks(5)
// Create a dropdown
var legisMenu = d3.select("#legisDropdown")
legisMenu
.append("select")
.selectAll("option")
.data(nest)
.enter()
.append("option")
.attr("value", ([key, ]) => key)
.text(([key, ]) => key)
// Function to create the initial graph
var initialGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "legisGroups")
.attr('stroke', ([key, ]) => myColor(key))
var initialPath = selectLegisGroups.selectAll(".line")
.data(([, values]) => values)
.enter()
.append("path")
initialPath
.attr('d', (d) => valueLine(Array.from(d.values())[1]))
.attr("class", "line")
.attr('fill', 'none')
.attr('stroke-width', 1.5)
.on('mouseover', function () {
d3.select(this)
.transition()
.duration(500)
.attr('stroke-width',3)
})
.on('mouseout', function () {
d3.select(this)
.transition()
.duration(500)
.attr('stroke-width',2)
})
.append('title') // Tooltip
.text(function (d) { return d[1].state +
'\nlvalue: ' + d['lvalue']})
}
// Create initial graph
initialGraph("Alabama")
// Update the data
var updateGraph = function(legis){
// Filter the data to include only state of interest
var selectLegis = nest.filter(([key, ]) => key == legis)
// Select all of the grouped elements and update the data
var selectLegisGroups = svg.selectAll(".legisGroups")
.data(selectLegis)
// Select all the lines and transition to new positions
selectLegisGroups.selectAll("path.line")
.data(([, values]) => values)
.transition()
.duration(1000)
.attr("d", (d) => valueLine(Array.from(d.values())[1]))
}
// Run update function when dropdown selection changes
legisMenu.on('change', function(){
// Find which fruit was selected from the dropdown
var selectedLegis = d3.select(this)
.select("select")
.property("value")
// Run update function with the selected state
updateGraph(selectedLegis)
});
// X-axis
svg.append('g')
.attr('class','axis')
.attr('id','xAxis')
.attr('transform', 'translate(0,' + h + ')')
.call(xAxis)
.append('text') // X-axis Label
.attr('id','xAxisLabel')
.attr('fill','black')
.attr('y',-10)
.attr('x',w)
.attr('dy','.71em')
.style('text-anchor','end')
.text('')
// Y-axis
svg.append('g')
.attr('class','axis')
.attr('id','yAxis')
.call(yAxis)
.append('text') // y-axis Label
.attr('id', 'yAxisLabel')
.attr('fill', 'black')
.attr('transform','rotate(-90)')
.attr('x',0)
.attr('y',5)
.attr('dy','.71em')
.style('text-anchor','end')
.text('wvalue')
})

D3 tooltip not appearing near mouse

I want tooltips to appear next to my mouse when it hovers over a node. I tried solutions I found on SO, but so far, only got this solution by Boxun to work, although it's not quite what I had in mind (D3.js: Position tooltips using element position, not mouse position?).
I was wondering why in my listener function,
.on('mousemove', function(d) {})
, the functions
Tooltips
.style("left", d3.mouse(this)[0])
.style("top", (d3.mouse(this)[1]))
or
Tooltips
.style("left", d3.event.pageX + 'px')
.style("top", d3.event.pageY + 'px')
shows up on top of the svg instead of where my mouse is.
From reading the answers to the link above, I think I have to transform my coordinates somehow, but I was not able to get that to work.
Here, I am using d3.event.pageX and my mouse is over cherry node.
import * as d3_base from "d3";
import * as d3_dag from "d3-dag";
const d3 = Object.assign({}, d3_base, d3_dag);
drawDAG({
graph: [
["apples", "banana"],
["cherry", "tomato"],
["cherry", "avocado"],
["squash", "banana"],
["lychee", "cherry"],
["dragonfruit", "mango"],
["tomato", "mango"]
]
})
async function drawDAG(response) {
loadDag(response['graph'])
.then(layoutAndDraw())
.catch(console.error.bind(console));
}
async function loadDag(source) {
const [key, reader] = ["zherebko", d3_dag.dagConnect().linkData(() => ({}))]
return reader(source);
}
function layoutAndDraw() {
const width = 800;
const height = 800;
const d3 = Object.assign({}, d3_base, d3_dag);
function sugiyama(dag) {
const layout = d3.sugiyama()
.size([width, height])
.layering(d3.layeringSimplex())
.decross(d3.decrossOpt())
.coord(d3.coordVert());
layout(dag);
draw(dag);
}
return sugiyama;
function draw(dag) {
// Create a tooltip
const Tooltip = d3.select("root")
.append("div")
.attr("class", "tooltip")
.style('position', 'absolute')
.style("opacity", 0)
.style("background-color", "black")
.style("padding", "5px")
.style('text-align', 'center')
.style('width', 60)
.style('height', 30)
.style('border-radius', 10)
.style('color', 'white')
// This code only handles rendering
const nodeRadius = 100;
const svgSelection = d3.select("root")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `${-nodeRadius} ${-nodeRadius} ${width + 2 * nodeRadius} ${height + 2 * nodeRadius}`);
const defs = svgSelection.append('defs');
const steps = dag.size();
const interp = d3.interpolateRainbow;
const colorMap = {};
dag.each((node, i) => {
colorMap[node.id] = interp(i / steps);
});
// How to draw edges
const line = d3.line()
.curve(d3.curveCatmullRom)
.x(d => d.x)
.y(d => d.y);
// Plot edges
svgSelection.append('g')
.selectAll('path')
.data(dag.links())
.enter()
.append('path')
.attr('d', ({
data
}) => line(data.points))
.attr('fill', 'none')
.attr('stroke-width', 3)
.attr('stroke', ({
source,
target
}) => {
const gradId = `${source.id}-${target.id}`;
const grad = defs.append('linearGradient')
.attr('id', gradId)
.attr('gradientUnits', 'userSpaceOnUse')
.attr('x1', source.x)
.attr('x2', target.x)
.attr('y1', source.y)
.attr('y2', target.y);
grad.append('stop').attr('offset', '0%').attr('stop-color', colorMap[source.id]);
grad.append('stop').attr('offset', '100%').attr('stop-color', colorMap[target.id]);
return `url(#${gradId})`;
});
// Select nodes
const nodes = svgSelection.append('g')
.selectAll('g')
.data(dag.descendants())
.enter()
.append('g')
.attr('width', 100)
.attr('height', 100)
.attr('transform', ({
x,
y
}) => `translate(${x}, ${y})`)
.on('mouseover', function(d) {
Tooltip
.style('opacity', .8)
.text(d.id)
})
.on('mouseout', function(d) {
Tooltip
.style('opacity', 0)
})
.on('mousemove', function(d) {
var matrix = this.getScreenCTM()
.translate(+this.getAttribute("cx"), +this.getAttribute("cy"));
Tooltip
.html(d.id)
.style("left", (window.pageXOffset + matrix.e - 50) + "px")
.style("top", (window.pageYOffset + matrix.f - 60) + "px");
})
// Plot node circles
nodes.append('rect')
.attr('y', -30)
.attr('x', (d) => {
return -(d.id.length * 15 / 2)
})
.attr('rx', 10)
.attr('ry', 10)
.attr('width', (d) => {
return d.id.length * 15;
})
.attr('height', (d) => 60)
.attr('fill', n => colorMap[n.id])
// Add text to nodes
nodes.append('text')
.text(d => {
let id = '';
d.id.replace(/_/g, ' ').split(' ').forEach(str => {
if (str !== 'qb')
id += str.charAt(0).toUpperCase() + str.substring(1) + '\n';
});
return id;
})
.attr('font-size', 25)
.attr('font-weight', 'bold')
.attr('font-family', 'sans-serif')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('fill', 'white')
.attr();
}
}
You can try using this instead, this will make sure that the tooltip is displayed on the exact mouse position.
d3.event.offsetY

Force graph labels overlay each other

I'm trying to move from visjs to d3js cause visjs always redraw the same data in a different way. The problem I faced is that d3js draw my nodes too close to each other, so labels overlay each others(see screenshot).
D3js is not an easy tool for beginners, but I'm sure there is some way to fix this problem. Please help to solve it.
const links = edges.map((edge) => ({source: edge.from, target: edge.to}));
function getNodeColor(node) {
return node.color.background;
}
const width = 1000;
const height = 800;
const svg = d3.select('svg');
svg.selectAll('*').remove();
svg.attr('width', width).attr('height', height);
// simulation setup with all forces
const linkForce = d3
.forceLink()
.id(function (link) {
return (link as NetworkNode).id;
})
.strength(function (link) {
return 1;
});
const simulation = d3
.forceSimulation()
.force('link', linkForce)
.force('charge', d3.forceManyBody().strength(-120))
.force('center', d3.forceCenter(width / 2, height / 2));
const linkElements = svg
.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', 1)
.attr('stroke', 'rgba(50, 50, 50, 0.2)');
const nodeElements = svg
.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', getNodeColor);
const textElements = svg
.append('g')
.attr('class', 'texts')
.selectAll('text')
.data(nodes)
.enter()
.append('text')
.text(function (node) {
return node.label;
})
.attr('font-size', 15)
.attr('dx', 15)
.attr('dy', 4);
simulation.nodes(nodes).on('tick', () => {
nodeElements
.attr('cx', function (node) {
return node.x;
})
.attr('cy', function (node) {
return node.y;
});
textElements
.attr('x', function (node) {
return node.x;
})
.attr('y', function (node) {
return node.y;
});
linkElements
.attr('x1', function (link) {
return (link as d3.SimulationLinkDatum<any>).source.x;
})
.attr('y1', function (link) {
return (link as d3.SimulationLinkDatum<any>).source.y;
})
.attr('x2', function (link) {
return (link as d3.SimulationLinkDatum<any>).target.x;
})
.attr('y2', function (link) {
return (link as d3.SimulationLinkDatum<any>).target.y;
});
});
// #ts-ignore
simulation.force('link').links(links);

d3 bars needs to move from bottom to up [duplicate]

This question already has an answer here:
reverse how vertical bar chart is drawn
(1 answer)
Closed 2 years ago.
This is a d3 bar graph.
I am trying to animate bars grow from bottom to upwards. how to achieve it?
also, how to make labels ie text of bars stay at the top of the bars and move along with them?
currently, bars are starting at top and going down to zero.
d3.select("body")
.append("h2")
.text("BAR GRAPH");
const dataset = [12, 31, 22, 17, 25, 18, 29, 14, 9];
const w = 500;
const h = 120;
const svg = d3
.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var bars = svg
.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => i * 30)
.attr("y", (d, i) => h - 3 * d)
.attr("width", 25)
.attr("height", (d, i) => 3 * d)
.attr("fill", "navy");
bars
.transition()
.duration(400)
.attr("y", h)
.attr("height", (d, i) => 3 * d);
svg
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(d => d)
.attr("x", (d, i) => i * 30)
.attr("y", (d, i) => h - 3 * d - 3)
.style("font-size", "25px")
.style("fill", "red")
.append("title")
.text(d => d);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Your code was almost on point for the bars, you just need to change the order of the affectation to the y attribute of bars and set the height attribute of bars to 0 at the start.
I just modified the three lines in your example and marked them with comments to show what to change to achieve your desired effect.
To make the labels follow the bars you can just add a transition on them with the same duration ! I added some lines in the last block and modified one to achieve the effect.
Hope it helps :)
d3.select("body")
.append("h2")
.text("BAR GRAPH");
const dataset = [12, 31, 22, 17, 25, 18, 29, 14, 9];
const w = 500;
const h = 120;
const svg = d3
.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var bars = svg
.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => i * 30)
.attr("y", h) // modified here
.attr("width", 25)
.attr("height", 0) // modified here
.attr("fill", "navy");
bars
.transition()
.duration(400)
.attr("y", (d, i) => h - 3 * d) // modified here
.attr("height", (d, i) => 3 * d);
// modified a bit here
var labels = svg
.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(d => d)
.attr("x", (d, i) => i * 30)
.attr("y", h) // modified here
.style("font-size", "25px")
.style("fill", "red");
labels
.append("title")
.text(d => d);
labels
.transition() // added here
.duration(400) // added here
.attr("y", (d, i) => h - 3 * d - 3) // added here
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Resetting drag with multiple elements

I have multiple svg groups (each containing a circle and text) which I am dragging via d3-drag from an initial position. I have a rectangular hit zone that I only want one of these draggable groups in at a time. So whenever two groups are in the hit zone, I would like the first group that was in the hit zone to fade away and reappear in its initial position.
I have tried doing this via a function which translates the group back to its initial position by finding the current position of the circle shape and translating like:
translate(${-current_x}, ${-current_y})
This does translate the group back to the (0,0) position, so I have to offset by its initial position. I do this by setting the initial x and y values of the circle shape as attributes in the circle element and incorporating these into the translation:
translate(${-current_x + initial_x}, ${-current_y + initial_y})
Here is a block of my attempt:
https://bl.ocks.org/interwebjill/fb9b0d648df769ed72aeb2755d3ff7d5
And here it is in snippet form:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("initial_x", (d, i) => circleRadius * (2 * i + 1))
.attr("initial_y", circleRadius)
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise().classed("active", true);
}
function dragged() {
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
}
function dragEnded() {
d3.select(this).classed("active", false);
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
let current_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('cx');
let current_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('cy');
let initial_x = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_x');
let initial_y = d3.select(`#${resetVar}`)
.select('circle')
.attr('initial_y');
d3.select(`#${resetVar}`)
.transition()
.duration(2000)
.style('opacity', 0)
.transition()
.duration(2000)
.attr('transform', `translate(${-current_x}, ${-current_y})`)
.transition()
.duration(2000)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>
Here are the problems:
While using translate(${-current_x}, ${-current_y}) works, when I try using translate(${-current_x + initial_x}, ${-current_y + initial_y}), the translation uses very large negative numbers (for example, translate(-52640, -4640)).
While using translate(${-current_x}, ${-current_y}) works, when I try to drag this translated group again, the group immediately repeats the previous translate(${-current_x}, ${-current_y})
Your code runs into difficulties because you are positioning both the g elements and the children text and circles.
Circles and text are originally positioned by x/y attributes:
draggables.append('circle')
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
draggables.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
Drag events move the circles and text here:
d3.select(this).select("text").attr("x", d3.event.x).attr("y", d3.event.y);
d3.select(this).select("circle").attr("cx", d3.event.x).attr("cy", d3.event.y);
And then we reset the circles and text by trying to offset the parent g with a transform:
d3.select(`#${resetVar}`).attr('transform', `translate(${-current_x}, ${-current_y})`)
Where current_x and current_y are the current x,y values for the circles and text. We have also stored the initial x,y values for the text, but altogether, this becomes a more convoluted then necessary as we have two competing sets of positioning coordinates.
This can be simplified a fair amount. Instead of positioning both the text and the circles, simply apply a transform to the parent g holding both the circle and the text. Then when we drag we update the transform, and when we finish, we reset the transform.
Now we have no modification of x,y/cx,cy attributes and transforms for positioning the elements relative to one another. No offsets and the parent g's transform will always represent the position of the circle and the text.
Below I keep track of the original transform with the datum (not an element attribute) - normally I would use a property of the datum, but you have non-object data, so I just replace the datum with the original transform:
const circleRadius = 40;
const variables = ['one', 'two', 'three', 'four'];
const inZone = [];
// DOM elements
const svg = d3.select("body").append("svg")
.attr("width", 960)
.attr("height", 500)
const dragDockGroup = svg.append('g')
.attr('id', 'draggables-dock');
// Immovable placemarkers:
const dock = dragDockGroup.selectAll('g')
.data(variables)
.enter().append("g")
.attr("id", (d, i) => `dock-${variables[i]}`);
dock.append("circle")
.attr("cx", (d, i) => circleRadius * (2 * i + 1))
.attr("cy", circleRadius)
.attr("r", circleRadius)
.style("stroke", "none")
.style("fill", "palegoldenrod");
dock.append("text")
.attr("x", (d, i) => circleRadius * (2 * i + 1))
.attr("y", circleRadius)
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
// Dragables
const draggablesGroup = svg.append('g')
.attr('id', 'draggables');
const draggables = draggablesGroup.selectAll('g')
.data(variables)
.enter()
.append("g")
.datum(function(d,i) {
return "translate("+[circleRadius * (2 * i + 1),circleRadius]+")";
})
.attr("transform", (d,i) => d)
.attr("id", (d, i) => variables[i])
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded));
draggables.append('circle')
.attr("r", circleRadius)
.style("stroke", "orange")
.style("fill", "yellowgreen");
draggables.append("text")
.attr("text-anchor", "middle")
.style("fill", "white")
.text((d, i) => variables[i]);
svg.append('rect')
.attr("x", 960/2)
.attr("y", 0)
.attr("width", 100)
.attr("height", 500/2)
.attr("fill-opacity", 0)
.style("stroke", "#848276")
.attr("id", "hitZone");
// functions
function dragStarted() {
d3.select(this).raise();
}
function dragged() {
d3.select(this).attr("transform","translate("+[d3.event.x,d3.event.y]+")")
}
function dragEnded() {
d3.select(this).lower();
let hit = d3.select(document.elementFromPoint(d3.event.sourceEvent.clientX, d3.event.sourceEvent.clientY)).attr("id");
if (hit == "hitZone") {
inZone.push(this.id);
if (inZone.length > 1) {
let resetVar = inZone.shift();
resetCircle(resetVar);
}
}
d3.select(this).raise();
}
function resetCircle(resetVar) {
d3.select(`#${resetVar}`)
.transition()
.duration(500)
.style('opacity', 0)
.transition()
.duration(500)
.attr("transform", (d,i) => d)
.transition()
.duration(500)
.style('opacity', 1);
}
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
<script src="https://d3js.org/d3.v5.min.js"></script>

Categories

Resources