阅读 619

CSS 实现自适应折线图

先看最终的效果:

1.gif

或者在线体验:jsbin.com/resedebati/…

一、背景:

最近的一个项目需要展示折线图,因为项目比较简单,没有引用相关的图表库,而是自己原生实现的。

二、方法一( javascript ):

第一反应就是用 javascript 实现。首先,用 css 画出折线图起止点以及转折点处的圆圈并实现自适应。如下图所示:

2.gif

接下来就是利用 javascript 获取这些圆圈的 DOM 节点,计算出相邻两点之间的长度以及角度,并设置相关的样式。因为屏幕宽度变化时,两点之间的长度和角度也会随之变化,所以需要监听 resize 事件并重新设置相关样式。

好了,一个简单的折线图就实现好了,可是接下来发现了这样实现的问题。

方法一问题:

这个项目是需要打印功能的,打印预览的时候,发现了 javascript 实现的问题,如下图:

1.png 因为浏览器执行打印的瞬间,会以 A4 纸的宽度抓取页面的 DOM 结构,由于顶点的圆圈是用 css 实现的,可以根据 A4 纸的宽度很好地实现自适应布局,但是折线不行。折线是用 javascript 实现的,所以打印时展示出来的还是打印瞬间浏览器宽度下渲染出来的折线。若浏览器比 A4 纸宽,就是上图的效果,若浏览器没有 A4 纸宽,就是下图的效果。除非浏览器宽度正好就是 A4 纸宽度,打印出来的效果就是正好,可是这基本不可能。 2.png 起初,我的思路是:在执行打印的瞬间也执行一次渲染折线的方法应该就可以了。可是捣鼓了很久,无果。。。后来请教张鑫旭张老师,张老师建议直接用 css 实现折线图。起初我是有点懵的,css 也可以实现折线图吗?

三、方法二( css:svg 的 path ):

先画出不带标记的折线图:

3.gif

<svg width="1470" height="200" viewBox="0 0 1470 200" preserveAspectRatio="none">
   <path d="M0 0L210 200L420 50L630 160L840 150L1060 180L1270 40L1470 120" fill="none" stroke="red"
              stroke-width="1" style="vector-effect: non-scaling-stroke;">
</svg>
复制代码

由于折线图的高度是固定的,所以需要在 svg 标签上添加 preserveAspectRatio="none" 属性来扭曲纵横比以充分适应 viewport,关于该属性与 viewport、viewBox 的关系,可以参考张老师的文章

接下来可以在 svg 内用 <marker> 定义一个圆形标记

<defs>
    <marker id="markerCircle" markerWidth="8" markerHeight="8" refX="4" refY="4">
        <circle cx="4" cy="4" r="2.5" fill="#fff" stroke="red" stroke-width="1">
    </marker>
</defs>
复制代码

然后在上面的 <path> 元素内用 marker-mid、marker-start、marker-end 通过 id 引用该标记,就可以实现起止点和转折点的圆圈。

<svg width="1470" height="200" viewBox="0 0 1470 200" preserveAspectRatio="none">
    <defs>
        <marker id="markerCircle" markerWidth="8" markerHeight="8" refX="4" refY="4">
            <circle cx="4" cy="4" r="2.5" fill="#fff" stroke="red" stroke-width="1"></circle>
        </marker>
    </defs>
    <path d="M0 0L210 200L420 50L630 160L840 150L1060 180L1270 40L1470 120" fill="none" stroke="red"
          stroke-width="1" marker-mid="url('#markerCircle')"  marker-end="url('#markerCircle')" marker-start="url('#markerCircle')" style="vector-effect: non-scaling-stroke;"></path>
</svg>
复制代码

其中,需要使用 vector-effect: non-scaling-stroke 使描边不会缩放保证折线的宽度不变。至此,使用纯 css 实现了自适应的折线图,再次感叹 css 的强大。但是这个方案还是有一个小瑕疵。

方法二问题:

由于整个 svg 使用了 <preserveAspectRatio="none"> 属性来扭曲纵横比以充分适应 viewport,而 <marker> 标记的渲染是跟 viewBox 的纵横比相关的,不同纵横比下的圆圈表现不同。

3.png

4.png

至此感觉走进了死胡同,javascript 实现打印时有问题,css 实现圆圈变形。。。好在这时另一位同事严文彬提供了一种完美的解决方案。

四、方法三( css:svg ):

既然方法二会由于强行改变 viewBox 的纵横比导致 <marker> 标记变形,干脆就不改变纵横比,同时将折线一段一段地通过比例渲染出来不就可以了?直接上代码:

<div class="line">
    <svg>
        <defs>
            <marker id="dot" markerWidth="8" markerHeight="8" refX="4" refY="4">
                <circle cx="4" cy="4" r="2.5"></circle>
            </marker>
        </defs>
        <line x1="0" y1="0%" x2="14.2857%" y2="100%"></line>
        <line x1="14.2857%" y1="100%" x2="28.5714%" y2="25%"></line>
        <line x1="28.5714%" y1="25%" x2="42.8571%" y2="80%"></line>
        <line x1="42.8571%" y1="80%" x2="57.1428%" y2="75%"></line>
        <line x1="57.1428%" y1="75%" x2="71.4286%" y2="90%"></line>
        <line x1="71.4286%" y1="90%" x2="85.7143%" y2="20%"></line>
        <line x1="85.7143%" y1="20%" x2="100%" y2="30%"></line>
    </svg>
</div>
复制代码
.line {
   width: calc(100% - 102px);
   height: 200px;
   position: absolute;
   left: 51px;
}
.line svg {
  width: 100%;height: 100%;
  color: red;
  stroke: currentColor;
  stroke-width: 1;
  overflow: visible;
  transform: scaleY(-1);
}
circle {
  stroke: currentColor;
  stroke-width: 1;
  fill: #fff;
}
.line line {
  marker-start: url(#dot)
}
.line line:last-child {
  marker-end: url(#dot)
}
复制代码

其中,x1 y1 是一条线的第一个顶点;x2 y2 是一条线的第二个顶点;上一条线的第二个顶点坐标与下一条线的第一个顶点坐标一样。最终的效果图就是文章开头的动图,自适应良好,圆圈未变形,打印效果也很好。

五、总结:

一波三折的实现过程中,学习了很多 svg 相关的知识点,也认识到自己 css 知识点方面的不足。一个效果的实现,第一反应用 javascript 解决的思维方式真的应该好好改改了。

非常感谢张老师以及文彬两位大佬的帮助,实现过程中大家都参考了张老师的第三本书《CSS新世界》中的很多知识点,该书非常全面地讲解了 CSS2.1 之后的知识,体验之后感觉非常赞!

六、注意点:

  1. svg 元素也可以直接使用 overflow: visible 来显示超出 viewBox 范围的内容。

  2. svg 内的百分比是以左上角为参考点的,可以使用 transform: scaleY(-1) 使整个 svg 以 Y 轴中心线翻转从而变成以左下角为参考点,符合习惯。

  3. <marker> 内的标记也可以自定义为三角形、正方形,可以根据喜好自由发挥。

感谢大家的阅读,若发现有不正确之处,欢迎指正。

参考文章

  1. 理解 SVG viewport, viewBox, preserveAspectRatio 缩放
  2. 《CSS新世界》
文章分类
前端
文章标签