d3动画地图:放大后,动画开始时如何正确投射点?
d3 animated map: after zooming in, how to correctly project points when animation begins?
我在 d3 中制作了随时间变化的点地图。它很好用。我添加了缩放。那也行。然而,它们并不完全协同工作。
我发现当我调用函数绘制圆圈时,我需要重新设置投影。我已经正确调整了比例。因此,如果您放大并按播放,圆圈的大小是正确的。
但是它们不在正确的位置,因为我不知道投影的 translate
部分需要是什么。
因此,如果您缩放和平移,然后播放,您会看到如下内容:
如果有人能提供见解,我将不胜感激。我的代码在下面,这里还有一个 Plunker:https://plnkr.co/edit/HlMLKgARm0hgzLPN?open=lib%2Fscript.js
(us.json 文件太大,无法粘贴到此处,但在 plunker 中。)
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #696969;
}
#play-button {
position: absolute;
bottom: 130px;
left: 50px;
background: #333;
padding-right: 26px;
border-radius: 3px;
border: none;
color: white;
margin: 0;
padding: 0 12px;
width: 60px;
cursor: pointer;
height: 30px;
}
#play-button:hover {
background-color: #696969;
}
.ticks {
font-size: 10px;
}
.track,
.track-inset,
.track-overlay {
stroke-linecap: round;
}
.track {
stroke: #000;
stroke-opacity: 0.3;
stroke-width: 10px;
}
.track-inset {
stroke: #dcdcdc;
stroke-width: 8px;
}
.track-overlay {
pointer-events: stroke;
stroke-width: 50px;
stroke: transparent;
cursor: crosshair;
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: 0.5;
stroke-width: 1.25px;
}
#zoom-buttons {
position: absolute;
margin-left: 10px;
margin-top: 10px;
padding: 5px;
background: #fff;
}
#zoom-buttons button {
background: #efefef;
color: #231F20;
border: 0;
padding: 0;
border-radius: 2px;
width: 25px;
height: 25px;
}
</style>
</head>
<body>
<div id="zoom-buttons">
<button id="zoom-in">+</button>
<button id="zoom-out">-</button>
</div>
<div id="vis">
<button id="play-button">Play</button>
</div>
<script>
var formatDateIntoYear = d3.timeFormat("%Y");
var formatDate = d3.timeFormat("%b %Y");
var parseDate = d3.timeParse("%m/%d/%y");
var startDate = new Date("2004-11-01"),
endDate = new Date("2017-04-01");
var margin = { top: 50, right: 50, bottom: 0, left: 50 },
width = 960 - margin.left - margin.right,
height = 750 - margin.top - margin.bottom;
var svg = d3.select("#vis")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width + 100)
.translate([width / 2, height / 2.2]);
var path = d3.geoPath()
.projection(projection);
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on('zoom', zoomed);
svg.call(zoom)
////////// slider //////////
var moving = false;
var currentValue = 0;
var targetValue = width;
var playButton = d3.select("#play-button");
var x = d3.scaleTime()
.domain([startDate, endDate])
.range([0, targetValue])
.clamp(true);
var slider = svg.append("g")
.attr("class", "slider")
.attr("transform", "translate(" + margin.left + "," + height + ")");
slider.append("line")
.attr("class", "track")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-inset")
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-overlay")
.call(d3.drag()
.on("start.interrupt", function () { slider.interrupt(); })
.on("start drag", function () {
currentValue = d3.event.x;
update(x.invert(currentValue));
})
);
slider.insert("g", ".track-overlay")
.attr("class", "ticks")
.attr("transform", "translate(0," + 18 + ")")
.selectAll("text")
.data(x.ticks(10))
.enter()
.append("text")
.attr("x", x)
.attr("y", 10)
.attr("text-anchor", "middle")
.text(function (d) { return formatDateIntoYear(d); });
var handle = slider.insert("circle", ".track-overlay")
.attr("class", "handle")
.attr("r", 9);
var label = slider.append("text")
.attr("class", "label")
.attr("text-anchor", "middle")
.text(formatDate(startDate))
.attr("transform", "translate(0," + (-25) + ")")
////////// map //////////
var dataset;
var layer1 = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("us.json", function (error, us) {
if (error) throw error;
layer1.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.attr("class", "feature")
.attr("stroke", "#ccc")
.attr("fill", "#fff")
layer1.append("path")
.datum(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
});
var plot = svg.append("g")
.attr("class", "plot")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let zoomScale = 1; // initial zoom level
d3.csv("circles.csv", prepare, function (data) {
dataset = data;
drawPlot(dataset);
playButton
.on("click", function () {
var button = d3.select(this);
if (button.text() == "Pause") {
moving = false;
clearInterval(timer);
button.text("Play");
} else {
moving = true;
timer = setInterval(step, 100);
button.text("Pause");
}
console.log("Slider moving: " + moving);
})
})
function prepare(d) {
d.id = d.id;
d.date = parseDate(d.date);
return d;
}
function step() {
update(x.invert(currentValue));
currentValue = currentValue + (targetValue / 151);
if (currentValue > targetValue) {
moving = false;
currentValue = 0;
clearInterval(timer);
// timer = 0;
playButton.text("Play");
console.log("Slider moving: " + moving);
}
}
function drawPlot(data) {
console.log('zoom scale', zoomScale)
// update projection based on zoom
projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale((width + 100) * zoomScale) // this works
.translate([width / 2, height / 2.2]) // what does this need to be?
var locations = plot.selectAll(".location")
.data(data);
locations.enter()
.append("circle")
.attr("class", "location")
.attr('cx', d => projection([d.lat, d.lng])[0])
.attr('cy', d => projection([d.lat, d.lng])[1])
.style("fill", '#333')
.style("stroke", '#111')
.style("opacity", 0.5)
.attr("r", 3)
.transition()
.duration(400)
.attr("r", 8)
locations.exit()
.remove();
}
function update(h) {
handle.attr("cx", x(h));
label
.attr("x", x(h))
.text(formatDate(h));
var newData = dataset.filter(d => d.date < h)
drawPlot(newData);
}
function zoomed() {
zoomScale = d3.event.transform.k
d3.zoomIdentity
.scale(zoomScale)
layer1
.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
plot
.selectAll('circle')
.attr('transform', d3.event.transform);
plot
.selectAll('.city')
.attr('transform', d3.event.transform);
}
d3.select('#zoom-in').on('click', function () {
console.log('zoomin in')
zoom.scaleBy(svg.transition().duration(750), 1.3);
});
d3.select('#zoom-out').on('click', function () {
zoom.scaleBy(svg.transition().duration(750), 1 / 1.3);
});
</script>
</body>
circles.csv:
id,date,lng,lat
1,11/24/04,38.285973,-122.365474
2,03/22/05,38.285973,-122.365474
3,06/02/05,37.792949,-122.459974
4,06/14/05,37.909155,-122.6518833333
5,07/01/05,33.631724,-117.950935
6,08/31/05,33.631724,-117.950935
7,09/01/05,34.039715,-118.888678
8,10/01/05,37.9217562781,-121.9406676292
9,11/14/05,34.039466,-118.579902
10,12/09/05,37.703584,-122.432026
11,02/14/06,38.38033,-123.08087
12,04/06/06,34.045192,-118.940949
13,05/26/06,34.045192,-118.940949
14,06/14/06,37.824757,-122.201442
15,07/07/06,37.816214,-122.210626
16,08/03/06,37.815468,-122.193975
17,09/14/06,37.822113,-122.195606
18,10/25/06,37.814383,-122.184705
19,11/20/06,37.822113,-122.195606
20,12/21/06,37.814383,-122.184705
21,01/23/07,37.31874272,-122.18163484
22,01/30/07,37.5738,-122.471
23,02/25/07,37.5738,-122.471
24,03/16/07,39.044386,-122.534122
25,04/13/07,39.044386,-122.534122
26,05/24/07,34.038791,-118.874559
27,06/16/07,37.31874272,-122.18163484
28,07/23/07,37.27148803,-122.15488797
29,08/13/07,37.32495478,-122.17827938
30,09/06/07,37.31874272,-122.18163484
31,10/19/07,34.125873,-118.707833
32,11/12/07,37.593929,-122.515068
33,12/11/07,37.593929,-122.515068
34,01/01/08,34.086431,-118.704786
35,02/06/08,37.005294,-121.683883
36,03/01/08,37.005294,-121.683883
37,04/01/08,34.038826,-118.875761
38,06/17/08,37.324748,-122.402458
39,07/03/08,37.324748,-122.402458
40,09/18/08,38.1186745227,-122.9507392645
41,10/08/08,37.91733,-122.335
42,11/19/08,37.8963,-122.355
43,12/18/08,37.4872,-121.929
44,01/20/09,34.040533,-118.891725
45,02/13/09,37.30647158,-122.17003327
46,03/20/09,37.30647158,-122.17003327
47,04/10/09,37.741558075,-122.4431838989
48,07/20/09,37.7649307251,-122.4374237061
49,08/10/09,37.502882,-122.478332
50,09/15/09,37.502882,-122.478332
51,10/19/09,37.9058615694,-122.6426225665
52,11/06/09,37.9058615694,-122.6426225665
53,12/17/09,37.4061431885,-122.2400054932
54,02/16/10,39.21226,-123.173217
55,03/14/10,37.895213,-122.031581
56,04/14/10,37.4225234985,-122.1740188599
57,05/05/10,38.601578,-121.138257
58,06/19/10,37.063256,-121.208691
59,07/01/10,37.67334,-122.408981
60,08/24/10,37.67334,-122.408981
61,09/01/10,37.754429,-122.136554
62,10/19/10,37.754429,-122.136554
63,11/06/10,38.5835812168,-122.699008584
64,12/27/10,37.548524,-122.505283
65,01/18/11,37.548524,-122.505283
66,02/22/11,37.919418335,-122.494720459
67,03/11/11,37.27148803,-122.15488797
68,04/26/11,37.27148803,-122.15488797
69,05/16/11,38.927165,-122.986536
70,07/14/11,38.771485,-121.042486
71,09/19/11,38.719269,-120.991916
72,11/16/11,37.27148803,-122.15488797
73,03/28/12,37.27148803,-122.15488797
74,04/19/12,38.593377,-121.462526
75,05/04/12,37.27148803,-122.15488797
76,07/19/12,37.27148803,-122.15488797
77,08/10/12,38.589829,-121.455835
78,09/16/12,34.099758,-118.711652
79,10/21/12,37.6951599121,-122.4476928711
80,11/15/12,37.8140938,-122.1833
81,12/03/12,37.19563508,-121.94371251
82,01/15/13,37.909155,-122.6518833333
83,03/24/13,37.19563508,-121.94371251
84,04/17/13,37.727423,-122.481812
85,05/13/13,37.8577162003,-122.5106131314
86,06/07/13,38.5785140991,-122.6990890503
87,07/03/13,37.281701,-120.862655
88,08/23/13,38.200491,-122.962797
89,09/22/13,37.6238162908,-122.1350258589
90,10/23/13,32.550129,-117.102812
91,11/14/13,32.548826,-117.101935
92,12/06/13,32.547429,-117.10247
93,02/04/14,38.6128130576,-122.7812719345
94,04/15/14,38.5835711527,-122.6989656687
95,07/03/14,37.4002395942,-122.2135899068
96,08/05/14,38.6361160278,-122.858757019
97,09/19/14,36.5,-119
98,10/28/14,36.5,-119
99,11/05/14,35.272334,-120.650649
100,12/19/14,37.8826501667,-122.5498195
101,02/06/15,37.8826501667,-122.5498195
102,03/01/15,33.570829,-117.551149
103,04/10/15,37.053838,-122.13295
104,05/18/15,37.900347,-122.024344
105,06/29/15,38.726416,-123.041826
106,08/20/15,39.3925,-123.648889
107,09/19/15,33.171683,-117.109108
108,10/22/16,37.9372660312,-122.1399235725
109,12/13/16,39.155156,-121.564922
110,02/16/17,32.825965,-117.053016
111,03/24/17,37.215688703,-122.0270562172
用头撞墙想了半天终于想通了
在这里工作的 Plunker:
https://plnkr.co/edit/LnISm6YJp1A5HFPT?open=lib%2Fscript.js
我需要做的不是创建新的投影,而是在创建圆圈时对其进行变换,以根据任何平移说明当前的 x,y 位置,并根据任何缩放变换比例。
下面是代码和一些注释,如果这对任何人都有帮助:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #696969;
}
#play-button {
position: absolute;
bottom: 130px;
left: 50px;
background: #333;
padding-right: 26px;
border-radius: 3px;
border: none;
color: white;
margin: 0;
padding: 0 12px;
width: 60px;
cursor: pointer;
height: 30px;
}
#play-button:hover {
background-color: #696969;
}
.ticks {
font-size: 10px;
}
.track,
.track-inset,
.track-overlay {
stroke-linecap: round;
}
.track {
stroke: #000;
stroke-opacity: 0.3;
stroke-width: 10px;
}
.track-inset {
stroke: #dcdcdc;
stroke-width: 8px;
}
.track-overlay {
pointer-events: stroke;
stroke-width: 50px;
stroke: transparent;
cursor: crosshair;
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: 0.5;
stroke-width: 1.25px;
}
#zoom-buttons {
position: absolute;
margin-left: 10px;
margin-top: 10px;
padding: 5px;
background: #fff;
}
#zoom-buttons button {
background: #efefef;
color: #231F20;
border: 0;
padding: 0;
border-radius: 2px;
width: 25px;
height: 25px;
}
</style>
</head>
<body>
<div id="zoom-buttons">
<button id="zoom-in">+</button>
<button id="zoom-out">-</button>
</div>
<div id="vis">
<button id="play-button">Play</button>
</div>
<script>
var formatDateIntoYear = d3.timeFormat("%Y");
var formatDate = d3.timeFormat("%b %Y");
var parseDate = d3.timeParse("%m/%d/%y");
var startDate = new Date("2004-11-01"),
endDate = new Date("2017-04-01");
var margin = { top: 50, right: 50, bottom: 0, left: 50 },
width = 960 - margin.left - margin.right,
height = 750 - margin.top - margin.bottom;
var svg = d3.select("#vis")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width + 100)
.translate([width / 2, height / 2.2]);
var path = d3.geoPath()
.projection(projection);
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on('zoom', zoomed);
svg.call(zoom)
////////// slider //////////
var moving = false;
var currentValue = 0;
var targetValue = width;
var playButton = d3.select("#play-button");
var x = d3.scaleTime()
.domain([startDate, endDate])
.range([0, targetValue])
.clamp(true);
var slider = svg.append("g")
.attr("class", "slider")
.attr("transform", "translate(" + margin.left + "," + height + ")");
slider.append("line")
.attr("class", "track")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-inset")
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-overlay")
.call(d3.drag()
.on("start.interrupt", function () { slider.interrupt(); })
.on("start drag", function () {
currentValue = d3.event.x;
update(x.invert(currentValue));
})
);
slider.insert("g", ".track-overlay")
.attr("class", "ticks")
.attr("transform", "translate(0," + 18 + ")")
.selectAll("text")
.data(x.ticks(10))
.enter()
.append("text")
.attr("x", x)
.attr("y", 10)
.attr("text-anchor", "middle")
.text(function (d) { return formatDateIntoYear(d); });
var handle = slider.insert("circle", ".track-overlay")
.attr("class", "handle")
.attr("r", 9);
var label = slider.append("text")
.attr("class", "label")
.attr("text-anchor", "middle")
.text(formatDate(startDate))
.attr("transform", "translate(0," + (-25) + ")")
////////// map //////////
var dataset;
var layer1 = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("us.json", function (error, us) {
if (error) throw error;
layer1.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.attr("class", "feature")
.attr("stroke", "#ccc")
.attr("fill", "#fff")
layer1.append("path")
.datum(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
});
var plot = svg.append("g")
.attr("class", "plot")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// define initial event transform
let zoomScale = 1,
panX = 0,
panY = 0
d3.csv("circles.csv", prepare, function (data) {
dataset = data;
drawPlot(dataset);
playButton
.on("click", function () {
var button = d3.select(this);
if (button.text() == "Pause") {
moving = false;
clearInterval(timer);
button.text("Play");
} else {
moving = true;
timer = setInterval(step, 100);
button.text("Pause");
}
})
})
function prepare(d) {
d.id = d.id;
d.date = parseDate(d.date);
return d;
}
function step() {
update(x.invert(currentValue));
currentValue = currentValue + (targetValue / 151);
if (currentValue > targetValue) {
moving = false;
currentValue = 0;
clearInterval(timer);
playButton.text("Play");
}
}
function drawPlot(data) {
var locations = plot.selectAll(".location")
.data(data);
//transform circles based on pan and zoom
locations.enter()
.append("circle")
.attr('transform', "translate(" + panX + "," + panY + ") scale(" + zoomScale + ")")
.attr("class", "location")
.attr('cx', d => projection([d.lat, d.lng])[0])
.attr('cy', d => projection([d.lat, d.lng])[1])
.style("fill", '#333')
.style("stroke", '#111')
.style("opacity", 0.5)
.attr("r", 3)
.transition()
.duration(400)
.attr("r", 8)
locations.exit()
.remove();
}
function update(h) {
handle.attr("cx", x(h));
label
.attr("x", x(h))
.text(formatDate(h));
var newData = dataset.filter(d => d.date < h)
drawPlot(newData);
}
function zoomed() {
// update global transform properties so that new circles know where to go and how large to be
zoomScale = d3.event.transform.k,
panX = d3.event.transform.x,
panY = d3.event.transform.y
d3.zoomIdentity
.scale(zoomScale)
layer1
.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
plot
.selectAll('circle')
.attr('transform', d3.event.transform);
plot
.selectAll('.city')
.attr('transform', d3.event.transform);
}
d3.select('#zoom-in').on('click', function () {
zoom.scaleBy(svg.transition().duration(750), 1.3);
});
d3.select('#zoom-out').on('click', function () {
zoom.scaleBy(svg.transition().duration(750), 1 / 1.3);
});
</script>
</body>
我在 d3 中制作了随时间变化的点地图。它很好用。我添加了缩放。那也行。然而,它们并不完全协同工作。
我发现当我调用函数绘制圆圈时,我需要重新设置投影。我已经正确调整了比例。因此,如果您放大并按播放,圆圈的大小是正确的。
但是它们不在正确的位置,因为我不知道投影的 translate
部分需要是什么。
因此,如果您缩放和平移,然后播放,您会看到如下内容:
如果有人能提供见解,我将不胜感激。我的代码在下面,这里还有一个 Plunker:https://plnkr.co/edit/HlMLKgARm0hgzLPN?open=lib%2Fscript.js (us.json 文件太大,无法粘贴到此处,但在 plunker 中。)
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #696969;
}
#play-button {
position: absolute;
bottom: 130px;
left: 50px;
background: #333;
padding-right: 26px;
border-radius: 3px;
border: none;
color: white;
margin: 0;
padding: 0 12px;
width: 60px;
cursor: pointer;
height: 30px;
}
#play-button:hover {
background-color: #696969;
}
.ticks {
font-size: 10px;
}
.track,
.track-inset,
.track-overlay {
stroke-linecap: round;
}
.track {
stroke: #000;
stroke-opacity: 0.3;
stroke-width: 10px;
}
.track-inset {
stroke: #dcdcdc;
stroke-width: 8px;
}
.track-overlay {
pointer-events: stroke;
stroke-width: 50px;
stroke: transparent;
cursor: crosshair;
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: 0.5;
stroke-width: 1.25px;
}
#zoom-buttons {
position: absolute;
margin-left: 10px;
margin-top: 10px;
padding: 5px;
background: #fff;
}
#zoom-buttons button {
background: #efefef;
color: #231F20;
border: 0;
padding: 0;
border-radius: 2px;
width: 25px;
height: 25px;
}
</style>
</head>
<body>
<div id="zoom-buttons">
<button id="zoom-in">+</button>
<button id="zoom-out">-</button>
</div>
<div id="vis">
<button id="play-button">Play</button>
</div>
<script>
var formatDateIntoYear = d3.timeFormat("%Y");
var formatDate = d3.timeFormat("%b %Y");
var parseDate = d3.timeParse("%m/%d/%y");
var startDate = new Date("2004-11-01"),
endDate = new Date("2017-04-01");
var margin = { top: 50, right: 50, bottom: 0, left: 50 },
width = 960 - margin.left - margin.right,
height = 750 - margin.top - margin.bottom;
var svg = d3.select("#vis")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width + 100)
.translate([width / 2, height / 2.2]);
var path = d3.geoPath()
.projection(projection);
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on('zoom', zoomed);
svg.call(zoom)
////////// slider //////////
var moving = false;
var currentValue = 0;
var targetValue = width;
var playButton = d3.select("#play-button");
var x = d3.scaleTime()
.domain([startDate, endDate])
.range([0, targetValue])
.clamp(true);
var slider = svg.append("g")
.attr("class", "slider")
.attr("transform", "translate(" + margin.left + "," + height + ")");
slider.append("line")
.attr("class", "track")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-inset")
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-overlay")
.call(d3.drag()
.on("start.interrupt", function () { slider.interrupt(); })
.on("start drag", function () {
currentValue = d3.event.x;
update(x.invert(currentValue));
})
);
slider.insert("g", ".track-overlay")
.attr("class", "ticks")
.attr("transform", "translate(0," + 18 + ")")
.selectAll("text")
.data(x.ticks(10))
.enter()
.append("text")
.attr("x", x)
.attr("y", 10)
.attr("text-anchor", "middle")
.text(function (d) { return formatDateIntoYear(d); });
var handle = slider.insert("circle", ".track-overlay")
.attr("class", "handle")
.attr("r", 9);
var label = slider.append("text")
.attr("class", "label")
.attr("text-anchor", "middle")
.text(formatDate(startDate))
.attr("transform", "translate(0," + (-25) + ")")
////////// map //////////
var dataset;
var layer1 = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("us.json", function (error, us) {
if (error) throw error;
layer1.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.attr("class", "feature")
.attr("stroke", "#ccc")
.attr("fill", "#fff")
layer1.append("path")
.datum(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
});
var plot = svg.append("g")
.attr("class", "plot")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
let zoomScale = 1; // initial zoom level
d3.csv("circles.csv", prepare, function (data) {
dataset = data;
drawPlot(dataset);
playButton
.on("click", function () {
var button = d3.select(this);
if (button.text() == "Pause") {
moving = false;
clearInterval(timer);
button.text("Play");
} else {
moving = true;
timer = setInterval(step, 100);
button.text("Pause");
}
console.log("Slider moving: " + moving);
})
})
function prepare(d) {
d.id = d.id;
d.date = parseDate(d.date);
return d;
}
function step() {
update(x.invert(currentValue));
currentValue = currentValue + (targetValue / 151);
if (currentValue > targetValue) {
moving = false;
currentValue = 0;
clearInterval(timer);
// timer = 0;
playButton.text("Play");
console.log("Slider moving: " + moving);
}
}
function drawPlot(data) {
console.log('zoom scale', zoomScale)
// update projection based on zoom
projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale((width + 100) * zoomScale) // this works
.translate([width / 2, height / 2.2]) // what does this need to be?
var locations = plot.selectAll(".location")
.data(data);
locations.enter()
.append("circle")
.attr("class", "location")
.attr('cx', d => projection([d.lat, d.lng])[0])
.attr('cy', d => projection([d.lat, d.lng])[1])
.style("fill", '#333')
.style("stroke", '#111')
.style("opacity", 0.5)
.attr("r", 3)
.transition()
.duration(400)
.attr("r", 8)
locations.exit()
.remove();
}
function update(h) {
handle.attr("cx", x(h));
label
.attr("x", x(h))
.text(formatDate(h));
var newData = dataset.filter(d => d.date < h)
drawPlot(newData);
}
function zoomed() {
zoomScale = d3.event.transform.k
d3.zoomIdentity
.scale(zoomScale)
layer1
.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
plot
.selectAll('circle')
.attr('transform', d3.event.transform);
plot
.selectAll('.city')
.attr('transform', d3.event.transform);
}
d3.select('#zoom-in').on('click', function () {
console.log('zoomin in')
zoom.scaleBy(svg.transition().duration(750), 1.3);
});
d3.select('#zoom-out').on('click', function () {
zoom.scaleBy(svg.transition().duration(750), 1 / 1.3);
});
</script>
</body>
circles.csv:
id,date,lng,lat
1,11/24/04,38.285973,-122.365474
2,03/22/05,38.285973,-122.365474
3,06/02/05,37.792949,-122.459974
4,06/14/05,37.909155,-122.6518833333
5,07/01/05,33.631724,-117.950935
6,08/31/05,33.631724,-117.950935
7,09/01/05,34.039715,-118.888678
8,10/01/05,37.9217562781,-121.9406676292
9,11/14/05,34.039466,-118.579902
10,12/09/05,37.703584,-122.432026
11,02/14/06,38.38033,-123.08087
12,04/06/06,34.045192,-118.940949
13,05/26/06,34.045192,-118.940949
14,06/14/06,37.824757,-122.201442
15,07/07/06,37.816214,-122.210626
16,08/03/06,37.815468,-122.193975
17,09/14/06,37.822113,-122.195606
18,10/25/06,37.814383,-122.184705
19,11/20/06,37.822113,-122.195606
20,12/21/06,37.814383,-122.184705
21,01/23/07,37.31874272,-122.18163484
22,01/30/07,37.5738,-122.471
23,02/25/07,37.5738,-122.471
24,03/16/07,39.044386,-122.534122
25,04/13/07,39.044386,-122.534122
26,05/24/07,34.038791,-118.874559
27,06/16/07,37.31874272,-122.18163484
28,07/23/07,37.27148803,-122.15488797
29,08/13/07,37.32495478,-122.17827938
30,09/06/07,37.31874272,-122.18163484
31,10/19/07,34.125873,-118.707833
32,11/12/07,37.593929,-122.515068
33,12/11/07,37.593929,-122.515068
34,01/01/08,34.086431,-118.704786
35,02/06/08,37.005294,-121.683883
36,03/01/08,37.005294,-121.683883
37,04/01/08,34.038826,-118.875761
38,06/17/08,37.324748,-122.402458
39,07/03/08,37.324748,-122.402458
40,09/18/08,38.1186745227,-122.9507392645
41,10/08/08,37.91733,-122.335
42,11/19/08,37.8963,-122.355
43,12/18/08,37.4872,-121.929
44,01/20/09,34.040533,-118.891725
45,02/13/09,37.30647158,-122.17003327
46,03/20/09,37.30647158,-122.17003327
47,04/10/09,37.741558075,-122.4431838989
48,07/20/09,37.7649307251,-122.4374237061
49,08/10/09,37.502882,-122.478332
50,09/15/09,37.502882,-122.478332
51,10/19/09,37.9058615694,-122.6426225665
52,11/06/09,37.9058615694,-122.6426225665
53,12/17/09,37.4061431885,-122.2400054932
54,02/16/10,39.21226,-123.173217
55,03/14/10,37.895213,-122.031581
56,04/14/10,37.4225234985,-122.1740188599
57,05/05/10,38.601578,-121.138257
58,06/19/10,37.063256,-121.208691
59,07/01/10,37.67334,-122.408981
60,08/24/10,37.67334,-122.408981
61,09/01/10,37.754429,-122.136554
62,10/19/10,37.754429,-122.136554
63,11/06/10,38.5835812168,-122.699008584
64,12/27/10,37.548524,-122.505283
65,01/18/11,37.548524,-122.505283
66,02/22/11,37.919418335,-122.494720459
67,03/11/11,37.27148803,-122.15488797
68,04/26/11,37.27148803,-122.15488797
69,05/16/11,38.927165,-122.986536
70,07/14/11,38.771485,-121.042486
71,09/19/11,38.719269,-120.991916
72,11/16/11,37.27148803,-122.15488797
73,03/28/12,37.27148803,-122.15488797
74,04/19/12,38.593377,-121.462526
75,05/04/12,37.27148803,-122.15488797
76,07/19/12,37.27148803,-122.15488797
77,08/10/12,38.589829,-121.455835
78,09/16/12,34.099758,-118.711652
79,10/21/12,37.6951599121,-122.4476928711
80,11/15/12,37.8140938,-122.1833
81,12/03/12,37.19563508,-121.94371251
82,01/15/13,37.909155,-122.6518833333
83,03/24/13,37.19563508,-121.94371251
84,04/17/13,37.727423,-122.481812
85,05/13/13,37.8577162003,-122.5106131314
86,06/07/13,38.5785140991,-122.6990890503
87,07/03/13,37.281701,-120.862655
88,08/23/13,38.200491,-122.962797
89,09/22/13,37.6238162908,-122.1350258589
90,10/23/13,32.550129,-117.102812
91,11/14/13,32.548826,-117.101935
92,12/06/13,32.547429,-117.10247
93,02/04/14,38.6128130576,-122.7812719345
94,04/15/14,38.5835711527,-122.6989656687
95,07/03/14,37.4002395942,-122.2135899068
96,08/05/14,38.6361160278,-122.858757019
97,09/19/14,36.5,-119
98,10/28/14,36.5,-119
99,11/05/14,35.272334,-120.650649
100,12/19/14,37.8826501667,-122.5498195
101,02/06/15,37.8826501667,-122.5498195
102,03/01/15,33.570829,-117.551149
103,04/10/15,37.053838,-122.13295
104,05/18/15,37.900347,-122.024344
105,06/29/15,38.726416,-123.041826
106,08/20/15,39.3925,-123.648889
107,09/19/15,33.171683,-117.109108
108,10/22/16,37.9372660312,-122.1399235725
109,12/13/16,39.155156,-121.564922
110,02/16/17,32.825965,-117.053016
111,03/24/17,37.215688703,-122.0270562172
用头撞墙想了半天终于想通了
在这里工作的 Plunker: https://plnkr.co/edit/LnISm6YJp1A5HFPT?open=lib%2Fscript.js
我需要做的不是创建新的投影,而是在创建圆圈时对其进行变换,以根据任何平移说明当前的 x,y 位置,并根据任何缩放变换比例。
下面是代码和一些注释,如果这对任何人都有帮助:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://unpkg.com/geo-albers-usa-territories@0.1.0/dist/geo-albers-usa-territories.js"></script>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #696969;
}
#play-button {
position: absolute;
bottom: 130px;
left: 50px;
background: #333;
padding-right: 26px;
border-radius: 3px;
border: none;
color: white;
margin: 0;
padding: 0 12px;
width: 60px;
cursor: pointer;
height: 30px;
}
#play-button:hover {
background-color: #696969;
}
.ticks {
font-size: 10px;
}
.track,
.track-inset,
.track-overlay {
stroke-linecap: round;
}
.track {
stroke: #000;
stroke-opacity: 0.3;
stroke-width: 10px;
}
.track-inset {
stroke: #dcdcdc;
stroke-width: 8px;
}
.track-overlay {
pointer-events: stroke;
stroke-width: 50px;
stroke: transparent;
cursor: crosshair;
}
.handle {
fill: #fff;
stroke: #000;
stroke-opacity: 0.5;
stroke-width: 1.25px;
}
#zoom-buttons {
position: absolute;
margin-left: 10px;
margin-top: 10px;
padding: 5px;
background: #fff;
}
#zoom-buttons button {
background: #efefef;
color: #231F20;
border: 0;
padding: 0;
border-radius: 2px;
width: 25px;
height: 25px;
}
</style>
</head>
<body>
<div id="zoom-buttons">
<button id="zoom-in">+</button>
<button id="zoom-out">-</button>
</div>
<div id="vis">
<button id="play-button">Play</button>
</div>
<script>
var formatDateIntoYear = d3.timeFormat("%Y");
var formatDate = d3.timeFormat("%b %Y");
var parseDate = d3.timeParse("%m/%d/%y");
var startDate = new Date("2004-11-01"),
endDate = new Date("2017-04-01");
var margin = { top: 50, right: 50, bottom: 0, left: 50 },
width = 960 - margin.left - margin.right,
height = 750 - margin.top - margin.bottom;
var svg = d3.select("#vis")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
var projection = geoAlbersUsaTerritories.geoAlbersUsaTerritories()
.scale(width + 100)
.translate([width / 2, height / 2.2]);
var path = d3.geoPath()
.projection(projection);
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on('zoom', zoomed);
svg.call(zoom)
////////// slider //////////
var moving = false;
var currentValue = 0;
var targetValue = width;
var playButton = d3.select("#play-button");
var x = d3.scaleTime()
.domain([startDate, endDate])
.range([0, targetValue])
.clamp(true);
var slider = svg.append("g")
.attr("class", "slider")
.attr("transform", "translate(" + margin.left + "," + height + ")");
slider.append("line")
.attr("class", "track")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-inset")
.select(function () { return this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-overlay")
.call(d3.drag()
.on("start.interrupt", function () { slider.interrupt(); })
.on("start drag", function () {
currentValue = d3.event.x;
update(x.invert(currentValue));
})
);
slider.insert("g", ".track-overlay")
.attr("class", "ticks")
.attr("transform", "translate(0," + 18 + ")")
.selectAll("text")
.data(x.ticks(10))
.enter()
.append("text")
.attr("x", x)
.attr("y", 10)
.attr("text-anchor", "middle")
.text(function (d) { return formatDateIntoYear(d); });
var handle = slider.insert("circle", ".track-overlay")
.attr("class", "handle")
.attr("r", 9);
var label = slider.append("text")
.attr("class", "label")
.attr("text-anchor", "middle")
.text(formatDate(startDate))
.attr("transform", "translate(0," + (-25) + ")")
////////// map //////////
var dataset;
var layer1 = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("us.json", function (error, us) {
if (error) throw error;
layer1.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.attr("class", "feature")
.attr("stroke", "#ccc")
.attr("fill", "#fff")
layer1.append("path")
.datum(topojson.mesh(us, us.objects.states, function (a, b) { return a !== b; }))
.attr("class", "mesh")
.attr("d", path)
.attr('fill', 'none')
});
var plot = svg.append("g")
.attr("class", "plot")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// define initial event transform
let zoomScale = 1,
panX = 0,
panY = 0
d3.csv("circles.csv", prepare, function (data) {
dataset = data;
drawPlot(dataset);
playButton
.on("click", function () {
var button = d3.select(this);
if (button.text() == "Pause") {
moving = false;
clearInterval(timer);
button.text("Play");
} else {
moving = true;
timer = setInterval(step, 100);
button.text("Pause");
}
})
})
function prepare(d) {
d.id = d.id;
d.date = parseDate(d.date);
return d;
}
function step() {
update(x.invert(currentValue));
currentValue = currentValue + (targetValue / 151);
if (currentValue > targetValue) {
moving = false;
currentValue = 0;
clearInterval(timer);
playButton.text("Play");
}
}
function drawPlot(data) {
var locations = plot.selectAll(".location")
.data(data);
//transform circles based on pan and zoom
locations.enter()
.append("circle")
.attr('transform', "translate(" + panX + "," + panY + ") scale(" + zoomScale + ")")
.attr("class", "location")
.attr('cx', d => projection([d.lat, d.lng])[0])
.attr('cy', d => projection([d.lat, d.lng])[1])
.style("fill", '#333')
.style("stroke", '#111')
.style("opacity", 0.5)
.attr("r", 3)
.transition()
.duration(400)
.attr("r", 8)
locations.exit()
.remove();
}
function update(h) {
handle.attr("cx", x(h));
label
.attr("x", x(h))
.text(formatDate(h));
var newData = dataset.filter(d => d.date < h)
drawPlot(newData);
}
function zoomed() {
// update global transform properties so that new circles know where to go and how large to be
zoomScale = d3.event.transform.k,
panX = d3.event.transform.x,
panY = d3.event.transform.y
d3.zoomIdentity
.scale(zoomScale)
layer1
.selectAll('path') // To prevent stroke width from scaling
.attr('transform', d3.event.transform);
plot
.selectAll('circle')
.attr('transform', d3.event.transform);
plot
.selectAll('.city')
.attr('transform', d3.event.transform);
}
d3.select('#zoom-in').on('click', function () {
zoom.scaleBy(svg.transition().duration(750), 1.3);
});
d3.select('#zoom-out').on('click', function () {
zoom.scaleBy(svg.transition().duration(750), 1 / 1.3);
});
</script>
</body>