A Pen by François Romain on CodePen.
Created
April 23, 2019 13:39
-
-
Save jimcrozier/01251e6f86acf34327281e047b4224f6 to your computer and use it in GitHub Desktop.
Smooth svg line chart with vue.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div id="app"> | |
| <pre> | |
| Smoothing <input type="range" min="0" max="0.25" step="0.01" v-model="options.line.smoothing"> <span v-html="options.line.smoothing"></span><br> | |
| Flattening <input type="range" min="0" max="1" step="0.01" v-model="options.line.flattening"> <span v-html="options.line.flattening"></span></pre> | |
| <div class="container" ref="container"> | |
| <svg-chart :datasets="datasets" :options="options" :svg="svg"></svg-chart> | |
| </div> | |
| </div> | |
| <div> | |
| <pre>Related article on Medium: <a href="https://medium.com/@francoisromain/smooth-a-svg-path-with-cubic-bezier-curves-e37b49d46c74" target="_blank">Smooth a Svg path with cubic bezier curves</a>.</pre> | |
| </div> | |
| <script type="text/x-template" id="svg-chart"> | |
| <svg :view-box.camel="viewbox"> | |
| <svg-chart-line :d="dataset" :o="options" :svg="svg" v-for="dataset in datasets"></svg-chart-line> | |
| <svg-chart-axis :o="options" :svg="svg"></svg-chart-axis> | |
| </svg> | |
| </script> | |
| <script type="text/x-template" id="svg-chart-line"> | |
| <g> | |
| <path :style="styles.path" :d="pathD"></path> | |
| <circle :cx="p[0]" :cy="p[1]" r="2.5" :style="styles.circles" v-for="p in pointsPositions" /> | |
| </g> | |
| </script> | |
| <script type="text/x-template" id="svg-chart-axis"> | |
| <g> | |
| <line x1="0" :y1="y" :x2="svg.w" :y2="y" stroke-width="1" stroke="silver"/> | |
| <line :x1="x" y1="0" :x2="x" :y2="svg.h" stroke-width="1" stroke="silver"/> | |
| <line :x1="tickX" :y1="y" :x2="tickX" :y2="y + 5" stroke-width="1" stroke="silver" v-for="tickX in tickXs"/> | |
| <line :x1="x - 5" :y1="tickY" :x2="x" :y2="tickY" stroke-width="1" stroke="silver" v-for="tickY in tickYs"/> | |
| </g> | |
| </script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const options = { | |
| xMin: -53, | |
| xMax: 198, | |
| yMin: -32, | |
| yMax: 128, | |
| line: { | |
| smoothing: 0.15, | |
| flattening: 0.5 | |
| } | |
| }; | |
| const datasets = [ | |
| { | |
| name: "one", | |
| colors: { | |
| path: "#B4DC7F", | |
| circles: "red" | |
| }, | |
| values: [ | |
| [-20, 10], | |
| [0, -15], | |
| [5, 0], | |
| [10, 60], | |
| [20, 10], | |
| [30, 60], | |
| [40, 80], | |
| [50, 60], | |
| [70, 10], | |
| [80, 50], | |
| [90, 50], | |
| [120, 10], | |
| [150, 80], | |
| [160, 10] | |
| ] | |
| }, | |
| { | |
| name: "two", | |
| colors: { | |
| path: "rgba(55, 165, 230, 1.0)", | |
| circles: "orange" | |
| }, | |
| values: [ | |
| [0, 10], | |
| [5, 60], | |
| [10, 20], | |
| [20, 150], | |
| [30, 40], | |
| [40, 10], | |
| [50, 30], | |
| [60, 20], | |
| [70, 110], | |
| [80, 90], | |
| [90, 120], | |
| [120, 50], | |
| [160, 50], | |
| [200, 120] | |
| ] | |
| }, | |
| { | |
| name: "three", | |
| colors: { | |
| path: "#FF9F1C", | |
| circles: "orange" | |
| }, | |
| values: [ | |
| [-50, 5], | |
| [-20, -5], | |
| [0, 0], | |
| [10, 10], | |
| [20, 40], | |
| [30, -10], | |
| [40, -10], | |
| [50, 20], | |
| [60, 10], | |
| [70, 40], | |
| [80, -15], | |
| [100, -10], | |
| [110, 30], | |
| [140, -10], | |
| [180, -10] | |
| ] | |
| } | |
| ]; | |
| const lib = { | |
| map(value, inMin, inMax, outMin, outMax) { | |
| return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin | |
| }, | |
| range(start, end, tick) { | |
| const s = Math.round(start / tick) * tick | |
| return Array.from({ | |
| length: Math.floor((end - start) / tick) | |
| }, (v, k) => { | |
| return k * tick + s | |
| }); | |
| } | |
| }; | |
| const svgChartAxis = { | |
| template: "#svg-chart-axis", | |
| props: ["o", "svg"], | |
| computed: { | |
| y() { | |
| return lib.map(0, this.o.yMin, this.o.yMax, this.svg.h, 0); | |
| }, | |
| x() { | |
| return lib.map(0, this.o.xMin, this.o.xMax, 0, this.svg.w); | |
| }, | |
| tickXs() { | |
| const ticks = lib.range(this.o.xMin, this.o.xMax, 10) | |
| return ticks.map(tick => | |
| lib.map(tick, this.o.xMin, this.o.xMax, 0, this.svg.w) | |
| ); | |
| }, | |
| tickYs() { | |
| const ticks = lib.range(this.o.yMin, this.o.yMax, 10); | |
| return ticks.map(tick => | |
| lib.map(tick, this.o.yMin, this.o.yMax, this.svg.h, 0) | |
| ); | |
| } | |
| } | |
| }; | |
| const svgChartLine = { | |
| template: "#svg-chart-line", | |
| props: ["d", "o", "svg"], | |
| computed: { | |
| styles() { | |
| return { | |
| path: { | |
| fill: this.d.colors.path, | |
| stroke: this.d.colors.path, | |
| strokeWidth: 1.5, | |
| fillOpacity: 0.15, | |
| strokeOpacity: 0.8 | |
| }, | |
| circles: { | |
| fill: this.d.colors.circles | |
| } | |
| }; | |
| }, | |
| pathD() { | |
| return this.pointsPositions.reduce((acc, e, i, a) => i === 0 | |
| ? `M ${a[a.length - 1][0]},${this.svg.h} | |
| L ${e[0]},${this.svg.h} L ${e[0]},${e[1]}` | |
| : `${acc} ${this.bezierCommand(e, i, a)}` | |
| , ""); | |
| }, | |
| pointsPositions() { | |
| return this.d.values.map(e => { | |
| const x = lib.map( | |
| e[0], | |
| this.o.xMin, | |
| this.o.xMax, | |
| 0, | |
| this.svg.w | |
| ); | |
| const y = lib.map( | |
| e[1], | |
| this.o.yMin, | |
| this.o.yMax, | |
| this.svg.h, | |
| 0 | |
| ); | |
| return [x, y]; | |
| }); | |
| } | |
| }, | |
| methods: { | |
| line(pointA, pointB) { | |
| const lengthX = pointB[0] - pointA[0]; | |
| const lengthY = pointB[1] - pointA[1]; | |
| return { | |
| length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)), | |
| angle: Math.atan2(lengthY, lengthX) | |
| }; | |
| }, | |
| controlPoint(current, previous, next, reverse) { | |
| const p = previous || current; | |
| const n = next || current; | |
| const o = this.line(p, n); | |
| // work in progress… | |
| const flat = lib.map(Math.cos(o.angle) * this.o.line.flattening, 0, 1, 1, 0) | |
| const angle = o.angle * flat + (reverse ? Math.PI : 0); | |
| const length = o.length * this.o.line.smoothing; | |
| const x = current[0] + Math.cos(angle) * length; | |
| const y = current[1] + Math.sin(angle) * length; | |
| return [x, y]; | |
| }, | |
| bezierCommand(point, i, a) { | |
| const cps = this.controlPoint(a[i - 1], a[i - 2], point); | |
| const cpe = this.controlPoint(point, a[i - 1], a[i + 1], true); | |
| const close = i === a.length - 1 ? " z" : ""; | |
| return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}${close}`; | |
| } | |
| } | |
| }; | |
| const svgChart = { | |
| template: "#svg-chart", | |
| components: { | |
| svgChartLine, | |
| svgChartAxis | |
| }, | |
| props: ["datasets", "options", "svg"], | |
| computed: { | |
| viewbox() { | |
| return `0 0 ${this.svg.w} ${this.svg.h}`; | |
| } | |
| } | |
| }; | |
| const app = new Vue({ | |
| el: "#app", | |
| components: { | |
| svgChart | |
| }, | |
| data: { | |
| options, | |
| datasets, | |
| svg: { | |
| w: 0, | |
| h: 0 | |
| } | |
| }, | |
| mounted() { | |
| window.addEventListener("resize", this.resize); | |
| this.resize(); | |
| }, | |
| methods: { | |
| resize() { | |
| this.svg.w = this.$refs.container.offsetWidth; | |
| this.svg.h = this.$refs.container.offsetHeight; | |
| } | |
| } | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| *, *:before, *:after { | |
| box-sizing: inherit; | |
| } | |
| .container { | |
| margin: 40px; | |
| height: 400px; | |
| background-color: #F7F5F2; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment