-
Notifications
You must be signed in to change notification settings - Fork 36
/
Copy pathzoom.html
134 lines (111 loc) · 5.38 KB
/
zoom.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<!DOCTYPE html>
<meta charset='utf-8'>
<style>
.axis line {
stroke-opacity: 0.3;
shape-rendering: crispEdges;
}
circle {
fill: transparent;
stroke: black;
stroke-width:1;
}
text {
font-size: 3;
fill: red;
}
</style>
<svg width='1000' height='500' preserveAspectRatio='none'>
</svg>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.js'></script>
<script>
var svg = d3.select('svg'),
// px dimensions of svg grid
pxWidth = +svg.attr('width'),
pxHeight = +svg.attr('height'),
// rwu dimensions (coordinates used within grid)
rwuHeight = 100,
// calculate a width based on the aspect ratio of the svg element and the real world unit height
rwuWidth = (pxWidth / pxHeight) * rwuHeight;
// set viewbox on svg in rwu so drawing coordinates are in rwu and not pixels
svg.attr('viewBox', `0 0 ${rwuWidth} ${rwuHeight}`);
// zoomScaleX/zoomScaleY are used to scale elements based on zoom behavior transformations
// the scales themselves are a 1 to 1 mapping, in the zoomHandler the scales are cloned and transformed based on the zoom event transformations
// the transformed scales are then applied to the axes, causing the grid to display with the correct dimensions
const zoomScaleX = d3.scaleLinear()
.domain([0, rwuWidth])
.range([0, rwuWidth]),
zoomScaleY = d3.scaleLinear()
.domain([0, rwuHeight])
.range([0, rwuHeight]);
// translates from pixels to real world units
const rwuScaleX = d3.scaleLinear()
.domain([0, pxWidth])
.range([0, rwuWidth]),
rwuScaleY = d3.scaleLinear()
.domain([0, pxHeight])
.range([0, rwuHeight]);
// generator functions for axes
const xAxisGenerator = d3.axisBottom(zoomScaleX)
// calculate number of horizontal ticks based on the aspect ratio of the svg element and the real world unit height
.ticks(10 * (rwuWidth / rwuHeight)) // number of ticks (multiplied by width to height ratio)
.tickSize(rwuHeight) // length of tick marks (full height of grid in rwu coming up from x axis)
.tickPadding(rwuScaleY(-20)), // padding between axisBottom and tick text (20px translated to rwu)
yAxisGenerator = d3.axisRight(zoomScaleY)
.ticks(10) // number of ticks
.tickSize(rwuWidth) // length of tick marks (full width of grid in rwu coming up from y axis)
.tickPadding(rwuScaleX(-20)); // padding between axisRight and tick text (20px translated to rwu)
const xAxis = svg.append('g')
.attr('class', 'axis axis--x')
.style('font-size', '2')
.call(xAxisGenerator),
yAxis = svg.append('g')
.attr('class', 'axis axis--y')
.style('font-size', '2')
.call(yAxisGenerator);
// configure zoom behavior in rwu
const zoomBehavior = d3.zoom()
.extent([[0, 0], [rwuWidth, rwuHeight]])
// scale must be between .5 * original bounds and 2 * original bounds
// .scaleExtent([.5, 10])
// allow panning by 20 rwu in any direction
// .translateExtent([[-20, -20], [rwuWidth + 20, rwuHeight + 20]])
.on('zoom', () => {
// don't change the original scale or you'll get exponential growth
// x = d3.event.transform.rescaleX(x)
// y = d3.event.transform.rescaleX(y)
//rescale the saved circles
circles.forEach(c => c.attr('transform', d3.event.transform))
const scaledRwuHeight = d3.event.transform.rescaleY(zoomScaleY).domain()[0] - d3.event.transform.rescaleY(zoomScaleY).domain()[1],
scaledRwuWidth = d3.event.transform.rescaleX(zoomScaleX).domain()[0] - d3.event.transform.rescaleX(zoomScaleX).domain()[1]
yAxis.call(yAxisGenerator.ticks( -10 * (scaledRwuHeight /rwuHeight) ));
xAxis.call(xAxisGenerator.ticks( -10 * (rwuWidth / rwuHeight) * (scaledRwuWidth /rwuWidth) ));
// create transformed copies of the scales and apply them to the axes
xAxis.call(xAxisGenerator.scale(d3.event.transform.rescaleX(zoomScaleX)));
yAxis.call(yAxisGenerator.scale(d3.event.transform.rescaleY(zoomScaleY)));
});
svg.call(zoomBehavior);
const circles = [];
// draw a new circle when the grid is clicked
svg.on('click', () => {
const circleGroup = svg.append('g');
const randomRadius = (rwuWidth * Math.random()) / 10;
circleGroup.append('circle')
.attr('r', randomRadius)
// translate coordinates of click to rwu and use for circle center
.attr('cx', rwuScaleX(d3.event.clientX))
.attr('cy', rwuScaleY(d3.event.clientY));
circleGroup.append('text')
.attr('dx', rwuScaleX(d3.event.clientX) - randomRadius + 3)
.attr('dy', rwuScaleY(d3.event.clientY) )
.text(`RWU: (${rwuScaleX(d3.event.clientX)}, ${rwuScaleY(d3.event.clientY)})`)
.attr('font-size', '3');
circleGroup.append('text')
.attr('dx', rwuScaleX(d3.event.clientX) - randomRadius + 3)
.attr('dy', rwuScaleY(d3.event.clientY) - 3)
.text(`PX: (${d3.event.clientX}, ${d3.event.clientY})`)
.attr('font-size', '3');
// store circle in circles array so that it can be rescaled in the zoom handler
circles.push(circleGroup);
});
</script>