D3 path tween changes side - javascript

I'm trying to do a path tween like this one: https://bl.ocks.org/mbostock/3916621
Problem: The path is changing the side. The gray path is changing from left to right and the white path is changing from bottom to top. This isn't the expected transition!
Edit: My expected transition should be a simple grow transition. So the small one should grow to the bigger one.
Example: https://jsfiddle.net/wdv3rufs/
const PATHS = {
FULL: {
GRAY: 'M1035,429l-4.6-73.7L1092,223l-66,1l-66.3-36.4l-102.5,67.6L623.8,0L467.4,302.1l-218.7-82.9L77.6,317.4L0,214.5V429H1035z',
WHITE: 'M0,429V292l249.4-72.9l135.4,56.6L623.8,0L824,232.5l135.7-44.9l26.7,190.5l29.3,16.8l19.3,34H0z'
},
SMALL: {
GRAY: 'M0,429h834l-134.2-34.6l-37,23.2l-130.6-35.2l-112.7,33.5l-144.2-67l-96.7,65.2L43.5,377.5L0,391.4V429z',
WHITE: 'M0,429h834l-134.2-34.6l-83.5,29l-84.1-41l-126.9,31.2l-130-64.7l-144.8,58.6l-87-30L0,386.1V429z'
}
};
const pathTween = (d1, precision) => {
return function() {
const path0 = this;
const path1 = path0.cloneNode();
const n0 = path0.getTotalLength();
const n1 = (path1.setAttribute('d', d1), path1).getTotalLength();
const distances = [0];
const dt = precision / Math.max(n0, n1);
let i = 0;
while ((i += dt) < 1) distances.push(i);
distances.push(1);
const points = distances.map(t => {
const p0 = path0.getPointAtLength(t * n0);
const p1 = path1.getPointAtLength(t * n1);
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});
return t => {
return t < 1 ? 'M' + points.map(p => p(t)).join('L') : d1;
};
};
};
const pathTransition = (path, d1) => {
path.transition()
.duration(10000)
.attrTween('d', pathTween(d1, 4));
}
var svg = d3.select('svg');
svg.append('path')
.attr('class', 'white-mountain')
.attr('d', PATHS.SMALL.WHITE)
.call(pathTransition, PATHS.FULL.WHITE);
svg.append('path')
.attr('class', 'gray-mountain')
.attr('d', PATHS.SMALL.GRAY)
.call(pathTransition, PATHS.FULL.GRAY);
How can I get this working?

Related

Why isn't my path working with gsap's motion path?

Background:
I am animating a circle down my web page to follow a certain path using GSAP's motion path plugin. I have scaled the path to ensure it follows the same proportions on all devices using this code:
let w = window.innerWidth;
let h = document.querySelector("main").offsetHeight;
const pathSpec = "M305.91,0c8.54,101.44,17.07,203.69,5.27,304.8s-45.5,202.26-112.37,279c-36,41.37-80.92,74.88-113.34,119.16-66.32,90.59-70.8,210.86-72.71,323.13C9.69,1206.79,9.48,1387.9,1.71,1568c-8,186.11,25.77,370.42,48.07,554.43q4.36,36,8.07,72.07c14.18,140.22,9.95,281.38,8.06,422-1.87,139.1,26.43,260.75,66.38,393.67l82.47,274.39,8.25,27.44,29.87,129c40.8,176.3,87,363.23,64.51,545.49-11,89.33-3.5,182.44-2.28,272.84l3.83,286.1c3.63,188.78,5.07,377.63,7.6,566.44l3.83,286.08c1.27,94.55-4,191.79,3.84,286,14.37,172.9-10,353.08-14.33,527.78q-7,283.21,1.14,566.55,8.14,281.61,31.34,562.53c7.43,90,21.31,180.76,26.7,270.47,5.6,93.24-4,190-6,283.47l-3.45,164.33";
const yScale = h / 8059.08;
const xScale = w / 380.28;
let newPath = "";
const pathBits = Array.from(pathSpec.matchAll(/([MmLlHhVvCcSsQqTtAa])([\d\.,\se-]*)/g));
console.log(pathBits);
pathBits.forEach((bit) => {
const command = bit[1];
const coordinates = bit[2];
newPath += command;
const coordArray = coordinates.split(/[,\s]/g);
// console.log("coordArray", coordArray);
newPath += coordArray.map((coord, index) => {
if (index % 2 == 0) {
return parseFloat(coord) * yScale;
}
else {
return parseFloat(coord) * xScale;
}
}).join(',');
});
So, essentially, I've created coordArray to split the array into the x and y coordinates and added it to newPath which is the scaled version. Then, I've added newPath to motionPath using:
const tlCircle = gsap.timeline();
tlCircle.to(".circle-animation", {
scrollTrigger: {
scroller: "main",
trigger: ".circle-animation",
start: "top 10%",
end: "max",
scrub: 1,
},
ease: "true",
motionPath: {
path: newPath,
autoRotate: true
}
});
When I put the pathSpec var in the path parameter, my circle animates but not when I put the newPath in, even though the web console shows it is in the same format as pathSpec but just scaled.
I've tried to debug this console logs to see what the input and outputs are which are what I want -- newPath is a. svg path in a string but scaled to fit my window but it doesn't work when I use it in motionPath.

