Skip to content

Instantly share code, notes, and snippets.

@jimcrozier
Created April 23, 2019 13:39
Show Gist options
  • Select an option

  • Save jimcrozier/01251e6f86acf34327281e047b4224f6 to your computer and use it in GitHub Desktop.

Select an option

Save jimcrozier/01251e6f86acf34327281e047b4224f6 to your computer and use it in GitHub Desktop.
Smooth svg line chart with vue.js
<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>
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;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
*, *: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