Как показать полный текст при масштабировании и усечении при уменьшении
Я создаю древовидную диаграмму с d3.js, она отлично работает... но я хочу, чтобы текст реагировал на масштабирование. Вот JSFiddle.
Посмотрите сначала node... у него много символов (в моем случае max будет 255)
При увеличении или уменьшении моего текста текст остается таким же, но я хочу видеть все при увеличении.
var json = {
"name": "Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia FernandezMaude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude Charlotte Licia Fernandez Maude asdlkhkjh asd asdsd",
"id": "06ada7cd-3078-54bc-bb87-72e9d6f38abf",
"_parents": [{
"name": "Janie Clayton Norton",
"id": "a39bfa73-6617-5e8e-9470-d26b68787e52",
"_parents": [{
"name": "Pearl Cannon",
"id": "fc956046-a5c3-502f-b853-d669804d428f",
"_parents": [{
"name": "Augusta Miller",
"id": "fa5b0c07-9000-5475-a90e-b76af7693a57"
}, {
"name": "Clayton Welch",
"id": "3194517d-1151-502e-a3b6-d1ae8234c647"
}]
}, {
"name": "Nell Morton",
"id": "06c7b0cb-cd21-53be-81bd-9b088af96904",
"_parents": [{
"name": "Lelia Alexa Hernandez",
"id": "667d2bb6-c26e-5881-9bdc-7ac9805f96c2"
}, {
"name": "Randy Welch",
"id": "104039bb-d353-54a9-a4f2-09fda08b58bb"
}]
}]
}, {
"name": "Helen Donald Alvarado",
"id": "522266d2-f01a-5ec0-9977-622e4cb054c0",
"_parents": [{
"name": "Gussie Glover",
"id": "da430aa2-f438-51ed-ae47-2d9f76f8d831",
"_parents": [{
"name": "Mina Freeman",
"id": "d384197e-2e1e-5fb2-987b-d90a5cdc3c15"
}, {
"name": "Charlotte Ahelandro Martin",
"id": "ea01728f-e542-53a6-acd0-6f43805c31a3"
}]
}, {
"name": "Jesus Christ Pierce",
"id": "bfd1612c-b90d-5975-824c-49ecf62b3d5f",
"_parents": [{
"name": "Donald Freeman Cox",
"id": "4f910be4-b827-50be-b783-6ba3249f6ebc"
}, {
"name": "Alex Fernandez Gonzales",
"id": "efb2396d-478a-5cbc-b168-52e028452f3b"
}]
}]
}]
};
var boxWidth = 250,
boxHeight = 100;
// Setup zoom and pan
var zoom = d3.behavior.zoom()
.scaleExtent([.1, 1])
.on('zoom', function() {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
})
// Offset so that first pan and zoom does not jump back to the origin
.translate([600, 600]);
var svg = d3.select("body").append("svg")
.attr('width', 1000)
.attr('height', 500)
.call(zoom)
.append('g')
// Left padding of tree so that the whole root node is on the screen.
// TODO: find a better way
.attr("transform", "translate(150,200)");
var tree = d3.layout.tree()
// Using nodeSize we are able to control
// the separation between nodes. If we used
// the size parameter instead then d3 would
// calculate the separation dynamically to fill
// the available space.
.nodeSize([100, 200])
// By default, cousins are drawn further apart than siblings.
// By returning the same value in all cases, we draw cousins
// the same distance apart as siblings.
.separation(function() {
return .9;
})
// Tell d3 what the child nodes are. Remember, we're drawing
// a tree so the ancestors are child nodes.
.children(function(person) {
return person._parents;
});
var nodes = tree.nodes(json),
links = tree.links(nodes);
// Style links (edges)
svg.selectAll("path.link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", elbow);
// Style nodes
var node = svg.selectAll("g.person")
.data(nodes)
.enter().append("g")
.attr("class", "person")
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Draw the rectangle person boxes
node.append("rect")
.attr({
x: -(boxWidth / 2),
y: -(boxHeight / 2),
width: boxWidth,
height: boxHeight
});
// Draw the person name and position it inside the box
node.append("text")
.attr("text-anchor", "start")
.attr('class', 'name')
.text(function(d) {
return d.name;
});
// Text wrap on all nodes using d3plus. By default there is not any left or
// right padding. To add padding we would need to draw another rectangle,
// inside of the rectangle with the border, that represents the area we would
// like the text to be contained in.
d3.selectAll("text").each(function(d, i) {
d3plus.textwrap()
.container(d3.select(this))
.valign("middle")
.draw();
});
/**
* Custom path function that creates straight connecting lines.
*/
function elbow(d) {
return "M" + d.source.y + "," + d.source.x + "H" + (d.source.y + (d.target.y - d.source.y) / 2) + "V" + d.target.x + "H" + d.target.y;
}
body {
text-align: center;
}
svg {
margin-top: 32px;
border: 1px solid #aaa;
}
.person rect {
fill: #fff;
stroke: steelblue;
stroke-width: 1px;
}
.person {
font: 14px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3plus/1.8.0/d3plus.min.js"></script>
Ответы
Ответ 1
Код в этот jsfiddle является попыткой решить проблемы с производительностью, которые у вас есть с очень большими древовидными диаграммами. Задержка устанавливается с помощью setTimeout
в обработчике события масштабирования, чтобы обеспечить масштабирование на "полной скорости" без изменения размера текста. Как только масштабирование остановится на короткое время, текст будет изменен в соответствии с новым масштабированием:
var scaleValue = 1;
var refreshTimeout;
var refreshDelay = 0;
var zoom = d3.behavior.zoom()
.scaleExtent([.1, 1.5])
.on('zoom', function () {
svg.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
scaleValue = d3.event.scale;
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
refreshTimeout = setTimeout(function () {
wrapText();
}, refreshDelay);
})
Задержка (в миллисекундах) зависит от количества узлов в дереве. Вы можете поэкспериментировать с математическим выражением, чтобы найти наилучшие параметры для широкого диапазона подсчетов node, которые вы ожидаете в своем дереве.
// Calculate the refresh delay
refreshDelay = Math.pow(node.size(), 0.5) * 2.0;
Вы также можете установить параметры в calcFontSize
в соответствии с вашими потребностями:
// Calculate the font size for the current scaling
var calcFontSize = function () {
return Math.min(24, 10 * Math.pow(scaleValue, -0.25))
}
Инициализация узлов была слегка изменена:
node.append("rect")
.attr({
x: 0,
y: -(boxHeight / 2),
width: boxWidth,
height: boxHeight
});
node.append("text")
.attr("text-anchor", "start")
.attr("dominant-baseline", "middle")
.attr('class', 'name')
.text(function (d) {
return d.name;
});
И текст обрабатывается в wrapText
:
// Adjust the font size to the zoom level and wrap the text in the container
var wrapText = function () {
d3.selectAll("text").each(function (d, i) {
var $text = d3.select(this);
if (!$text.attr("data-original-text")) {
// Save original text in custom attribute
$text.attr("data-original-text", $text.text());
}
var content = $text.attr("data-original-text");
var tokens = content.split(/(\s)/g);
var strCurrent = "";
var strToken = "";
var box;
var lineHeight;
var padding = 4;
$text.text("").attr("font-size", calcFontSize());
var $tspan = $text.append("tspan").attr("x", padding).attr("dy", 0);
while (tokens.length > 0) {
strToken = tokens.shift();
$tspan.text((strCurrent + strToken).trim());
box = $text.node().getBBox();
if (!lineHeight) {
lineHeight = box.height;
}
if (box.width > boxWidth - 2 * padding) {
$tspan.text(strCurrent.trim());
if (box.height + lineHeight < boxHeight) {
strCurrent = strToken;
$tspan = $text.append("tspan").attr("x", padding).attr("dy", lineHeight).text(strCurrent.trim());
} else {
break;
}
}
else {
strCurrent += strToken;
}
}
$text.attr("y", -(box.height - lineHeight) / 2);
});
}
Ответ 2
Я сделал образец вашего требования в этом fiddle
Возможно, потребуется изменить настройки текста по вертикали; но это может быть основой для вас. Вычисления выполняются в функции wrap()
и вызывают загрузку и масштабирование страницы.
function wrap() {
var texts = d3.selectAll("text"),
lineHeight = 1.1, // ems
padding = 2, // px
fSize = scale > 1 ? fontSize / scale : fontSize,
// find how many lines can be included
lines = Math.floor((boxHeight - (2 * padding)) / (lineHeight * fSize)) || 1;
texts.each(function(d, i) {
var text = d3.select(this),
words = d.name.split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
tspan = text.text(null).append("tspan").attr("dy", "-0.5em").style("font-size", fSize + "px");
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
// check if the added word can fit in the box
if ((tspan.node().getComputedTextLength() + (2 * padding)) > boxWidth) {
// remove current word from line
line.pop();
tspan.text(line.join(" "));
lineNumber++;
// check if a new line can be placed
if (lineNumber > lines) {
// left align text of last line
tspan.attr("x", (tspan.node().getComputedTextLength() - boxWidth) / 2 + padding);
--lineNumber;
break;
}
// create new line
tspan.text(line.join(" "));
line = [word]; // place the current word in new line
tspan = text.append("tspan")
.style("font-size", fSize + "px")
.attr("dy", "1em")
.text(word);
}
// left align text
tspan.attr("x", (tspan.node().getComputedTextLength() - boxWidth) / 2 + padding);
}
// align vertically inside the box
text.attr("text-anchor", "middle").attr("y", padding - (lineHeight * fSize * lineNumber) / 2);
});
}
Также обратите внимание, что я добавил стиль dominant-baseline: hanging;
в .person
class
Ответ 3
Текстовая упаковка может быть интенсивной, если у нас много текста. Чтобы решить эти проблемы, откройте мой первый ответ, эта новая версия улучшил производительность благодаря предварительным рендерингам.
Этот script создает элемент вне DOM
и сохраняет в нем все узлы и ребра. Затем он проверяет, какие элементы будут видны, удаляя их из DOM
и добавляя их, когда это необходимо.
Я использую jQuery
для data()
и для выбора элементов. В моем примере на скрипке есть 120 узлов. Но он должен работать аналогично гораздо больше, так как только отображаемые узлы отображаются на экране.
Я изменил режим масштабирования, так что зум сосредоточен на курсоре мыши и был удивлен, увидев, что панорамирование/масштабирование работает и на iOS.
Посмотрите в действии.
UPDATE
Я применил тайм-аут (решение ConnorsFan), поскольку он имеет большое значение. Кроме того, я добавил минимальную шкалу, для которой текст должен быть повторно обернут.
$(function() {
var viewport_width = $(window).width(),
viewport_height = $(window).height(),
node_width = 120,
node_height = 60,
separation_width = 100,
separation_height = 55,
node_separation = 0.78,
font_size = 20,
refresh_delay = 200,
refresh_timeout,
zoom_extent = [0.5, 1.15],
// Element outside DOM, to calculate pre-render
buffer = $("<div>");
// Parse "transform" attribute
function parse_transform(input_string) {
var transformations = {},
matches, seek;
for (matches in input_string = input_string.match(/(\w+)\(([^,)]+),?([^)]+)?\)/gi)) {
seek = input_string[matches].match(/[\w.\-]+/g), transformations[seek.shift()] = seek;
}
return transformations;
}
// Adapted from ConnorsFan answer
function get_font_size(scale) {
fs = ~~Math.min(font_size, 15 * Math.pow(scale, -0.25));
fs = ~~(((font_size / scale) + fs) / 2)
return [fs, fs]
}
// Use d3plus to wrap the text
function wrap_text(scale) {
if (scale > 0.75) {
$("svg > g > g").each(function(a, b) {
f = $(b);
$("text", f)
.text(f.data("text"));
});
d3.selectAll("text").each(function(a, b) {
d3_el = d3.select(this);
d3plus.textwrap()
.container(d3_el)
.align("center")
.valign("middle")
.width(node_width)
.height(node_height)
.valign("middle")
.resize(!0)
.size(get_font_size(scale))
.draw();
});
}
}
// Handle pre-render (remove elements that leave viewport, add them back when appropriate)
function pre_render() {
buffer.children("*")
.each(function(i, el) {
d3.transform(d3.select(el).attr("transform"));
var el_path = $(el)[0],
svg_wrapper = $("svg"),
t = parse_transform($("svg > g")[0].getAttribute("transform")),
element_data = $(el_path).data("coords"),
element_min_x = ~~element_data.min_x,
element_max_x = ~~element_data.max_x,
element_min_y = ~~element_data.min_y,
element_max_y = ~~element_data.max_y,
svg_wrapper_width = svg_wrapper.width(),
svg_wrapper_height = svg_wrapper.height(),
s = parseFloat(t.scale),
x = ~~t.translate[0],
y = ~~t.translate[1];
if (element_min_x * s + x <= svg_wrapper_width &&
element_min_y * s + y <= svg_wrapper_height &&
0 <= element_max_x * s + x &&
0 <= element_max_y * s + y) {
if (0 == $("#" + $(el).prop("id")).length) {
if (("n" == $(el).prop("id").charAt(0))) {
// insert nodes above edges
$(el).clone(1).appendTo($("svg > g"));
wrap_text(scale = t.scale);
} else {
// insert edges
$(el).clone(1).prependTo($("svg > g"));
}
}
} else {
id = $(el).prop("id");
$("#" + id).remove();
}
});
}
d3.scale.category20();
var link = d3.select("body")
.append("svg")
.attr("width", viewport_width)
.attr("height", viewport_height)
.attr("pointer-events", "all")
.append("svg:g")
.call(d3.behavior.zoom().scaleExtent(zoom_extent)),
layout_tree = d3.layout.tree()
.nodeSize([separation_height * 2, separation_width * 2])
.separation(function() {
return node_separation;
})
.children(function(a) {
return a._parents;
}),
nodes = layout_tree.nodes(json),
edges = layout_tree.links(nodes);
// Style links (edges)
link.selectAll("path.link")
.data(edges)
.enter()
.append("path")
.attr("class", "link")
.attr("d", function(a) {
return "M" + a.source.y + "," + a.source.x + "H" + ~~(a.source.y + (a.target.y - a.source.y) / 2) + "V" + a.target.x + "H" + a.target.y;
});
// Style nodes
var node = link.selectAll("g.person")
.data(nodes)
.enter()
.append("g")
.attr("transform", function(a) {
return "translate(" + a.y + "," + a.x + ")";
})
.attr("class", "person");
// Draw the rectangle person boxes
node.append("rect")
.attr({
x: -(node_width / 2),
y: -(node_height / 2),
width: node_width,
height: node_height
});
// Draw the person name and position it inside the box
node_text = node.append("text")
.attr("text-anchor", "start")
.text(function(a) {
return a.name;
});
// Text wrap on all nodes using d3plus. By default there is not any left or
// right padding. To add padding we would need to draw another rectangle,
// inside of the rectangle with the border, that represents the area we would
// like the text to be contained in.
d3.selectAll("text")
.each(function(a, b) {
d3plus.textwrap()
.container(d3.select(this))
.valign("middle")
.resize(!0)
.size(get_font_size(1))
.draw();
});
// START Create off-screen render
// Append node edges to memory, to allow pre-rendering
$("svg > g > path")
.each(function(a, b) {
el = $(b)[0];
if (d = $(el)
.attr("d")) {
// Parse d parameter from rect, in the format found in the d3 tree dom: M0,0H0V0V0
for (var g = d.match(/([MLQTCSAZVH])([^MLQTCSAZVH]*)/gi), c = g.length, h, k, f, l, m = [], e = [], n = 0; n < c; n++) {
command = g[n], void 0 !== command && ("M" == command.charAt(0) ? (coords = command.substring(1, command.length), m.push(~~coords.split(",")[0]), e.push(~~coords.split(",")[1])) : "V" == command.charAt(0) ? e.push(~~command.substring(1, command.length)) : "H" == command.charAt(0) && m.push(~~command.substring(1, command.length)));
}
0 < m.length && (h = Math.min.apply(this, m), f = Math.max.apply(this, m));
0 < e.length && (k = Math.min.apply(this, e), l = Math.max.apply(this, e));
$(el).data("position", a);
$(el).prop("id", "e" + a);
$(el).data("coords", {
min_x: h,
min_y: k,
max_x: f,
max_y: l
});
// Store element coords in memory
hidden_element = $(el).clone(1);
buffer.append(hidden_element);
}
});
// Append node elements to memory
$("svg > g > g").each(function(a, b) {
el = $("rect", b);
transform = b.getAttribute("transform");
null !== transform && void 0 !== transform ? (t = parse_transform(transform), tx = ~~t.translate[0], ty = ~~t.translate[1]) : ty = tx = 0;
// Calculate element area
el_min_x = ~~el.attr("x");
el_min_y = ~~el.attr("y");
el_max_x = ~~el.attr("x") + ~~el.attr("width");
el_max_y = ~~el.attr("y") + ~~el.attr("height");
$(b).data("position", a);
$(b).prop("id", "n" + a);
$(b).data("coords", {
min_x: el_min_x + tx,
min_y: el_min_y + ty,
max_x: el_max_x + tx,
max_y: el_max_y + ty
});
text_el = $("text", $(b));
0 < text_el.length && $(b).data("text", d3.select(text_el[0])[0][0].__data__.name);
// Store element coords in memory
hidden_element = $(b).clone(1);
// store node in memory
buffer.append(hidden_element);
});
// END Create off-screen render
d3_svg = d3.select("svg");
svg_group = d3.select("svg > g");
// Setup zoom and pan
zoom = d3.behavior.zoom()
.on("zoom", function() {
previous_transform = $("svg > g")[0].getAttribute("transform");
svg_group.style("stroke-width", 1.5 / d3.event.scale + "px");
svg_group.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
pre_render();
if (previous_transform !== null) {
previous_transform = parse_transform(previous_transform);
if (previous_transform.scale != d3.event.scale) {
// ConnorsFan solution
if (refresh_timeout) {
clearTimeout(refresh_timeout);
}
scale = d3.event.scale;
refresh_timeout = setTimeout(function() {
wrap_text(scale = scale);
}, refresh_delay, scale);
}
}
});
// Apply initial zoom / pan
d3_svg.call(zoom);
});