d3.js transform.rescaleX() returning big and negative values

I'm making a semantic zoom on xAxis using the helper function transform.rescaleX() inside zoomed function
private manageZoom(svgs: AllSvg, allAxis: AllAxis, dimension: Dimension, sillons: Circulation[]): D3ZoomBehavior {
const zoom: D3ZoomBehavior = d3
.zoom()
.scaleExtent([1, 40])
.translateExtent([
[0, 0],
[dimension.width, dimension.height]
])
.on('zoom', zoomed.bind(null, allAxis, sillons, dimension, this.drawSillonService));
svgs.svgContainer.call(zoom);
return zoom;
function zoomed(
{ xAxis, xAxisBottom, yAxis }: AllAxis,
sillons: Circulation[],
dimension: Dimension,
drawSillonService: DrawSillonService,
{ transform }: any
) {
xAxis.axisContainer.call(xAxis.axis.scale(transform.rescaleX(xAxis.scale)) as any);
xAxisBottom.axisContainer.call(xAxisBottom.axis.scale(transform.rescaleX(xAxisBottom.scale)) as any);
svgs.sillons
.selectAll('path')
.nodes()
.forEach((path, j) => {
const pathSelect = d3.select(path);
const pathData = JSON.parse(pathSelect.attr('data'));
const line = d3
.line()
.x((d) => {
const transformScaled = transform.rescaleX(xAxis.scale);
const value = transformScaled(d[0]);
return value;
})
.y((d) => d[1]);
pathSelect.attr('d', line(pathData));
});
...
Here's the values of transform:
k:1.6817928305074288
x:-278.99232789420023
y:
-200.42112372679287
when d[0] = 0 the transformScaled(d[0]) return -133335,..
I don't know why I get these values. When I calculate transformation manually using x * transform.k + transform.x I get the right values (-79.5), but I would use rescaleX function.

D3 - Add dots in middle of lines between two points

I am drawing lines with D3 between two points which is working fine but want to add dots / points in the middle of the lines.
I'm following this example in the D3 docs but can't get it to work with my own code.
Here's the code that draws the lines:
export default function drawLines(): DrawLines {
const getSVG = (
ref: SVGSVGElement | null,
viewBoxHeight: number,
): d3Selection | undefined => {
if (ref) {
const svg = d3
.select(ref)
.attr('viewBox', `0 0 350 ${viewBoxHeight}`);
return svg;
}
return undefined;
};
const getLine = (idx: number, arr: Array<NodePositions>): d3Line => {
const line = d3
.line<Number>()
.x((value, index) => arr[idx].x[index])
.y((value) => Number(value));
return line;
};
const drawLine = (nodes: Array<NodePositions> | undefined, svg: d3Selection): void => {
if (nodes) {
nodes.forEach((el, idx, arr) => {
if (svg) {
svg
.selectAll(null)
.data([el.y])
.join('path')
.attr('d', (value: Number[] | Iterable<Number>) => getLine(idx, arr)(value));
}
});
}
};
return { getSVG, drawLine };
}
How can I add points / dots or shapes in the middle of the lines?
Try this:
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => (d.x[0] + d.x[1]) / 2)
.attr('cy', d => (d.y[0] + d.y[1]) / 2)
.attr('r', 3)
.style('fill', 'red');

Negative and Zero Log Values in d3

I am trying to plot multiple overlaying charts with X Axis Time and Y axis should toggle between Linear and Log Scale. I am not able to represent my charts appropriately for log scale even after using d3.scale.log().clamp(true).domain([]).range([]).nice()
Below is the code which I am using to map data points to Y axis
_initScaleYMap = function() {
const chart = this.$root().datum();
const data = chart.getDataSets() || [];
const dataSets = chart.getDataSets().filter((ds) => ds.getValueAxisId());
this.scaleYMap(new Map());
const dataSetsGroupedByValueAxisId = _.groupBy(dataSets, (dataSet) => dataSet.getValueAxisId());
Object.entries(dataSetsGroupedByValueAxisId).forEach(([valueAxisId, matchedDataSets]) => {
data.forEach((d) => {
const chartValueAxis = chart._getValueAxisById(valueAxisId);
if (!chartValueAxis) {
throw `Value axis '${valueAxisId} '`;
}
const min = chartValueAxis.getMin();
const max = chartValueAxis.getMax();
const domain = (Number.isFinite(min) && Number.isFinite(max) && !this.forceArea()) ? [min, max] : getYDomain(chart, matchedDataSets);
if (!d.getShowLog()) {
this.scaleYMap().set(valueAxisId,
d3.scale.linear()
.domain(domain)
.range([this.height() - this.margin().top - this.margin().bottom, 0])
);
} else {
this.scaleYMap().set(valueAxisId,
d3.scale.log().domain(domain).range([this.height() - this.margin().top - this.margin().bottom, 1]).nice()
);
}
});
});
return this;
};
I am attaching the before and after photos, before being the linear scale for y and after being the Log scale
On Linear Scale
On Log Scale

D3 semantic zooming with Reusable Pattern

I'm trying to implement semantic zooming while using Mike Bostock's Towards Reusable Charts pattern (where a chart is represented as a function). In my zoom handler, I'd like to use transform.rescaleX to update my scale and then simply call the function again.
It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. Here's my fiddle:
function chart() {
let aspectRatio = 10.33;
let margin = { top: 0, right: 0, bottom: 5, left: 0 };
let current = new Date();
let scaleBand = d3.scaleBand().padding(.2);
let scaleTime = d3.scaleTime().domain([d3.timeDay(current), d3.timeDay.ceil(current)]);
let axis = d3.axisBottom(scaleTime);
let daysThisMonth = d3.timeDay.count(d3.timeMonth(current), d3.timeMonth.ceil(current));
let clipTypes = [ClipType.Scheduled, ClipType.Alarm, ClipType.Motion];
let zoom = d3.zoom().scaleExtent([1 / daysThisMonth, 1440]);
let result = function(selection) {
selection.each(function(data) {
let selection = d3.select(this);
let outerWidth = this.getBoundingClientRect().width;
let outerHeight = outerWidth / aspectRatio;
let width = outerWidth - margin.left - margin.right;
let height = outerHeight - margin.top - margin.bottom;
scaleBand.domain(d3.range(data.length)).range([0, height * .8]);
scaleTime.range([0, width]);
zoom.on('zoom', _ => {
scaleTime = d3.event.transform.rescaleX(scaleTime);
selection.call(result);
});
let svg = selection.selectAll('svg').data([data]);
let svgEnter = svg.enter().append('svg').attr('viewBox', '0 0 ' + outerWidth + ' ' + outerHeight);//.attr('preserveAspectRatio', 'xMidYMin slice');
svg = svg.merge(svgEnter);
let defsEnter = svgEnter.append('defs');
let defs = svg.select('defs');
let gMainEnter = svgEnter.append('g').attr('id', 'main');
let gMain = svg.select('g#main').attr('transform', 'translate(' + margin.left + ' ' + margin.top + ')');
let gAxisEnter = gMainEnter.append('g').attr('id', 'axis');
let gAxis = gMain.select('g#axis').call(axis.scale(scaleTime));
let gCameraContainerEnter = gMainEnter.append('g').attr('id', 'camera-container');
let gCameraContainer = gMain.select('g#camera-container').attr('transform', 'translate(' + 0 + ' ' + height * .2 + ')').call(zoom);
let gCameraRowsEnter = gCameraContainerEnter.append('g').attr('id', 'camera-rows');
let gCameraRows = gCameraContainer.select('g#camera-rows');
let gCameras = gCameraRows.selectAll('g.camera').data(d => {
return d;
});
let gCamerasEnter = gCameras.enter().append('g').attr('class', 'camera');
gCameras = gCameras.merge(gCamerasEnter);
gCameras.exit().remove();
let rectClips = gCameras.selectAll('rect.clip').data(d => {
return d.clips.filter(clip => {
return clipTypes.indexOf(clip.type) !== -1;
});
});
let rectClipsEnter = rectClips.enter().append('rect').attr('class', 'clip').attr('height', _ => {
return scaleBand.bandwidth();
}).attr('y', (d, i, g) => {
return scaleBand(Array.prototype.indexOf.call(g[i].parentNode.parentNode.childNodes, g[i].parentNode)); //TODO: sloppy
}).style('fill', d => {
switch(d.type) {
case ClipType.Scheduled:
return '#0F0';
case ClipType.Alarm:
return '#FF0';
case ClipType.Motion:
return '#F00';
};
});
rectClips = rectClips.merge(rectClipsEnter).attr('width', d => {
return scaleTime(d.endTime) - scaleTime(d.startTime);
}).attr('x', d => {
return scaleTime(d.startTime);
});
rectClips.exit().remove();
let rectBehaviorEnter = gCameraContainerEnter.append('rect').attr('id', 'behavior').style('fill', '#000').style('opacity', 0);
let rectBehavior = gCameraContainer.select('rect#behavior').attr('width', width).attr('height', height * .8);//.call(zoom);
});
};
return result;
}
// data model
let ClipType = {
Scheduled: 0,
Alarm: 1,
Motion: 2
};
let data = [{
id: 1,
src: "assets/1.jpg",
name: "Camera 1",
server: 1
}, {
id: 2,
src: "assets/2.jpg",
name: "Camera 2",
server: 1
}, {
id: 3,
src: "assets/1.jpg",
name: "Camera 3",
server: 2
}, {
id: 4,
src: "assets/1.jpg",
name: "Camera 4",
server: 2
}].map((_ => {
let current = new Date();
let randomClips = d3.randomUniform(24);
let randomTimeSkew = d3.randomUniform(-30, 30);
let randomType = d3.randomUniform(3);
return camera => {
camera.clips = d3.timeHour.every(Math.ceil(24 / randomClips())).range(d3.timeDay.offset(current, -30), d3.timeDay(d3.timeDay.offset(current, 1))).map((d, indexEndTime, g) => {
return {
startTime: indexEndTime === 0 ? d : d3.timeMinute.offset(d, randomTimeSkew()),
endTime: indexEndTime === g.length - 1 ? d3.timeDay(d3.timeDay.offset(current, 1)) : null,
type: Math.floor(randomType())
};
}).map((d, indexStartTime, g) => {
if(d.endTime === null)
d.endTime = g[indexStartTime + 1].startTime;
return d;
});
return camera;
};
})());
let myChart = chart();
let selection = d3.select('div#container');
selection.datum(data).call(myChart);
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Edit: The zoom handler below works fine, but I'd like a more general solution:
let newScaleTime = d3.event.transform.rescaleX(scaleTime);
d3.select('g#axis').call(axis.scale(newScaleTime));
d3.selectAll('rect.clip').attr('width', d => {
return newScaleTime(d.endTime) - newScaleTime(d.startTime);
}).attr('x', d => {
return newScaleTime(d.startTime);
});
The short answer is you need to implement a reference scale to indicate what the scale's base state is when unmanipulated by the zoom. Otherwise you will run into the problem you describe: "It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. "
To see why a reference scale is needed, zoom in on the graph and out (once each) without moving the mouse. When you zoom in, the axis changes. When you zoom out the axis does not. Note the scale factor on the intial zoom in and the first time you zoom out: 1.6471820345351462 on the zoom in, 1 on the zoom out. The number represents how much the to magnify/minify whatever it is we are zooming in on. On the initial zoom in we magnify by a factor of ~1.65. On the preceding zoom out we minify by a factor of 1, ie: not at all. If on the other hand you zoom out first, you minify by a factor of about 0.6 and then if you were to zoom in you magnify by a factor of 1. I've built a stripped down of your example to show this:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
console.log(d3.event.transform.k);
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
The scale should be relative to the initial zoom factor, usually 1. In otherwords, the zoom is cumulative, it records magnification/minification as a factor of the initial scale, not the last step (otherwise transform k values would only be one of three values: one value for zooming out, another for zooming in and one for remaining the same and all relative to the current scale). This is why rescaling the initial scale doesn't work - you lose the reference point to the initial scale that the zoom is referencing.
From the docs, if you redefine a scale with d3.event.transform.rescaleX, we get a scale that reflects the zoom's (cumulative) transformation:
[the rescaleX] method does not modify the input scale x; x thus
represents the untransformed scale, while the returned scale
represents its transformed view. (docs)
Building on this, if we zoom in twice in a row, the first time we zoom in we see the transform.k value is ~1.6x on the first time, the second time it is ~2.7x. But, since we rescale the scale, we apply a zoom of 2.7x on a scale that has already been zoomed in 1.6x, giving us a scale factor of ~4.5x rather than 2.7x. To make matters worse, if we zoom in twice and then out once, the zoom (out) event gives us a scale value that is still greater than 1 (~1.6 on first zoom in, ~2.7 on second, ~1.6 on zoom out), hence we are still zooming in despite scrolling out:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
var magnification = 1000/(scale.domain()[1] - scale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
I haven't discussed the x offset portion of the zoom, but you can imagine that a similar problem occurs - the zoom is cumulative but you lose the initial reference point that those cumulative changes are in reference to.
The idiomatic solution is to use a reference scale and the zoom to create a working scale used for plotting rectangles/axes/etc. The working scale is initially the same as the reference scale (generally) and is set as so: workingScale = d3.event.transform.rescaleX(referenceScale) on each zoom.
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let workingScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let referenceScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(workingScale));
selection.call(zoom);
zoom.on('zoom', function() {
workingScale = d3.event.transform.rescaleX(referenceScale);
var magnification = 1000/(workingScale.domain()[1] - workingScale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>

Categories