I have gone through quite a few tutorial but still couldn't work out how to correctly update data, any help would be highly appreciated, thank you!
For some reason, the code doesn't run properly in the snippet, I am quite new to use it,
what I want to achieve is by clicking the button, the data got updated, I tried to use selection.exit().remove, but the exit array is always empty, I don't really understand how that works
const arr = [{
name: "A",
dataset_1: 5,
dataset_2: 6,
},
{
name: "B",
dataset_1: 7,
dataset_2: 22,
},
{
name: "C",
dataset_1: 33,
dataset_2: 23,
},
{
name: "D",
dataset_1: 20,
dataset_2: 12,
},
{
name: "E",
dataset_1: 21,
dataset_2: 15,
},
{
name: "F",
dataset_1: 15,
dataset_2: 18,
},
];
//function for adding dataset
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
arr.map((i) => (i[`dataset_${counter}`] = ran()));
};
add(arr);
};
//function for removing dataset
const remove_set = (arr) => {
arr.map((i) => delete i[`dataset_${counter}`]);
counter >= 1 ? counter-- : counter;
};
//draw area chart
const draw = () => {
//No.1 define place to draw
let svg = d3.select("svg"),
margin = {
left: 50,
right: 50,
top: 15,
bottom: 30
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg
.append("g")
.attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")"
);
//No.2 set axes
let categoriesNames = arr.map((d) => d.name);
let xScale = d3.scalePoint().domain(categoriesNames).range([0, width]); // scalepoint make the axis starts with value compared with scaleBand
let copy = JSON.parse(JSON.stringify(arr));
copy.map((i) => delete i.name);
var yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(copy, (i) => Math.max(...Object.values(i)))]);
let colorScale = d3.scaleOrdinal().range(d3.schemeSet3);
let dataSets = Object.keys(arr[0]).filter((i) => i !== "name");
colorScale.domain(dataSets);
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
g.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
g.exit().remove();
//No.3 draw chart
let areaData = dataSets.map((i) => {
return {
id: i,
values: arr.map((d) => {
return {
name: d.name,
value: d[i]
};
}),
};
});
let generator = d3
.area()
.curve(d3.curveMonotoneX)
.y0(yScale(0))
.x((d) => xScale(d.name))
.y1((d) => yScale(d.value));
let area = g
.selectAll(".area")
.data(areaData)
.enter()
.append("g")
.attr("class", (d) => `area${d.id}`);
area.exit().remove();
area
.append("path")
.attr("d", (d) => {
return generator(d.values);
})
.attr("opacity", 0.6)
.style("fill", (d) => colorScale(d.id));
};
//buttons
draw();
const update = () => {
add_set(arr);
draw();
};
const remove = () => {
remove_set(arr);
draw();
};
d3.select("body")
.append("button")
.text("Remove dataset")
.on("click", remove);
d3.select("body")
.append("button")
.text("Add dataset")
.on("click", update);
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<html>
<body>
<svg width="700" height="500"></svg>
</body>
</html>
selection.exit() is empty, because selection is a subset of nodes from g, and g is newly added every time you call draw. You need to separate one-time logic (draw axes, append container elements, set height/width/etc) from logic you want to run each time.
You should also see in your code that the colour of one of the areas changed. That was because another area was drawn in front of it. If something was really wrong, it would have repurposed the existing areas and failed to remove the one you didn't need. This is a pointer that you append something the second time that you shouldn't have appended!
But there was more. You use g.selectAll(".area"), append a g element, but don't give it class area. Instead, you give it class areadataset_1 and areadataset_2. This way, on subsequent calls, you can't find the area elements!
You also need to learn about .merge(). I recommend looking up some tutorials, like this one. You only updated the new paths, never the old ones. Since you didn't need g, I removed it for now, making the rest of the logic easier.
const arr = [{
name: "A",
dataset_1: 5,
dataset_2: 6,
},
{
name: "B",
dataset_1: 7,
dataset_2: 22,
},
{
name: "C",
dataset_1: 33,
dataset_2: 23,
},
{
name: "D",
dataset_1: 20,
dataset_2: 12,
},
{
name: "E",
dataset_1: 21,
dataset_2: 15,
},
{
name: "F",
dataset_1: 15,
dataset_2: 18,
},
];
//function for adding dataset
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
arr.map((i) => (i[`dataset_${counter}`] = ran()));
};
add(arr);
};
//function for removing dataset
const remove_set = (arr) => {
arr.map((i) => delete i[`dataset_${counter}`]);
counter >= 1 ? counter-- : counter;
};
const margin = {
left: 50,
right: 50,
top: 15,
bottom: 30
};
let svg,
width,
height,
g;
const setup = () => {
//No.1 define place to draw
svg = d3.select("svg");
width = +svg.attr("width") - margin.left - margin.right;
height = +svg.attr("height") - margin.top - margin.bottom;
g = svg
.append("g")
.attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")"
);
}
//draw area chart
const draw = () => {
//No.2 set axes
let categoriesNames = arr.map((d) => d.name);
let xScale = d3.scalePoint().domain(categoriesNames).range([0, width]); // scalepoint make the axis starts with value compared with scaleBand
let copy = JSON.parse(JSON.stringify(arr));
copy.map((i) => delete i.name);
var yScale = d3
.scaleLinear()
.range([height, 0])
.domain([0, d3.max(copy, (i) => Math.max(...Object.values(i)))]);
let colorScale = d3.scaleOrdinal().range(d3.schemeSet3);
let dataSets = Object.keys(arr[0]).filter((i) => i !== "name");
colorScale.domain(dataSets);
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
g.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
g.exit().remove();
//No.3 draw chart
let areaData = dataSets.map((i) => {
return {
id: i,
values: arr.map((d) => {
return {
name: d.name,
value: d[i]
};
}),
};
});
let generator = d3
.area()
.curve(d3.curveMonotoneX)
.y0(yScale(0))
.x((d) => xScale(d.name))
.y1((d) => yScale(d.value));
let area = g
.selectAll(".area")
.data(areaData);
area.exit().remove();
area
.enter()
.append("path")
.attr("class", (d) => `area area${d.id}`)
.attr("opacity", 0.6)
.merge(area)
.attr("d", (d) => {
return generator(d.values);
})
.style("fill", (d) => colorScale(d.id));
};
//buttons
setup();
draw();
const update = () => {
add_set(arr);
draw();
};
const remove = () => {
remove_set(arr);
draw();
};
d3.select("body")
.append("button")
.text("Remove dataset")
.on("click", remove);
d3.select("body")
.append("button")
.text("Add dataset")
.on("click", update);
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<html>
<body>
<svg width="700" height="500"></svg>
</body>
</html>
In my case the selectAll and append were using different types:
groups.selectAll('rect.bar-item')
///...
enter.append('path')
Making them the same rect or only path solved it for me.
Hope this helps someone.
Related
I have an "add dataset" button, and once I click it the legend should show the changed dataset, this is all good. I also have a click event with the legend, so when I click the legend, the dataset get deleted, and I put a line through to the style, and here is the problem, when I use the add button to add another dataset, the deleted dataset legend should be changed to the new dataset number. The console shows the data is correct, but the legend is not changing. I have followed some tutorial and I completely followed it, but still get this, I have been working on this for several hours, I would like some help, thank you
let jsonObj = [{
category: "Jan",
values: [{
value: 9,
source: "dataset1",
},
{
value: 8,
source: "dataset2",
},
],
},
{
category: "Feb",
values: [{
value: 15,
source: "dataset1",
},
{
value: 21,
source: "dataset2",
},
],
},
];
// function for adding data
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 15 + 1);
const add = (arr) => {
counter++;
arr.map((i) =>
i.values.push({
value: ran(),
source: `dataset${counter}`
})
);
};
add(arr);
};
//initial variables
let svg, totalWidth, totalHeight, legend;
const setup = () => {
totalWidth = 100;
totalHeight = 100;
svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
};
const draw = (data) => {
//No.6 set lengend
legend = svg
.selectAll(".legend")
.data(data[0].values.map((d) => d.source));
console.log(data[0].values.map((d) => d.source));
let entering = legend.enter();
let newLegend = entering
.append("g")
.attr("class", "legend")
.attr("transform", (d, i) => "translate(0," + i * 20 + ")")
.append("text")
.attr("x", totalWidth)
.attr("y", 9)
.style("text-anchor", "end")
.text((d) => d);
entering.text((d) => d);
let exiting = legend.exit();
exiting.remove();
newLegend.on("click", function(e, d) {
jsonObj.forEach((i) => {
i.values = i.values.filter((s) => s.source != d);
});
newLegend
.filter((x) => x === d)
.style("text-decoration", "line-through");
});
};
setup();
draw(jsonObj);
const update = () => {
add_set(jsonObj);
draw(jsonObj);
};
<script src="https://d3js.org/d3.v6.min.js"></script>
<button onclick="update()">Add dataset</button>
The following solution is exactly what you asked for.
I don't remove any nodes, because you always re-use them.
There is also a difference between the entering nodes and the newLegend nodes - one has actual content, the other is just a sort of empty array.
let jsonObj = [{
category: "Jan",
values: [{
value: 9,
source: "dataset1",
},
{
value: 8,
source: "dataset2",
},
],
},
{
category: "Feb",
values: [{
value: 15,
source: "dataset1",
},
{
value: 21,
source: "dataset2",
},
],
},
];
// function for adding data
let counter = 2;
const add_set = (arr) => {
const ran = () => Math.floor(Math.random() * 15 + 1);
const add = (arr) => {
counter++;
arr.map((i) =>
i.values.push({
value: ran(),
source: `dataset${counter}`
})
);
};
add(arr);
};
//initial variables
let svg, totalWidth, totalHeight, legend;
const setup = () => {
totalWidth = 100;
totalHeight = 100;
svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
};
const draw = (data) => {
//No.6 set lengend
legend = svg
.selectAll(".legend")
.data(data[0].values.map((d) => d.source));
console.log(data[0].values.map((d) => d.source));
legend.exit().remove();
let newLegend = legend.enter()
.append("g")
.attr("class", "legend")
.attr("transform", (d, i) => "translate(0," + i * 20 + ")");
newLegend
.append("text")
.datum((d) => d)
.attr("x", totalWidth)
.attr("y", 9)
.style("text-anchor", "end")
.on("click", function(e, d) {
jsonObj.forEach((i) => {
i.values = i.values.filter((s) => s.source != d);
});
newLegend
.filter((x) => x === d)
.style("text-decoration", "line-through");
});
legend = newLegend.merge(legend);
legend
.style("text-decoration", null)
.select("text")
.text((d) => d);
};
setup();
draw(jsonObj);
const update = () => {
add_set(jsonObj);
draw(jsonObj);
};
<script src="https://d3js.org/d3.v6.min.js"></script>
<button onclick="update()">Add dataset</button>
I want to do an animation on the lines, but the second line will draw from two parts, one from begining, and the other from close to the second last point and disappear, so I got a result like this
I was following others'code
const pathLength = path.node().getTotalLength();
const transitionPath = d3.transition().ease(d3.easeQuad).duration(3000);
path
.attrs({
"stroke-dashoffset": pathLength,
"stroke-dasharray": pathLength,
})
.transition(transitionPath)
.attr("stroke-dashoffset", 0);
if you need all the code, I can paste, but it is really just this part that works with the animation, thank you!
I think you're accidentally using the same path length twice - namely that of the first path. path.node() returns the first node, even if there are multiple nodes in the selection.
const data = [{
category: "series_1",
values: [{
name: "A",
value: 10
},
{
name: "B",
value: 21
},
{
name: "C",
value: 19
},
{
name: "D",
value: 23
},
{
name: "E",
value: 20
},
],
}, ];
let counter = 1;
const add_set = (arr) => {
let copy = JSON.parse(JSON.stringify(arr[0]));
const random = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
copy.values.map((i) => (i.value = random()));
copy.category = `series_${counter}`;
arr.push(copy);
};
add(arr);
};
add_set(data);
//No.1 define the svg
let graphWidth = 600,
graphHeight = 300;
let margin = {
top: 60,
right: 10,
bottom: 30,
left: 45
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data[0].values.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
let colorScale = d3.scaleOrdinal(d3.schemeCategory10);
colorScale.domain(data.map((d) => d.category));
let yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([
d3.min(data, (i) => d3.min(i.values, (x) => x.value)),
d3.max(data, (i) => d3.max(i.values, (x) => x.value)),
]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
let lineGenerator = d3
.line()
.x((d) => xScale(d.name))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
var lines = mainGraph
.selectAll(".path")
.data(data.map((i) => i.values))
.enter()
.append("path")
.attr("d", lineGenerator)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", (d, i) => colorScale(i));
//No.6 append circles
let circleData = data.map((i) => i.values);
mainGraph
.selectAll(".circle-container")
.data(circleData)
.enter()
.append("g")
.attr("class", "circle-container")
.attr("fill", (d, i) => console.log(d) || colorScale(i))
.selectAll("circle")
.data((d) => d)
.enter()
.append("circle")
.attrs({
cx: (d) => xScale(d.name),
cy: (d) => yScale(d.value),
r: 3,
opacity: 1,
});
// HERE we let the lines grow
lines
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `0 ${pathLength}`
})
.transition()
.duration(2500)
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `${pathLength} ${pathLength}`
});
.line {
stroke: blue;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
The question is probably misleading but I didn't know how to say it more precisely.
Basically, my data goes as follow:
{
"pitcher": 547943,
"pitch_type": "CH",
"velo": 80.15329032258065,
"hmov": 0,
"vmov": 0,
"name": "hyun-jin-ryu"
},
{
"pitcher": 547943,
"pitch_type": "CH",
"velo": 80.15329032258065,
"hmov": 12.729861677419354,
"vmov": 5.4084,
"name": "hyun-jin-ryu"
},
{
"pitcher": 547943,
"pitch_type": "CU",
"velo": 72.77105263157895,
"hmov": 0,
"vmov": 0,
"name": "hyun-jin-ryu"
},
{
"pitcher": 547943,
"pitch_type": "CU",
"velo": 72.77105263157895,
"hmov": -13.357961403508773,
"vmov": -13.062238596491229,
"name": "hyun-jin-ryu"
}
I want to get a path for each pitch_type, starting from (hmov[0],vmov[0]) or 0,0 and going to (hmov[1], vmov[1]). I also created a color scale associated to "velo" but can't find a way to assign it to my path stroke. I suspect it has to do with having 2 values of velo, but cant really say if that is the problem for sure.
//Loop through each pitch
dataNest.forEach(function(d) {
svg.append("path")
.data([data])
.attr("d", pitchLine(d.values))
.attr("stroke", function(d) { return veloScale(d); }) //Problematic part
.attr("stroke-witdh", 2);
});
Full code:
const margin = {top: 25, bottom: 25, right: 25, left: 25};
const height = 300 - margin.top - margin.bottom;
const width = 300 - margin.left - margin.right;
//Set Ranges
let x = d3.scaleLinear().range([0, width]);
let y = d3.scaleLinear().range([height, 0]);
let veloScale = d3.scaleSequential(d3.interpolateViridis);
//Set line generator
let pitchLine = d3.line()
.x(function(d) { return x(d.hmov); })
.y(function(d) { return y(d.vmov); });
//Add SVG canvas
let svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
///////////////////////////////
//Get the Data
d3.json("ryu.json").then(function(data) {
data.forEach(function(d) {
d.hmov = +d.hmov;
d.vmov = +d.vmov;
d.velo = +d.velo;
});
//Scales
x.domain(d3.extent(data, function(d) { return d.hmov; }));
y.domain(d3.extent(data, function(d) { return d.vmov; }));
veloScale.domain(d3.extent(data, function(d) { return d.velo; }))
//Nesting data
let dataNest = d3.nest()
.key(function(d) { return d.pitch_type; })
.entries(data);
//Loop through each pitch
dataNest.forEach(function(d) {
svg.append("path")
.data([data])
.attr("d", pitchLine(d.values))
.attr("stroke", function(d) { return veloScale(d); })
.attr("stroke-witdh", 2);
});
The section of your code where you loop through the dataNest array should be updated so that
a) the data is joined at the correct level, ie the dataNest array to create each path, and the d.values for the datum of the path.
b) the stroke colour function passes in the value from the array you want to map to a colour. As you may have different velo values for each element in the d.values array, you will need to decide which one you will use. The example below uses the velo value from the first element in the array.
//Create a separate g element to contain the path, based on the nested array
let pitch = svg.selectAll(".pitch-type")
.data(dataNest)
.enter()
.append("g")
//for each g element, add a path and assign the d.values for that path
pitch.append("path")
.datum(d => d.values)
.attr("d", d => pitchLine(d))
//pass in the velo value to get a colour
.attr("stroke", d => veloScale(d[0].velo))
.attr("stroke-witdh", 2);
let data = [{
"pitcher": 547943,
"pitch_type": "CH",
"velo": 80.15329032258065,
"hmov": 0,
"vmov": 0,
"name": "hyun-jin-ryu"
},
{
"pitcher": 547943,
"pitch_type": "CH",
"velo": 80.15329032258065,
"hmov": 12.729861677419354,
"vmov": 5.4084,
"name": "hyun-jin-ryu"
},
{
"pitcher": 547943,
"pitch_type": "CU",
"velo": 72.77105263157895,
"hmov": 0,
"vmov": 0,
"name": "hyun-jin-ryu"
},
{
"pitcher": 547943,
"pitch_type": "CU",
"velo": 72.77105263157895,
"hmov": -13.357961403508773,
"vmov": -13.062238596491229,
"name": "hyun-jin-ryu"
}]
const margin = {top: 25, bottom: 25, right: 25, left: 25};
const height = 300 - margin.top - margin.bottom;
const width = 300 - margin.left - margin.right;
//Set Ranges
let x = d3.scaleLinear().range([0, width]);
let y = d3.scaleLinear().range([height, 0]);
let veloScale = d3.scaleSequential(d3.interpolateViridis);
//Set line generator
let pitchLine = d3.line()
.x(function(d) { return x(d.hmov); })
.y(function(d) { return y(d.vmov); });
//Add SVG canvas
let svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
data.forEach(function(d) {
d.hmov = +d.hmov;
d.vmov = +d.vmov;
d.velo = +d.velo;
});
//Scales
x.domain(d3.extent(data, function(d) { return d.hmov; }));
y.domain(d3.extent(data, function(d) { return d.vmov; }));
veloScale.domain(d3.extent(data, function(d) { return d.velo; }))
//Nesting data
let dataNest = d3.nest()
.key(function(d) { return d.pitch_type; })
.entries(data);
let pitch = svg.selectAll(".pitch-type")
.data(dataNest)
.enter()
.append("g")
pitch.append("path")
.datum(d => d.values)
.attr("d", d => pitchLine(d))
.attr("stroke", d => veloScale(d[0].velo))
.attr("stroke-witdh", 2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
I have a D3 line graph currently calculating both the line x-axis and the x-axis labels by month. In the screenshot below, I have two different dates in the same month. Currently they display with the same x-axis value:
I want the x-axis line to calculate by days while the labels remain months. See relevant code below:
<script>
$(document).ready(function() {
let eventSourceArr = [];
$.ajax({
url: '/diary',
type: 'GET',
dataType: 'json',
success: function(data) {
console.log('Diary GET success');
console.log('data: ',data);
//Need to show days, not just months, in the graph
for (let i in data.data) {
//Convert UNIX time to date
let a = new Date(data.data[i].created_at*1000);
let year = a.getFullYear();
year = year.toString();
year = year.slice(2);
let month = a.getMonth();
month = month.toString();
if (month.length === 1) {
month = '0' + month;
}
let date = a.getDate();
date = date.toString();
if (date.length === 1) {
date = '0' + date;
}
let formattedTime = month + '/' + year;
eventSourceArr.push({"feeling": data.data[i].feeling, "month": formattedTime});
console.log('eventSourceArr: ',eventSourceArr);
}
let sixMonthsAgo;
let yearSixMonthsAgo;
if ((parseInt(moment().format('MM')) - 6) > 0) {
sixMonthsAgo = parseInt(moment().format('M') - 6);
yearSixMonthsAgo = parseInt(moment().format('YYYY'));
}
else if ((parseInt(moment().format('MM')) - 6) <= 0) {
sixMonthsAgo = parseInt(moment().format('M')) + 6;
yearSixMonthsAgo = parseInt(moment().format('YYYY')) - 1;
};
let vis = d3.select("#visualisation"),
WIDTH = 1000,
HEIGHT = 475,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xScale = d3.time.scale()
.domain([new Date(yearSixMonthsAgo, sixMonthsAgo, 1), new Date()])
.range([MARGINS.left, WIDTH - MARGINS.right]),
yScale = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([1, 10]),
xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(d3.time.months)
.tickFormat(d3.time.format("%B")),
yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis);
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
let lineGen = d3.svg.line()
.x(function(d) {
// let formatDate = d3.time.format("%-m/%y");
let formatDate = d3.time.format("%-j");
let monthData = formatDate.parse(d.month);
console.log('formatDate: ',formatDate);
console.log('monthData: ',monthData);
return xScale(monthData);
// return xScale(d.created_at);
})
.y(function(d) {
return yScale(parseInt(d.feeling));
})
.interpolate("basis");
vis.append('svg:path')
.attr('d', lineGen(eventSourceArr))
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
},
error: function() {
console.log('Diary GET failure');
}
});
});
</script>
The raw data.data object looks like this:
{"id": 2, "feeling": 3, "notes": "Had a bad day", "updated_at": 1520273147, "created_at": 1520273147}
The x-axis line is visualizing "created_at". There are two data items in the screenshot, both different dates in February.
How do I change this?
EDIT: Added entire script instead of sections.
I updated your code into v4 and did no parsing of times, created a fake second data point for show purposes. Axis are not perfect as I only have 2 data points but this should get you in the right direction.
var data = [{
"id": 2,
"feeling": 3,
"notes": "Had a bad day",
"updated_at": 1520273147000,
"created_at": 1520273147000
},
{
"id": 3,
"feeling": 5,
"notes": "Had a bad day",
"updated_at": 1520273287000,
"created_at": 1520273287000
}
];
let vis = d3.select("#visualization"),
WIDTH = 1000,
HEIGHT = 475,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xScale = d3.scaleTime()
.domain([1520273146000, 1520273288000])
.range([MARGINS.left, WIDTH - MARGINS.right]),
yScale = d3.scaleLinear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([1, 10]);
vis.attr("height", HEIGHT).attr('width', WIDTH)
vis.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(d3.axisBottom(xScale).tickFormat(d3.timeFormat("%b")));
vis.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(d3.axisLeft(yScale));
let lineGen = d3.line()
.x(function(d) {
return xScale(+d.created_at);
})
.y(function(d) {
return yScale(+d.feeling);
});
vis.datum(data).append('path')
.attr("class", "genLine")
.attr('d', lineGen)
.attr('stroke', 'blue')
.attr('stroke-width', 2)
.attr('fill', 'none');
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<svg id='visualization'></svg>
</body>
I have a codepen here - https://codepen.io/anon/pen/GyeEpJ?editors=0010#0
Its a stacked bar chart with a line chart on top
The line chart points appear at the left side of the bar below.
How can I position the points in the line so they appear above the ticks in the x axis.
let dataline = d3.line()
.x((d) => {
return x(d.date);
})
.y((d) =>{
return y(d.total);
});
let layersLineArea = chart.append('g')
.attr('class', 'layers-lines');
let layersLine = layersLineArea.append('path')
.data([totalData])
.attr("class", "line")
.attr('d', dataline);
You're using a band scale, which is not suitable for a line chart.
The simplest solution is adding half the bandwidth in the line generator:
let dataline = d3.line()
.x((d) => {
return x(d.date) + x.bandwidth()/2;
})
.y((d) =>{
return y(d.total);
});
Here is your code with that change:
let keys = [];
let maxVal = [];
let dataToStack = [];
let totalData = [];
let legendKeys = ['usedInf', 'newInf'];
let w = 800;
let h = 450;
let margin = {
top: 60,
bottom: 40,
left: 50,
right: 20,
};
let width = w - margin.left - margin.right;
let height = h - margin.top - margin.bottom;
let colors = ['#FFC400', '#FF4436', '#FFEBB6', '#FFC400', '#B4EDA0'];
let data = [{
"one": 10,
"two": 12,
"three": 18,
"four": 22,
"five": 30,
"six": 44,
"seven": 125,
"date": "2015-05-31T00:00:00"
}, {
"one": 30,
"two": 42,
"three": 38,
"four": 62,
"five": 90,
"six": 144,
"seven": 295,
"date": "2015-06-30T00:00:00"
}, {
"one": 30,
"two": 92,
"three": 18,
"four": 100,
"five": 120,
"six": 10,
"seven": 310,
"date": "2015-07-31T00:00:00"
}, ];
for (let i = 0; i < data.length; i++) {
dataToStack.push({
date: data[i]['date'].toString(),
usedInf: data[i]['one'] + data[i]['two'] + data[i]['three'],
newInf: data[i]['four'] + data[i]['five'] + data[i]['six']
});
totalData.push({
date: data[i]['date'].toString(),
total: data[i]['seven']
});
}
//------------------------- Stack ------------------------//
let stack = d3.stack()
.keys(legendKeys);
let stackedSeries = stack(dataToStack);
//------------------------- Stack ------------------------//
let x = d3.scaleBand()
.domain(dataToStack.map(function(d) {
//let date = new Date(d.date);
return d.date;
}))
.rangeRound([0, width])
.padding(0.05);
let y = d3.scaleLinear()
.domain([0, d3.max(stackedSeries, function(d) {
return d3.max(d, (d) => {
return d[1];
})
})])
.range([height, 0]);
let svg = d3.select('.chart').append('svg')
.attr('class', 'chart')
.attr('width', w)
.attr('height', h);
let chart = svg.append('g')
.classed('graph', true)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//------------------------- Bar Chart ------------------------//
let layersBarArea = chart.append('g')
.attr('class', 'layers-bars');
let layersBar = layersBarArea.selectAll('.layer-bar').data(stackedSeries)
.enter()
.append('g')
.attr('class', 'layer-bar')
.style('fill', (d, i) => {
return colors[i];
});
layersBar.selectAll('rect')
.data((d) => {
return d
})
.enter()
.append('rect')
.attr('height', (d, i) => {
return y(d[0]) - y(d[1]);
})
.attr('y', (d) => {
return y(d[1]);
})
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth());
//------------------------- Bar Chart ------------------------//
//------------------------- Line Chart ------------------------//
let dataline = d3.line()
.x((d) => {
return x(d.date) + x.bandwidth() / 2;
})
.y((d) => {
return y(d.total);
});
let layersLineArea = chart.append('g')
.attr('class', 'layers-lines');
let layersLine = layersLineArea.append('path')
.data([totalData])
.attr("class", "line")
.attr('d', dataline);
//------------------------- Line Chart ------------------------//
chart.append('g')
.classed('x axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
chart.append('g')
.classed('y axis', true)
.call(d3.axisLeft(y)
.ticks(10));
.line {
fill: none;
stroke: #00D7D2;
stroke-width: 5px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="chart"></div>