如何在放大时显示全文并在缩小时截断它

Posted

技术标签:

【中文标题】如何在放大时显示全文并在缩小时截断它【英文标题】:How to show full text when zoom in & truncate it when zoom out 【发布时间】:2017-04-17 08:22:12 【问题描述】:

我正在使用 d3.js 创建一个树形图,它工作正常...但我希望文本对缩放做出反应,这里是 JSFiddle。

请查看第一个节点...它有很多字符(在我的情况下最多为 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's 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>

【问题讨论】:

还是不清楚你想要达到什么目的:( @pritishvaidya 到底是什么让你感到困惑 :) 您希望文本在放大时不被截断?因为使用 d3 缩放时明显的字体大小会发生变化,您是否希望文本出现在框外? 放大和缩小时要做什么,请详细说明 @softwarenewbie7331 我希望文本在放大时慢慢开始越来越少地截断,但是当所有文本都以矩形显示时,它可能会增加大小,如果这有意义的话? 【参考方案1】:

我在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

【讨论】:

谢谢你的回答......它看起来像我需要的,但它使它非常有问题并且很难使用它:( 您注意到使用 wrap 函数的主要错误是什么?也许我可以帮助解决这些问题。 我面临的一个主要问题是,与以前相比,现在屏幕响应缩放的时间太长了。原因很明显,因为它很慢,但我担心它是否能满足我的要求:( @Mathematics 我已更新代码以提高性能并修复最后一行拟合问题。如果您的应用程序需要更高的性能,我强烈建议使用画布而不是 SVG 来呈现树。请参阅this example performance comparison 使用画布和 SVG 绘制相同的数据集。 Canvas 比 SVG 快 400-600%! d3 有一些插件可以使用 canvas 而不是 SVG,而且 d3 v4 似乎原生支持 canvas。这里有一些链接。 Learnings from a D3.js addict on starting with Canvas、Using Canvas and D3、d3js and canvas、D3 Without SVG 和 SO Questions。您可以谷歌了解更多信息。【参考方案2】:

this 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);
    )

延迟(以毫秒为单位)取决于树中的节点数。您可以尝试使用数学表达式来为您期望在树中的各种节点计数找到最佳参数。

// 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);
    );

【讨论】:

我修改了代码以提高性能并减少刷新延迟。【参考方案3】:

如果我们有很多文本,文本换行可能会占用大量流程。为了解决这些问题,在my first answer 中出现,这个new version 改进了性能,这要归功于预渲染。

此脚本在DOM 之外创建一个元素,并将所有节点和边存储到其中。然后它检查哪些元素是可见的,从DOM 中删除它们,并在适当的时候将它们添加回来。

我将jQuery 用于data(),并用于选择元素。在我的小提琴示例中,有 120 个节点。但它的工作原理应该类似,因为渲染的唯一节点是屏幕上的节点。

我更改了缩放行为,使缩放以鼠标光标为中心,并且惊讶地发现平移/缩放也适用于 ios

See it in action.

更新

我应用了超时(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's 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's 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's 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);
);

【讨论】:

感谢您的努力,但使用起来看起来太慢了 :*(, +1 无论如何......但如果你能提高性能,我将在未来增加 50 - 10,000 个节点...... . @Mathematics 我明白了!有什么办法可以让我掌握一些具有数千个节点的样本数据?对我来说是一个很好的方法,看看我是否能解决这个问题。你也许可以把它放在 pastebin.com 上?谢谢! @Mathematics 不用担心提供数据!我设法创建了一个示例数据集并查看了问题!如果我想出提高性能的方法,我会告诉你的。

以上是关于如何在放大时显示全文并在缩小时截断它的主要内容,如果未能解决你的问题,请参考以下文章

Django admin list_display 在鼠标悬停时显示全文

mapkit 缩小然后放大

如何仅在悬停时显示截断的文本而不更改框列表的高度?

如何在 QTabWidget 中隐藏选项卡并在按下按钮时显示它

当用户放大 UIScrollView 时如何隐藏某些内容,然后在缩小时显示它?

如何保存选定的 TableView 行数据并在用户返回屏幕时显示?