尺度
简介
D3 提供多种称为 scale 的结构来支持从模型到可视化模型的映射。尺度不仅包括这种映射,同时也是其他组件的基础,例如过渡和坐标轴。
使用连续尺度
div id="linear" class="clear"><span>n</span></div>
<div id="linear-capped" class="clear">
<span>1 <= a*n + b <= 20</span>
</div>
<div id="pow" class="clear"><span>n^2</span></div>
<div id="pow-capped" class="clear">
<span>1 <= a*n^2 + b <= 10</span>
</div>
<div id="log" class="clear"><span>log(n)</span></div>
<div id="log-capped" class="clear">
<span>1 <= a*log(n) + b <= 10</span>
</div>
<script type="text/javascript">
var max = 11, data = [];
for (var i = 1; i < max; ++i) data.push(i);
var linear = d3.scaleLinear() // 线性尺度
.domain([1, 10]) // 值域
.range([1, 10]); // 定义域
var linearCapped = d3.scaleLinear()
.domain([1, 10])
.range([1, 20]);
var pow = d3.scalePow().exponent(2); // 指数为 2 的幂级尺度
var powCapped = d3.scalePow()
.exponent(2)
.domain([1, 10])
.rangeRound([1, 10]); // rangeRound 会对值域的数字进行向上取整
var log = d3.scaleLog(); // 对数尺度
var logCapped = d3.scaleLog()
.domain([1, 10])
.rangeRound([1, 10]);
function render(data, scale, selector) {
d3.select(selector).selectAll("div")
.data(data)
.enter()
.append("div")
.classed("cell", true)
.style("display", "inline-block")
.text(function (d) {
return d3.format(".2")(scale(d), 2);
});
}
render(data, linear, "#linear");
render(data, linearCapped, "#linear-capped");
render(data, pow, "#pow");
render(data, powCapped, "#pow-capped");
render(data, log, "#log");
render(data, logCapped, "#log-capped");
</script>
时间尺度
<div id="time" class="clear">
<span>Linear Time Progression<br></span>
<span>Mapping [01/01/2016, 12/31/2016] to [0, 1200]<br></span>
</div>
<script type="text/javascript">
var start = new Date(2016, 0, 1), // <-A
end = new Date(2016, 11, 31),
range = [0, 1200],
time = d3.scaleTime().domain([start, end]) // <-B
.rangeRound(range), // <-C
max = 12,
data = [];
for (var i = 0; i < max; ++i){ // <-D
var date = new Date(start.getTime());
date.setMonth(start.getMonth() + i);
data.push(date);
}
function render(data, scale, selector) { // <-E
d3.select(selector).selectAll("div.fixed-cell")
.data(data)
.enter()
.append("div")
.classed("fixed-cell", true)
.style("margin-left", function(d){ // <-F
return scale(d) + "px";
})
.html(function (d) { // <-G
var format = d3.timeFormat("%x"); // <-H
return format(d) + "<br>" + scale(d) + "px";
});
}
render(data, time, "#time");
</script>
有序尺度
<div id="alphabet" class="clear">
<span>Ordinal Scale with Alphabet<br></span>
<span>Mapping [1..10] to ["a".."j"]<br></span>
</div>
<div id="category10" class="clear">
<span>Ordinal Color Scale Category 10<br></span>
<span>Mapping [1..10] to category 10 colors<br></span>
</div>
<div id="category20" class="clear">
<span>Ordinal Color Scale Category 20<br></span>
<span>Mapping [1..10] to category 20 colors<br></span>
</div>
<div id="category20b" class="clear">
<span>Ordinal Color Scale Category 20b<br></span>
<span>Mapping [1..10] to category 20b colors<br></span>
</div>
<div id="category20c" class="clear">
<span>Ordinal Color Scale Category 20c<br></span>
<span>Mapping [1..10] to category 20c colors<br></span>
</div>
<script type="text/javascript">
var max = 10, data = [];
for (var i = 1; i <= max; ++i) data.push(i);
var alphabet = d3.scaleOrdinal()
.domain(data)
.range(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]);
function render(data, scale, selector) {
var cells = d3.select(selector).selectAll("div.cell")
.data(data);
cells.enter()
.append("div")
.classed("cell", true)
.style("display", "inline-block")
.style("background-color", function(d){
return scale(d).indexOf("#") >=0 ? scale(d) : "white";
})
.text(function (d) {
return scale(d);
});
}
render(data, alphabet, "#alphabet");
render(data, d3.scaleOrdinal(d3.schemeCategory10), "#category10");
render(data, d3.scaleOrdinal(d3.schemeCategory20), "#category20");
render(data, d3.scaleOrdinal(d3.schemeCategory20b), "#category20b");
render(data, d3.scaleOrdinal(d3.schemeCategory20c), "#category20c");
</script>
什么是插值
尺度是这样选取不同的值的:给定一个函数 f(X) 在 x0,x1,x1…,xn 处的值。现有 x’,其值在上述取值点之间。那么,求 f(x’) 近似值的过程叫做插值。
字符串插值
<div id="font" class="clear">
<span>Font Interpolation<br></span>
</div>
<script type="text/javascript">
var max = 11, data = [];
var sizeScale = d3.scaleLinear() // <-A
.domain([0, max])
.range([ // <-B
"italic bold 12px/30px Georgia, serif",
"italic bold 120px/180px Georgia, serif"
]);
for (var i = 0; i < max; ++i) data.push(i);
function render(data, scale, selector) {
var cells = d3.select(selector).selectAll("div.cell")
.data(data);
cells.enter()
.append("div")
.classed("cell", true)
.style("display", "inline-block")
.append("span")
.style("font", function(d,i){
return scale(d);
})
.text(function(d,i){return i;});
}
render(data, sizeScale, "#font");
</script>
默认情况下,线性标尺将使用 d3.interpolateString
函数来处理基于字符串的值域。d3.interpolateString
函数会找出字符串中内嵌的数字,只针对这些数字进行插值。本例中,实际上是使用线性标尺将定义域映射为字体大小。
颜色插值
<div id="color" class="clear">
<span>Linear Color Interpolation<br></span>
</div>
<div id="color-diverge" class="clear">
<span>Poly-Linear Color Interpolation<br></span>
</div>
<script type="text/javascript">
var max = 21, data = [];
var colorScale = d3.scaleLinear()
.domain([0, max])
.range(["white", "#4169e1"]);
var divergingScale = function(pivot) {
return d3.scaleLinear()
.domain([0, pivot, max])
.range(["white", "#4169e1", "white"])
// 分段标尺是一种非均匀的线性标尺,相当于两种标尺的组合
};
for (var i = 0; i < max; ++i) data.push(i);
function render(data, scale, selector) {
var cells = d3.select(selector).selectAll("div.cell")
.data(data);
cells.enter()
.append("div").merge(cells)
.classed("cell", true)
.style("display", "inline-block")
.style("background-color", function(d){
return scale(d);
})
.text(function(d,i){return i;});
}
render(data, colorScale, "#color");
render(data, divergingScale(5), "#color-diverge");
</script>
<div class="control-group clear">
<button onclick="render(data, divergingScale(5), '#color-diverge')">Pivot at 5</button>
<button onclick="render(data, divergingScale(10), '#color-diverge')">Pivot at 10</button>
<button onclick="render(data, divergingScale(15), '#color-diverge')">Pivot at 15</button>
<button onclick="render(data, divergingScale(20), '#color-diverge')">Pivot at 20</button>
</div>
复合对象插值
<div id="compound" class="clear">
<span>Compound Interpolation<br></span>
</div>
<script type="text/javascript">
var max = 21, data = [];
var compoundScale = d3.scalePow()
.exponent(2)
.domain([0, max])
.range([
{color:"#add8e6", height:"15px"},
{color:"#4169e1", height:"150px"}
]);
for (var i = 0; i < max; ++i) data.push(i);
function render(data, scale, selector) {
var bars = d3.select(selector).selectAll("div.v-bar")
.data(data);
bars.enter()
.append("div")
.classed("v-bar", true)
.style("height", function(d){
return scale(d).height;
})
.style("background-color", function(d){
return scale(d).color;
})
.text(function(d,i){return i;});
}
render(data, compoundScale, "#compound");
</script>
坐标轴
基础
<div class="control-group">
<button onclick="renderAll(d3.axisBottom)">//坐标轴支持四种朝向:上、下、左、右
horizontal bottom
</button>
<button onclick="renderAll(d3.axisTop)">
horizontal top
</button>
<button onclick="renderAll(d3.axisLeft)">
vertical left
</button>
<button onclick="renderAll(d3.axisRight)">
vertical right
</button>
</div>
<script type="text/javascript">
var height = 500,
width = 500,
margin = 25,
offset = 50,
axisWidth = width - 2 * margin,
svg;
function createSvg(){ // <-A
svg = d3.select("body").append("svg")
.attr("class", "axis")
.attr("width", width)
.attr("height", height);
}
function renderAxis(fn, scale, i){
var axis = fn()
.scale(scale)
.ticks(5); // 有五个刻度
svg.append("g")
.attr("transform", function(){
if([d3.axisTop, d3.axisBottom].indexOf(fn) >= 0)
return "translate(" + margin + "," + i * offset + ")";
else
return "translate(" + i * offset + ", " + margin + ")";
})
.call(axis);
}
function renderAll(fn){
if(svg) svg.remove();
createSvg();
renderAxis(fn, d3.scaleLinear()
.domain([0, 1000])
.range([0, axisWidth]), 1);
renderAxis(fn, d3.scalePow()
.exponent(2)
.domain([0, 1000])
.range([0, axisWidth]), 2);
renderAxis(fn, d3.scaleTime()
.domain([new Date(2016, 0, 1), new Date(2017, 0, 1)])
.range([0, axisWidth]), 3);
}
</script>
自定义刻度
<script type="text/javascript">
var height = 500,
width = 500,
margin = 25,
axisWidth = width - 2 * margin;
var svg = d3.select("body").append("svg")
.attr("class", "axis")
.attr("width", width)
.attr("height", height);
var scale = d3.scaleLinear()
.domain([0, 1]).range([0, axisWidth]);
var axis = d3.axisBottom()
.scale(scale)
.ticks(10)
.tickSize(12) // 刻度大小 12 px
.tickPadding(10) // 标签数字与坐标轴的距离 10 px
.tickFormat(d3.format(".0%"));
svg.append("g")
.attr("transform", function(){
return "translate(" + margin +
"," + margin + ")";
})
.call(axis);
</script>
绘制表格线
<script type="text/javascript">
var height = 500,
width = 500,
margin = 25;
var svg = d3.select("body").append("svg")
.attr("class", "axis")
.attr("width", width)
.attr("height", height);
function renderXAxis(){
var axisLength = width - 2 * margin;
var scale = d3.scaleLinear()
.domain([0, 100])
.range([0, axisLength]);
var xAxis = d3.axisBottom()
.scale(scale);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", function(){
return "translate(" + margin + "," + (height - margin) + ")";
})
.call(xAxis);
d3.selectAll("g.x-axis g.tick")
.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", - (height - 2 * margin));
}
function renderYAxis(){
var axisLength = height - 2 * margin;
var scale = d3.scaleLinear()
.domain([100, 0])
.range([0, axisLength]);
var yAxis = d3.axisLeft()
.scale(scale);
svg.append("g")
.attr("class", "y-axis")
.attr("transform", function(){
return "translate(" + margin + "," + margin + ")";
})
.call(yAxis);
d3.selectAll("g.y-axis g.tick")
.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", axisLength)
.attr("y2", 0);
}
renderYAxis();
renderXAxis();
</script>
动态调整坐标尺度
<script type="text/javascript">
var height = 500,
width = 500,
margin = 25,
xAxis, yAxis, xAxisLength, yAxisLength;
var svg = d3.select("body").append("svg")
.attr("class", "axis")
.attr("width", width)
.attr("height", height);
function renderXAxis(){
xAxisLength = width - 2 * margin;
var scale = d3.scaleLinear()
.domain([0, 100])
.range([0, xAxisLength]);
xAxis = d3.axisBottom()
.scale(scale);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", function(){
return "translate(" + margin + "," + (height - margin) + ")";
})
.call(xAxis);
}
function renderYAxis(){
yAxisLength = height - 2 * margin;
var scale = d3.scaleLinear()
.domain([100, 0])
.range([0, yAxisLength]);
yAxis = d3.axisLeft()
.scale(scale);
svg.append("g")
.attr("class", "y-axis")
.attr("transform", function(){
return "translate(" + margin + "," + margin + ")";
})
.call(yAxis);
}
function rescale(){
var max = Math.round(Math.random() * 100);
xAxis.scale().domain([0, max]);
svg.select("g.x-axis")
.transition()
.call(xAxis);
yAxis.scale().domain([max, 0]);
svg.select("g.y-axis")
.transition()
.call(yAxis);
renderXGridlines();
renderYGridlines();
}
function renderXGridlines(){
d3.selectAll("g.x-axis g.tick")
.select("line.grid-line")
.remove();
d3.selectAll("g.x-axis g.tick")
.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", - yAxisLength);
}
function renderYGridlines(){
d3.selectAll("g.y-axis g.tick")
.select("line.grid-line").remove();
d3.selectAll("g.y-axis g.tick")
.append("line")
.classed("grid-line", true)
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", xAxisLength)
.attr("y2", 0);
}
renderYAxis();
renderXAxis();
renderXGridlines();
renderYGridlines();
</script>
<div class="control-group">
<button onclick="rescale()">ReScale</button>
</div>
参考
【1】Data Visualization and D3.js(视频)
【2】D3 4.x 数据可视化实战手册@[加]朱启
【3】https://github.com/NickQiZhu/d3-cookbook-v2(D3 4.x 数据可视化实战手册 代码)
【4】D3.js 中文文档