Vue样本图——如何使用Vue框架创建图表组件





5.00/5 (5投票s)
创建演示Vue框架样本图的组件。

目录
引言
学习新框架的一个好方法是使用它来开发一些东西。对我来说,我有一个使用 AngularJS 编写的应用程序,我想将其迁移到一个更现代的框架。经过一些研究,我发现 Vue 是最适合我需求的解决方案,并想尝试一下。为了更有动力,我决定开发一些有用的东西(而不是一个会被扔掉的学习项目)。
该迁移应用程序的一个能力是测量一些样本(关于系统中发生的事情)并更新一些性能计数器(可以使用 Windows 性能监视器查看)。我当时想,如果我们也能在应用程序的 UI 中看到这些样本的图表,那将是很好的。所以,我有一个想法,开发一个组件来展示样本图表。由于当我开始开发这个组件时,最新的 Vue 版本是 2.6.10,所以它使用的是这个版本。
背景
在本文中,我们将使用 Vue 框架创建一个用于展示样本图表的前端组件。本文假设您对 HTML、CSS 和 JavaScript 有基本了解。由于我们处理的是前端解决方案,因此建议对现代前端开发(尤其是在使用 Vue 时)有一定的基本了解。无论如何,我尝试为第一次提到的任何概念提供指向详细解释的链接。
工作原理
绘制图表
Canvas vs svg
我们图表的主要部分是图表的绘制。HTML5 为我们提供了两种显示图形的方法:Canvas 和 SVG。虽然 SVG
图形更具矢量性且响应性更好,但 Canvas
图形具有更好的性能。
在我们的图表实现中,我们同时使用了这两种方法。我们使用 SVG
作为默认方法,并提供更改为 Canvas
方法的选项(用于数据量大的重绘情况)。
为此,我们创建了 2 个内部 组件(每个方法一个)
const canvasDrawingComponent = {
props: ['values', 'range', 'min', 'max', 'settings', 'highlighted_index'],
template: `<canvas class="sz-graph-canvas"></canvas>`
};
const svgDrawingComponent = {
props: ['values', 'range', 'min', 'max', 'settings', 'highlighted_index'],
template: `<div class="sz-graph-svg"><svg></svg></div>`
};
以及一个用于根据所选方法呈现绘图的组件
const drawingComponent = {
props: ['values', 'range', 'range_offset', 'min', 'max', 'settings', 'highlighted_index'],
components: {
'sz-graph-canvas': canvasDrawingComponent,
'sz-graph-svg': svgDrawingComponent
},
template:
`<div class="sz-graph-drawing">
<sz-graph-canvas v-if="settings.drawing.drawUsingCanvas"
:values="values" :range="range" :min="min" :max="max" :settings="settings"
:highlighted_index="highlighted_index"></sz-graph-canvas>
<sz-graph-svg v-else
:values="values" :range="range" :min="min" :max="max" :settings="settings"
:highlighted_index="highlighted_index"></sz-graph-svg>
</div>`
};
Canvas 绘图
在我们的图表绘制中,每个值集都有一个图表以及网格线(用于指示刻度值)。值图表可以包含:连接值的线、线与图表底部之间的填充区域,以及用于强调实际值的圆圈。
要使用 canvas
元素绘制我们的图表,我们将一个 drawGraph
函数添加到我们的 canvasDrawingComponent
组件中
methods: {
drawGraph() {
// Get the root ('canvas') element.
const c = this.$el;
// Set the canvas drawing area dimensions to be equal
// to the canvas element dimensions.
c.width = c.offsetWidth;
c.height = c.offsetHeight;
// Get the canvas drawing context and clear it.
const w = c.width, h = c.height;
const ctx = c.getContext("2d");
ctx.clearRect(0, 0, w, h);
ctx.setLineDash([]);
// Draw grid lines.
if (this.settings.drawing.showGridLines) {
this.drawGridLines(ctx, w, h);
}
// Draw graph lines.
for (let valuesInx = 0; valuesInx < this.values.length; valuesInx++) {
if (this.settings.drawing.graph.visibility[valuesInx]) {
this.drawGraphLine(ctx, w, h, this.values[valuesInx], valuesInx);
}
}
},
drawGridLines(ctx, w, h) {
const scaleSettings = this.settings.scale;
ctx.lineWidth = 1;
ctx.strokeStyle = this.settings.drawing.gridLinesColor;
ctx.beginPath();
const gridGap = h / 16;
// Draw horizontal grid lines according to the scale settings.
for (let i = 1; i < 16; i++) {
if (h > scaleSettings.minimalValuesGap * 16 ||
(i % 2 == 0 && h > scaleSettings.minimalValuesGap * 8) ||
(i % 4 == 0 && h > scaleSettings.minimalValuesGap * 4) ||
(i % 8 == 0 && h > scaleSettings.minimalValuesGap * 2)) {
ctx.moveTo(0, i * gridGap);
ctx.lineTo(w, i * gridGap);
}
}
ctx.stroke();
ctx.closePath();
},
drawGraphLine(ctx, w, h, vals, valuesInx) {
const graphSettings = this.settings.drawing.graph;
const valsCount = vals.length;
if (valsCount > 1) {
const minVal = this.min, maxVal = this.max;
const valsRange = maxVal - minVal;
const widthUnit = w / this.range, heightUnit = h / valsRange;
ctx.lineWidth = valuesInx == this.highlighted_index ? 3 : 1;
ctx.strokeStyle = graphSettings.colors[valuesInx];
// Draw graph fill.
if (graphSettings.showFill) {
ctx.fillStyle = graphSettings.fillColors[valuesInx];
ctx.beginPath();
ctx.moveTo(0, h - (vals[0] - minVal) * heightUnit);
for (let i = 1; i < valsCount; i++) {
ctx.lineTo(i * widthUnit, h - (vals[i] - minVal) * heightUnit);
}
ctx.lineTo((valsCount - 1) * widthUnit, h);
ctx.lineTo(0, h);
ctx.lineTo(0, h - (vals[0] - minVal) * heightUnit);
ctx.fill();
ctx.closePath();
}
// Draw graph line.
if (graphSettings.showLines) {
ctx.beginPath();
ctx.moveTo(0, h - (vals[0] - minVal) * heightUnit);
for (let i = 1; i < valsCount; i++) {
ctx.lineTo(i * widthUnit, h - (vals[i] - minVal) * heightUnit);
}
ctx.stroke();
ctx.closePath();
}
// Draw graph circles.
if (graphSettings.showCircles) {
ctx.fillStyle = graphSettings.colors[valuesInx];
ctx.beginPath();
ctx.arc(0, h - (vals[0] - minVal) * heightUnit, 3, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.closePath();
for (let i = 1; i < valsCount; i++) {
ctx.beginPath();
ctx.arc(i * widthUnit,
h - (vals[i] - minVal) * heightUnit, 3, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.closePath();
}
}
}
}
}
mounted() {
this.$watch('values',
() => {
this.drawGraph();
});
this.drawGraph();
}
在 drawGraph
函数中,我们获取 canvas
HTML 元素并通过其上下文应用绘图。由于 每个组件必须有一个单一的根元素,我们可以通过使用 canvas
作为根模板元素并通过 $el 属性获取它来实现这一点。
在 drawGridLines
函数中,我们根据图表的刻度绘制图表的网格线(将在本文 后面 介绍)。
在 drawGraphLine
函数中,我们根据图表的显示选项绘制给定值集的图表。这些选项可以使用图表的设置进行控制,具体将在本文 后面 介绍。
Svg 绘图
使用 Canvas
时,我们通过(使用 JavaScript 代码)访问实际的 HTML 元素并使用其上下文的函数来绘制图表。使用 SVG
时,由于 SVG
图形是 HTML 元素,我们可以通过添加一些 计算属性 并将它们 绑定 到模板元素来绘制图表。
data() {
return {
height: 0
};
},
template:
`<div class="sz-graph-svg"><svg>
<g v-if="settings.drawing.showGridLines"
:stroke="settings.drawing.gridLinesColor" stroke-width="1" >
<line v-for="lp in gridLines" x1="0" :y1="lp.y" x2="100%" :y2="lp.y" />
</g>
<g v-for="(n, valInx) in values.length" v-if="settings.drawing.graph.visibility[valInx]"
:stroke="settings.drawing.graph.colors[valInx]"
:stroke-width="valInx == highlighted_index ? 3 : 1" stroke-linecap="round" >
<svg v-if="settings.drawing.graph.showFill"
viewBox="0 0 100 100" preserveAspectRatio="none"
style="width: 100%; height:100%">
<path :d="fillPathes[valInx]"
:fill="settings.drawing.graph.fillColors[valInx]" stroke-width="0" />
</svg>
<line v-if="settings.drawing.graph.showLines" v-for="lp in graphLines[valInx]"
:x1="lp.x1" :y1="lp.y1" :x2="lp.x2" :y2="lp.y2" />
<circle v-if="settings.drawing.graph.showCircles" cx="0"
:cy="firstGraphValues[valInx]"
r=3 :fill="settings.drawing.graph.colors[valInx]" />
<circle v-if="settings.drawing.graph.showCircles"
v-for="lp in graphLines[valInx]"
:cx="lp.x2" :cy="lp.y2" r=3 :fill="settings.drawing.graph.colors[valInx]" />
</g>
</svg></div>`,
mounted() {
this.height = this.$el.offsetHeight;
},
computed: {
gridLines() {
let res = [];
const h = this.height;
const scaleSettings = this.settings.scale;
const gridGap = 100 / 16;
// Get horizontal grid lines coordinates according to the scale settings.
for (let i = 1; i < 16; i++) {
if (h > scaleSettings.minimalValuesGap * 16 ||
(i % 2 == 0 && h > scaleSettings.minimalValuesGap * 8) ||
(i % 4 == 0 && h > scaleSettings.minimalValuesGap * 4) ||
(i % 8 == 0 && h > scaleSettings.minimalValuesGap * 2)) {
res.push({
y: `${i * gridGap}%`
});
}
}
return res;
},
fillPathes() {
let res = [];
if (this.range > 1) {
const minVal = this.min, maxVal = this.max;
const valsRange = maxVal - minVal;
const widthUnit = 100 / this.range, heightUnit = 100 / valsRange;
// Get the path's data for the graphs' fill.
res = this.values.map(vals => {
let pathStr = 'M';
for (let valInx = 0; valInx < vals.length; valInx++) {
pathStr = `${pathStr}${valInx * widthUnit},
${100 - (vals[valInx] - minVal) * heightUnit} `;
}
pathStr = `${pathStr}${(vals.length - 1) * widthUnit},100 0,100z`;
return pathStr;
});
}
return res;
},
graphLines() {
let res = [];
if (this.range > 1) {
const minVal = this.min, maxVal = this.max;
const valsRange = maxVal - minVal;
const widthUnit = 100 / this.range, heightUnit = 100 / valsRange;
res = this.values.map(vals => {
const linesPoints = [];
// Get graph line coordinates.
for (let valInx = 1; valInx < vals.length; valInx++) {
linesPoints.push({
x1: `${(valInx - 1) * widthUnit}%`,
y1: `${100 - (vals[valInx - 1] - minVal) * heightUnit}%`,
x2: `${valInx * widthUnit}%`,
y2: `${100 - (vals[valInx] - minVal) * heightUnit}%`
});
}
return linesPoints;
});
}
return res;
},
firstGraphValues() {
const minVal = this.min, maxVal = this.max;
const valsRange = maxVal - minVal;
const heightUnit = 100 / valsRange;
return this.values.map(vals => vals.length > 0 ?
`${100 - (vals[0] - minVal) * heightUnit}%` : '0');
}
}
这样,我们不必在值更改时监视并为每次更改调用一个重绘整个图表的函数(就像我们使用 canvas
元素时那样),我们只需为所需数据定义计算属性,然后让 Vue 完成必要的重渲染工作。
图表刻度
除了图表绘制之外,我们可能还想显示所呈现图表值的刻度。为此,我们创建了另一个内部组件
const scaleComponent = {
props: ['min', 'max', 'settings']
};
在该组件中,我们添加了一个计算属性来显示刻度值
data() {
return { elHeight: 0 };
},
mounted() {
this.elHeight = this.$el.offsetHeight;
},
computed: {
values() {
const scaleSettings = this.settings.scale;
const max = parseFloat(this.max);
const min = parseFloat(this.min);
const secondPart = (max - min) / 2;
const thirdPart = secondPart / 2;
const fourthPart = thirdPart / 2;
const fifthPart = fourthPart / 2;
const elHeight = this.elHeight && this.elHeight > 0 ? this.elHeight : 200;
return {
first: [min, max],
second: [secondPart + min],
third: [thirdPart + min, max - thirdPart],
fourth: [fourthPart + min, fourthPart * 3 + min,
fourthPart * 5 + min, max - fourthPart],
fifth: [fifthPart + min, fifthPart * 3 + min,
fifthPart * 5 + min, fifthPart * 7 + min,
fifthPart * 9 + min, fifthPart * 11 + min,
fifthPart * 13 + min, max - fifthPart],
showSecond: elHeight > scaleSettings.minimalValuesGap * 2,
showThird: elHeight > scaleSettings.minimalValuesGap * 4,
showFourth: elHeight > scaleSettings.minimalValuesGap * 8,
showFifth: elHeight > scaleSettings.minimalValuesGap * 16
};
}
}
以及一个用于显示这些值的模板
template:
`<div class="sz-graph-scale">
<div class="sz-fifth" v-if="values.showFifth">
<div class="sz-scale-val" v-for="v in values.fifth">{{ v }}</div>
</div>
<div class="sz-fourth" v-if="values.showFourth">
<div class="sz-scale-val" v-for="v in values.fourth">{{ v }}</div>
</div>
<div class="sz-third" v-if="values.showThird">
<div class="sz-scale-val" v-for="v in values.third">{{ v }}</div>
</div>
<div class="sz-second" v-if="values.showSecond">
<div class="sz-scale-val" v-for="v in values.second">{{ v }}</div>
</div>
<div class="sz-first">
<div class="sz-scale-val" v-for="v in values.first">{{ v }}</div>
</div>
</div>`
为了在刻度上正确地显示刻度值的位置,我们为刻度元素使用了一个单格 网格布局(所有元素都在同一个单元格中),并为每个值集使用 flexbox 布局。为了将刻度从图表的顶部延伸到底部,我们为主刻度值使用了 space-between
对齐方式,并为其余值使用了 space-around
对齐方式。
.sz-graph-scale {
grid-area: scale;
position: relative;
margin: 0;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 100%;
grid-template-areas: "all";
padding-right: 12px;
border-right: solid 0.1em #333;
}
.sz-graph-scale .sz-first, .sz-graph-scale .sz-second,
.sz-graph-scale .sz-third, .sz-graph-scale .sz-fourth,
.sz-graph-scale .sz-fifth {
grid-area: all;
display: flex;
position: relative;
flex-direction: column-reverse;
justify-content: space-around;
}
.sz-graph-scale .sz-first {
justify-content: space-between;
margin-top: -0.5em;
margin-bottom: -0.5em;
}
为了显示刻度的小刻度线(指示刻度中值的 [y] 轴位置),我们为刻度值的 before
伪元素 添加了一些样式。
.sz-scale-val::before {
position: absolute;
content: "";
height: 0.05em;
background: #333;
right: -12px;
bottom: 0.58em;
}
.sz-first .sz-scale-val::before {
width: 10px;
}
.sz-second .sz-scale-val::before {
width: 8px;
}
.sz-third .sz-scale-val::before {
width: 6px;
}
.sz-fourth .sz-scale-val::before {
width: 4px;
}
.sz-fifth .sz-scale-val::before {
width: 2px;
}
图表范围
控制显示的图表范围
在我们的图表中,我们可能还想看到的另一个信息是当前显示范围的指示。除了查看当前范围之外,我们可能还想暂停图表更新,然后滚动到另一个想要的范围。为此,我们创建了另一个内部组件
const rangeComponent = {
props: ['min', 'max', 'paused', 'back_offset', 'settings'],
template:
`<div class="sz-graph-range">
<div class="sz-min-val">{{ minStr }}</div>
<input type="range" v-show="rangeVisible"
min="0" :max="rangeMax" v-model="rangeVal">
</input>
<button v-if="settings.showPauseButton" @click="onPauseClicked">
<svg v-if="this.paused" viewBox="0 0 100 100"
preserveAspectRatio="none">
<polygon points="20,10 85,50 20,90" />
</svg>
<svg v-else>
<rect x="10%" y="5%" width="30%"
height="90%" rx="10%" ry="10%" />
<rect x="60%" y="5%" width="30%"
height="90%" rx="10%" ry="10%" />
</svg>
</button>
<div class="sz-max-val">{{ maxStr }}</div>
</div>`,
data() {
return {
rangeVal: 0
};
},
computed: {
rangeMax() {
let ret = this.min + this.back_offset;
// If we aren't paused, the back-offset is zero.
// So, set the range value to the range maximum.
if (!this.paused) {
this.rangeVal = ret;
}
return ret;
},
rangeVisible() {
return this.settings.hasGraphRange && this.paused && this.rangeMax > 0;
}
}
};
在此组件中,我们有范围边界的值、一个用于暂停和恢复的按钮,以及一个 range
input
用于滚动到所需范围(当图表暂停时)。这些元素使用网格布局水平排列,如下所示。
.sz-graph-range {
grid-area: range;
position: relative;
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "min range button max";
border-top: solid 0.1em #333;
min-height: 0.5em;
}
.sz-graph-range .sz-min-val {
grid-area: min;
}
.sz-graph-range .sz-max-val {
grid-area: max;
}
.sz-graph-range input[type="range"] {
grid-area: range;
}
.sz-graph-range button {
position: relative;
grid-area: button;
margin-top: 0.25em;
margin-bottom: 0.25em;
width: 2em;
}
在暂停/恢复按钮中,我们有一个暂停状态下显示的恢复图形()和非暂停状态下显示的暂停图形(
)。为了通知暂停状态的变化,我们为每次点击发出一个
paused-changed
事件。
methods: {
onPauseClicked() {
this.$emit('paused-changed', !this.paused);
}
}
为范围赋予有意义的描述
现在,我们有一个通用的范围值显示解决方案,可以显示最小和最大索引的范围。但是,有时我们可能需要对显示的范围值进行更有意义的描述。在我们的解决方案中,我们支持两种样本描述类型:数字范围和时间范围。对于每个描述符,我们可以定义一个起始值和一个恒定的步长。为此,我们向 图表设置 添加了另一组属性。
samplesDescriptors: {
type: 'number', // One of: ['number', 'time'].
startingValue: 0,
stepSize: 1,
format: '',
digitsAfterDecimalPoint: 0
}
为了计算实际范围值,我们创建了一个 mixin 来执行必要的计算。
const rangeOperationsMixin = {
methods: {
formatDate(date, format) {
const twoDigitsStr = num => num > 9 ? `${num}` : `0${num}`;
const threeDigitsStr = num => num > 99 ? `${num}` : `0${twoDigitsStr(num)}`;
const monthsNames = ['January', 'February',
'March', 'April', 'May', 'June',
'July', 'August', 'September',
'October', 'November', 'December'];
const daysNames = ['Sunday', 'Monday', 'Tuesday',
'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const year = date.getFullYear();
const month = date.getMonth();
const dayInMonth = date.getDate();
const dayInWeek = date.getDay();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const milliseconds = date.getMilliseconds();
let res = format;
res = res.replace('YYYY', `${year}`);
res = res.replace('YY', twoDigitsStr(year % 100));
res = res.replace('MMMM', monthsNames[month]);
res = res.replace('MMM', monthsNames[month].substr(0, 3));
res = res.replace('MM', twoDigitsStr(month + 1));
res = res.replace('DDDD', daysNames[dayInWeek]);
res = res.replace('DDD', daysNames[dayInWeek].substr(0, 3));
res = res.replace('DD', daysNames[dayInWeek].substr(0, 2));
res = res.replace('dd', twoDigitsStr(dayInMonth));
res = res.replace('HH', twoDigitsStr(hours));
res = res.replace('mm', twoDigitsStr(minutes));
res = res.replace('sss', threeDigitsStr(milliseconds));
res = res.replace('ss', twoDigitsStr(seconds));
return res;
},
addMillisecondsToDate(date, milliseconds) {
return new Date(date.getTime() + milliseconds);
},
getSampleDescriptorStr(value, samplesDescriptors) {
if (samplesDescriptors.type == 'time') {
return this.formatDate(
this.addMillisecondsToDate(
samplesDescriptors.startingValue,
value * samplesDescriptors.stepSize), samplesDescriptors.format);
}
else {
return (samplesDescriptors.startingValue +
value * samplesDescriptors.stepSize).toFixed
(samplesDescriptors.digitsAfterDecimalPoint);
}
}
}
};
在此 mixin 中,我们有三个函数:
formatDate
:根据指定的格式获取给定日期的格式化字符串。addMillisecondsToDate
:根据给定的起始日期和额外的毫秒数获取新日期。getSampleDescriptorStr
:根据给定的索引和样本描述符设置,获取范围值的格式化字符串。
为了使用此 mixin 显示范围值的描述,我们向 rangeComponent
组件添加了另外两个计算属性。
const rangeComponent = {
// ...
mixins: [rangeOperationsMixin],
computed: {
minStr() {
return this.getSampleDescriptorStr(this.min, this.settings.samplesDescriptors);
},
maxStr() {
return this.getSampleDescriptorStr(this.max, this.settings.samplesDescriptors);
},
// ...
},
// ...
};
指示点值
点值描述
所以,我们有一个图表绘制和一个显示当前范围边界的范围控件。我们可能想在图表中看到的另一个有用选项是显示每个图表点的值。为此,我们添加了另一个内部组件。
const pointValuesComponent = {
props: ['mouse_x', 'mouse_y', 'values_descriptor', 'values', 'settings'],
template:
`<div class="sz-graph-point-values" :style="{ top: y, left: x }">
<header>{{ values_descriptor }}</header>
<div v-for="v in valuesDetails" >
<span class="sz-graph-point-bullet" :style="{ background: v.color }">
<span v-if="v.hasFill" :style="{ background: v.fillColor }"></span>
</span>{{ v.value }}
</div>
</div>`,
data() {
return {
x: '0',
y: '0'
};
},
computed: {
valuesDetails() {
const vals = this.values;
const graphSettings = this.settings.drawing.graph;
const res = [];
// Get values descriptions for each point in the given values.
for (let valInx = 0; valInx < vals.length; valInx++) {
if (graphSettings.visibility[valInx]) {
let val = vals[valInx];
if (this.settings.pointIndication.digitsAfterDecimalPoint > 0) {
val = val.toFixed
(this.settings.pointIndication.digitsAfterDecimalPoint);
}
res.push({
color: graphSettings.showLines || graphSettings.showCircles
? graphSettings.colors[valInx]
: graphSettings.fillColors[valInx],
fillColor: graphSettings.fillColors[valInx],
hasFill: graphSettings.showFill,
value: val
});
}
}
return res;
}
}
};
在此组件中,我们有一个边框,其中包含
- 一个标题,其中包含当前点值的描述。
- 每个显示图表的 [y] 轴描述符。在此描述符中,我们有:
- 一个带有相应图表颜色指示的圆点,以及
- 在被指示位置的图表值。
当鼠标悬停在图表绘制区域时,此边框将显示(如 下一节 所述)。当鼠标悬停时显示额外内容的一个副作用是,额外内容被视为父元素的一部分。因此,只要鼠标悬停在额外内容上(即使它在父元素外部),它也会被视为悬停在父元素上。这种行为可能导致不理想的结果。只要鼠标悬停在额外内容上(即使它在父元素外部),额外内容仍会显示。除了不理想的显示之外,它还可以隐藏(并阻止访问)其他元素。
为了解决这个问题,我们将点值边框的位置设置得根据鼠标与父元素边界的距离。如果鼠标靠近父元素的左侧,我们将边框显示在光标的右侧(反之亦然)。如果鼠标靠近父元素的底部,我们将边框显示在光标上方(反之亦然)。这可以通过 监视 鼠标位置的变化来实现,并相应地设置边框位置,如下所示。
watch: {
mouse_x() {
const el = this.$el;
const width = el.offsetWidth;
const parentWidth = el.parentNode.offsetWidth;
const mouseX = this.mouse_x;
// Set the x coordinate according to this element's width
// and the parent element's width.
if (mouseX >= 0) {
let res = mouseX;
if (res > parentWidth / 2) {
res -= width;
}
this.x = `${res}px`;
}
},
mouse_y() {
const el = this.$el;
const height = el.offsetHeight;
const parentHeight = el.parentNode.offsetHeight;
const mouseY = this.mouse_y;
// Set the y coordinate according to this element's height
// and the parent element's height.
if (mouseY >= 0) {
let res = mouseY;
if (res > parentHeight / 2) {
res -= height;
}
this.y = `${res}px`;
}
}
}
在鼠标位置显示点指示
现在,有了这个 pointValuesComponent
,我们可以显示每个点的值。在我们的解决方案中,我们想显示离鼠标光标最近的点的值。为此,我们在 drawingComponent
组件中添加了计算属性来指示最近的点索引。
const drawingComponent = {
//...
data() {
return {
mousePos: { x: -1, y: -1 },
lastAbsolutePointValuesIndex: -1,
lastPointValuesDescriptor: ''
};
},
computed: {
pointValuesIndex() {
const el = this.$el;
let res = -1;
if (this.mousePos.x >= 0) {
res = 0;
if (this.range > 1) {
const width = el ? el.offsetWidth : 1;
const valueWidthUnit = width / this.range;
res = Math.round(this.mousePos.x / valueWidthUnit);
}
}
return res;
},
absolutePointValuesIndex() {
let res = this.pointValuesIndex;
if (res >= 0) {
res += this.range_offset;
}
if (this.lastAbsolutePointValuesIndex != res) {
this.lastAbsolutePointValuesIndex = res;
this.$emit('pointed-values-index-changed', res);
}
return res;
}
},
methods: {
onMouseMove(e) {
const el = this.$el;
let rect = el.getBoundingClientRect();
this.mousePos.y = e.clientY - rect.top;
this.mousePos.x = e.clientX - rect.left;
},
onMouseLeave(e) {
// Indicate that there is no mouse inside the component.
this.mousePos.y = -1;
this.mousePos.x = -1;
}
}
};
使用这些属性,我们可以计算出被指示位置的值。
const drawingComponent = {
//...
mixins: [rangeOperationsMixin],
computed: {
// ...
pointValues() {
let valueIndex = this.pointValuesIndex;
if (valueIndex < 0) {
valueIndex = 0;
}
return this.values.map(v => valueIndex < v.length ? v[valueIndex] : 0);
},
pointValuesDescriptor() {
const valuesIndex = this.absolutePointValuesIndex;
const res = valuesIndex >= 0 ?
this.getSampleDescriptorStr(valuesIndex, this.settings.samplesDescriptors) : '';
if (this.lastPointValuesDescriptor != res) {
this.lastPointValuesDescriptor = res;
this.$emit('pointed-values-descriptor-changed', res);
}
return res;
}
}
// ...
};
以及它们在图表中的坐标。
const drawingComponent = {
//...
computed: {
// ...
pointValueX() {
const el = this.$el;
let res = 0;
if (this.range > 1 && this.mousePos.x >= 0) {
const width = el ? el.offsetWidth : 1;
const valueWidthUnit = width / this.range;
const valueIndex = Math.round(this.mousePos.x / valueWidthUnit);
res = valueIndex * valueWidthUnit;
}
return res;
},
pointValuesY() {
const values = this.values;
const graphSettings = this.settings.drawing.graph;
const el = this.$el;
const res = [];
if (this.range > 1 && this.mousePos.x >= 0) {
const width = el ? el.offsetWidth : 1, height = el ? el.offsetHeight : 1;
const valueWidthUnit = width / this.range;
const valueHeightUnit = height / (this.max - this.min);
const valueIndex = Math.round(this.mousePos.x / valueWidthUnit);
for (let valInx = 0; valInx < values.length; valInx++) {
if (graphSettings.visibility[valInx]) {
if (valueIndex < values[valInx].length) {
res.push((this.max -
values[valInx][valueIndex]) * valueHeightUnit);
}
}
}
}
return res;
}
}
// ...
};
为了在鼠标悬停时显示点值,我们在 drawingComponent
组件的模板中添加了一个额外的元素。
const drawingComponent = {
//...
template:
`<div class="sz-graph-drawing"
@mousemove="onMouseMove" @mouseleave="onMouseLeave">
<sz-graph-canvas v-if="settings.drawing.drawUsingCanvas"
:values="values" :range="range" :min="min"
:max="max" :settings="settings"
:highlighted_index="highlighted_index"></sz-graph-canvas>
<sz-graph-svg v-else
:values="values" :range="range"
:min="min" :max="max" :settings="settings"
:highlighted_index="highlighted_index"></sz-graph-svg>
<div class="sz-graph-point-indicator">
<svg v-show="settings.pointIndication.showLines"
class="sz-graph-point-lines">
<line :x1="pointValueX" y1="0"
:x2="pointValueX" y2="100%" />
<line v-for="valY in pointValuesY" x1="0" :y1="valY"
x2="100%" :y2="valY" />
<circle v-for="valY in pointValuesY"
:cx="pointValueX" :cy="valY" r="4" />
</svg>
<sz-graph-point-values v-show="settings.pointIndication.showValues"
:mouse_x="mousePos.x" :mouse_y="mousePos.y"
:values_descriptor="pointValuesDescriptor"
:values="pointValues" :settings="settings" ></sz-graph-point-values>
</div>
</div>`
// ...
};
并对其进行样式设置,使其仅在鼠标悬停在图表绘制区域时出现。
.sz-graph-point-indicator {
opacity: 0;
transition: opacity 0.5s;
}
.sz-graph-drawing:hover .sz-graph-point-indicator {
opacity: 1;
transition: opacity 0.4s;
}
在该元素中,我们显示 SVG
线条以指示刻度中每个点的位置,以及一个带有这些点值的边框。
组合图表内容
图表可见视图
在拥有所有图表组件之后,我们可以组合我们的图表视图。为此,我们添加了另一个内部组件。
const contentComponent = {
props: ['values', 'settings', 'show_settings',
'highlighted_index', 'paused', 'back_offset'],
components: {
'sz-graph-scale': scaleComponent,
'sz-graph-range': rangeComponent,
'sz-graph-drawing': drawingComponent
},
template:
`<div class="sz-graph-content">
<sz-graph-scale v-show="settings.scaleVisibility != 'collapsed'"
:style="{opacity: settings.scaleVisibility == 'hidden' ? 0 : 1}"
:min="scaleMin" :max="scaleMax"
:settings="settings"></sz-graph-scale>
<sz-graph-range v-show="settings.rangeVisibility != 'collapsed'"
:style="{opacity: settings.rangeVisibility == 'hidden' ? 0 : 1}"
:min="rangeMin" :max="rangeMax"
:paused="paused" :back_offset="back_offset"
:settings="settings" @paused-changed="onPausedChanged"
@back-offset-changed="onBackOffsetChanged"></sz-graph-range>
<sz-graph-drawing :values="visibleValues" :range="visibleRange"
:range_offset="rangeMin" :min="scaleMin"
:max="scaleMax" :settings="settings"
:highlighted_index="highlighted_index"
@pointed-values-index-changed="onPointedValueIndexChanged"
@pointed-values-descriptor-changed="onPointedValueDescriptorChanged">
</sz-graph-drawing>
</div>`,
methods: {
onPausedChanged(newVal) {
this.$emit('paused-changed', newVal);
},
onBackOffsetChanged(newVal) {
this.$emit('back-offset-changed', newVal);
},
onPointedValueIndexChanged(newVal) {
this.$emit('pointed-values-index-changed', newVal);
},
onPointedValueDescriptorChanged(newVal) {
this.$emit('pointed-values-descriptor-changed', newVal);
}
}
};
如上所示,我们的图表有一个显示根据给定边界刻度值的刻度组件,一个允许控制可见范围的范围组件,以及一个根据刻度和可见范围显示可见值的绘图组件。为了计算所需值,我们添加了一些计算属性。
computed: {
activeValues() {
return this.paused ? this.pausedValues : this.values;
},
scaleMin() {
const vals = this.activeValues;
const scaleSettings = this.settings.scale;
let res = 0;
// Get the scale's minimum according to the graph's settings.
if (vals.every(v => v.length < 2) || scaleSettings.hasMin) {
// There is a minimum definition or there aren't enough values.
// Use the settings definition.
res = scaleSettings.min;
}
else {
// There is no minimum definition.
// Get the minimal value of the graph's values.
res = Math.min(...vals.reduce((acc, v) => acc.concat(v), []));
}
// Round to integer if needed.
if (scaleSettings.useIntegerBoundaries) {
res = Math.floor(res);
}
return res;
},
scaleMax() {
const vals = this.activeValues;
const scaleSettings = this.settings.scale;
let res = 100;
// Get the scale's maximum according to the graph's settings.
if (vals.every(v => v.length < 2) || scaleSettings.hasMax) {
// There is a maximum definition or there aren't enough values.
// Use the settings definition.
res = scaleSettings.max;
}
else {
// There is no maximum definition.
// Get the maximal value of the graph's values.
res = Math.max(...vals.reduce((acc, v) => acc.concat(v), []));
}
// Round to integer if needed.
if (scaleSettings.useIntegerBoundaries) {
res = Math.ceil(res);
}
return res;
},
rangeMin() {
const settings = this.settings;
const maxLength = Math.max(...this.activeValues.map(v => v.length));
let minVal = 0;
if (settings.hasGraphRange && maxLength > settings.graphRange) {
minVal = maxLength - settings.graphRange - 1 - this.back_offset;
}
return minVal;
},
rangeMax() {
const settings = this.settings;
const maxLength = Math.max(...this.activeValues.map(v => v.length));
let maxVal = maxLength - 1 - this.back_offset;
if (settings.hasGraphRange && maxLength <= settings.graphRange) {
maxVal = settings.graphRange;
}
return maxVal;
},
visibleValues() {
const settings = this.settings;
let vals = this.getValuesCopy(this.activeValues);
if (settings.hasGraphRange) {
const maxLength = Math.max(...vals.map(v => v.length));
if (maxLength > settings.graphRange) {
vals = vals.map(v => v.slice
(v.length - 1 - settings.graphRange - this.back_offset,
v.length - this.back_offset));
}
}
return vals;
},
visibleRange() {
return this.settings.hasGraphRange ? this.settings.graphRange :
(this.activeValues.length > 0 ? this.activeValues[0].length - 1 : 0);
}
}
在 activeValues
属性中,我们获取图表的活动值。如前所述,我们的图表可以暂停和恢复。当图表暂停时,我们不希望显示在暂停后添加到图表的值。为此,当图表暂停时,我们存储图表值的副本,并使用该副本直到图表恢复。当图表暂停时,我们可以通过监视 paused
属性来获取图表值的副本,如下所示。
data() {
return {
pausedValues: []
};
},
watch: {
paused() {
if (this.paused) {
this.pausedValues = this.getValuesCopy(this.values);
}
}
},
methods: {
getValuesCopy(values) {
return values.map(v => [...v]);
}
// ...
}
在 scaleMin
属性中,我们获取最小刻度值。该值可以在 图表设置 中定义,或者根据图表值的边界计算得出。同样,我们使用 scaleMax
属性获取最大刻度值。
在 rangeMin
属性中,我们获取范围中最小值的索引。在我们的图表中,我们可以显示所有值的视图,或者指定范围的视图。此函数根据范围设置和当前偏移量(可以使用 rangeComponent
滚动)计算显示的最小索引。同样,我们使用 rangeMax
属性获取范围中最大值的索引。
在 visibleValues
属性中,我们获取应该在图表绘制中可见的实际值。
在 visibleRange
属性中,我们获取活动范围的大小。
图表图例
除了显示的图表之外,有时我们还可能希望看到一个描述图表值集的图例。为此,我们添加了另一个内部组件。
const legendComponent = {
props: ['values', 'settings'],
template:
`<div class="sz-graph-legend">
<div v-for="v in legendValues" >
<span class="sz-graph-legend-bullet" :style="{ background: v.color }">
<span v-if="v.hasFill" :style="{ background: v.fillColor }"></span>
</span>{{ v.title }}
</div>
</div>`,
computed: {
legendValues() {
const graphSettings = this.settings.drawing.graph;
let res = [];
// Create an appropriate description for each values set.
for (let valInx = 0; valInx < this.values.length; valInx++) {
if (graphSettings.visibility[valInx]) {
res.push({
color: graphSettings.showLines || graphSettings.showCircles
? graphSettings.colors[valInx]
: graphSettings.fillColors[valInx],
fillColor: graphSettings.fillColors[valInx],
hasFill: graphSettings.showFill,
title: graphSettings.titles[valInx]
});
}
}
return res;
}
}
};
在此组件中,我们有一个边框,显示每个值集的描述。在我们的图表中,我们可以将图例放置在图表的右侧、左侧、顶部或底部。为此,我们使用了四个不同的元素来显示图表的图例。
const contentComponent = {
components: {
'sz-graph-legend': legendComponent,
// ...
},
template:
`<div class="sz-graph-content">
<header>
<sz-graph-legend v-if="settings.legendPosition=='top'"
:values="activeValues" :settings="settings"></sz-graph-legend>
</header>
<aside class="sz-left">
<sz-graph-legend v-if="settings.legendPosition=='left'"
:values="activeValues" :settings="settings"></sz-graph-legend>
</aside>
<aside class="sz-right">
<sz-graph-legend v-if="settings.legendPosition=='right'"
:values="activeValues" :settings="settings"></sz-graph-legend>
</aside>
<footer>
<sz-graph-legend v-if="settings.legendPosition=='bottom'"
:values="activeValues" :settings="settings"></sz-graph-legend>
</footer>
<!-- ... -->
</div>`
};
根据 图表设置,我们确定显示哪个元素。
除了图例之外,为了能够编辑图表设置,我们还添加了一个设置()按钮,在点击时发出一个
settings-clicked
事件。
const contentComponent = {
// ...
template:
`<div class="sz-graph-content">
<!-- ... -->
<button v-if="show_settings" class="sz-graph-settings"
@click="onSettingsClicked">
<svg>
<rect x="0" y="11%" width="40%"
height="8%" />
<rect x="42.5%" y="1%" width="10%"
height="28%" rx="4%" ry="8%" />
<rect x="55%" y="11%" width="45%"
height="8%" />
<rect x="0" y="44%" width="75%" height="8%" />
<rect x="77.5%" y="34%" width="10%"
height="28%" rx="4%" ry="8%" />
<rect x="90%" y="44%" width="10%"
height="8%" />
<rect x="0" y="77%" width="20%" height="8%" />
<rect x="22.5%" y="67%" width="10%"
height="28%" rx="4%" ry="8%" />
<rect x="35%" y="77%" width="75%"
height="8%" />
</svg>
</button>
</div>`,
// ...
methods: {
// ...
onSettingsClicked() {
this.$emit('settings-clicked');
}
}
};
图例元素的位置(以及其他图表元素的位置)是使用网格布局确定的,如下所示。
.sz-graph-content {
display: grid;
position: absolute;
height: 100%;
width: 100%;
margin: 0;
grid-template-columns: auto auto 1fr auto;
grid-template-rows: auto 1fr auto auto;
grid-template-areas: "left . header right" "left scale graph right"
"left settings range right" ". footer footer .";
}
.sz-graph-content header {
grid-area: header;
min-height: 0.5em;
}
.sz-graph-content footer {
grid-area: footer;
}
.sz-graph-content .sz-left {
grid-area: left;
flex-direction: column;
}
.sz-graph-content .sz-right {
grid-area: right;
flex-direction: column;
}
.sz-graph-scale {
grid-area: scale;
position: relative;
margin: 0;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 100%;
grid-template-areas: "all";
padding-right: 12px;
border-right: solid 0.1em #333;
}
.sz-graph-range {
grid-area: range;
position: relative;
display: grid;
grid-template-columns: auto 1fr auto auto;
grid-template-areas: "min range button max";
border-top: solid 0.1em #333;
min-height: 0.5em;
}
.sz-graph-drawing {
grid-area: graph;
position: relative;
margin-bottom: 0em;
}
.sz-graph-settings {
position: relative;
grid-area: settings;
margin: 0.1em;
margin-top: 0.5em;
}
图表设置
如本文所述,我们可以通过图表设置来控制图表的行为。图表设置是根图表元素数据的一部分。
Vue.component('sz-graph', {
data() {
return {
settings: {
scale: {
hasMin: false,
hasMax: false,
min: 0,
max: 100,
minimalValuesGap: 25,
useIntegerBoundaries: true
},
hasGraphRange: true,
graphRange: 100,
drawing: {
showGridLines: true,
gridLinesColor: '#dddddd',
graph: {
defaultColor: '#ff0000',
defaultFillColor: '#ffdddd',
colors: ['#bb0000', '#00bb00', '#bbbb00',
'#0000bb', '#bb00bb', '#00bbbb'],
fillColors: ['#ffbbbb', '#bbffbb', '#ffffbb',
'#bbbbff', '#ffbbff', '#bbffff'],
titles: [],
visibility: [],
showLines: true,
showCircles: true,
showFill: true
},
drawUsingCanvas: false
},
pointIndication: {
showValues: true,
showLines: true,
digitsAfterDecimalPoint: -1
},
samplesDescriptors: {
type: 'number', // One of: ['number', 'time'].
startingValue: 0,
stepSize: 1,
format: '',
digitsAfterDecimalPoint: 0
},
legendPosition: 'right', // One of: ['right', 'left',
'top', 'bottom', 'none'].
scaleVisibility: 'visible', // One of ['visible', 'hidden', 'collapsed'].
rangeVisibility: 'visible', // One of ['visible', 'hidden', 'collapsed'].
showPauseButton: true,
changesCounter: 0
},
isSettingsOpened: false
};
}
});
在我们的图表设置中,我们有以下属性:
scale
:hasMin
:确定图表刻度是否具有固定的最小值。hasMax
:确定图表刻度是否具有固定的最大值。min
:固定的最小刻度值。max
:固定的最大刻度值。minimalValuesGap
:图表刻度中值之间的最小距离(以像素为单位)。useIntegerBoundaries
:确定是否将刻度最小和最大值四舍五入为整数。
hasGraphRange
:确定图表是显示所有值还是显示部分范围。graphRange
:显示的范围大小。drawing
:showGridLines
:确定是否绘制图表网格线。gridLinesColor
:图表网格线的颜色。graph
:defaultColor
:当图表索引不存在于colors
属性中时的线条颜色。defaultFillColor
:当图表索引不存在于fillColors
属性中时的填充颜色。colors
:每个图表的线条颜色(按索引)。fillColors
:每个图表的填充颜色(按索引)。titles
:每个图表的标题(显示在图表图例中)。visibility
:确定每个图表的可见性(按索引)。showLines
:确定是否为可见图表绘制线条。showCircles
:确定是否为可见图表在实际值的位置绘制圆圈。showFill
:确定是否为可见图表绘制填充。
drawUsingCanvas
:确定是使用canvas
HTML 元素还是svg
HTML 元素绘制图表。
pointIndication
:showValues
:当鼠标悬停在图表绘制区域时,确定是否显示一个边框,其中包含图表的值(在离鼠标光标最近的点)。showLines
:当鼠标悬停在图表绘制区域时,确定是否显示刻度指示线(在离鼠标光标最近的点)。digitsAfterDecimalPoint
:在值边框中显示的、小数点后的位数。
samplesDescriptors
:type
:样本计数器描述的类型。可以是number
或time
。startingValue
:样本计数器的起始值。stepSize
:每个样本增加样本计数器的步长。format
:时间格式。当样本描述类型为time
时使用。digitsAfterDecimalPoint
:在样本描述中显示的、小数点后的位数。当样本描述类型为number
时使用。
legendPosition
:确定图例显示的位置。可以是right
、left
、top
、bottom
或none
。scaleVisibility
:确定是否显示图表刻度。可以是visible
、hidden
或collapsed
。rangeVisibility
:确定是否显示图表范围。可以是visible
、hidden
或collapsed
。showPauseButton
:确定是否在图表范围中显示暂停/恢复按钮。changesCounter
:用于指示图表设置更改的内部计数器。
为了让用户更新图表设置,我们在图表内容上方显示一个设置编辑器面板(当用户单击设置按钮时)。这是这样做的。
Vue.component('sz-graph', {
props: {
'values': { type: Array, default: [] }
},
// ...
components: {
'sz-graph-content': contentComponent,
'sz-graph-settings-panel': settingsPanelComponent
},
template:
`<div class="sz-graph">
<sz-graph-content :values="fixedValues" :settings="settings"
:show_settings="show_settings"
:highlighted_index="highlighted_index" :paused="isPaused"
:back_offset="backOffset" @settings-clicked="onSettingsOpened">
</sz-graph-content>
<div v-if="isSettingsOpened"
class="sz-graph-settings-panel-border" ></div>
<div class="sz-graph-settings-panel-container"
:class="{opened: isSettingsOpened}">
<sz-graph-settings-panel :settings="settings"
:values_length="fixedValues.length"
@closed="onSettingsClosed" ></sz-graph-settings-panel>
</div>
</div>`,
computed: {
fixedValues() {
let res = this.values;
if (Array.isArray(res)) {
// Validate that we return an array of values arrays.
if (!res.every(v => Array.isArray(v))) {
if (res.every(v => !Array.isArray(v))) {
// All the values aren't arrays. It's one array of values.
// Return it as array of array.
res = [res];
} else {
res = res.map(v => Array.isArray(v) ? v : []);
}
}
} else {
res = [];
}
this.fixGraphSettings(res);
return res;
}
},
methods: {
onSettingsOpened() {
this.isSettingsOpened = true;
},
onSettingsClosed() {
this.isSettingsOpened = false;
this.notifySettingsChanged();
},
notifySettingsChanged() {
this.settings.changesCounter++;
this.settings = Object.assign({}, this.settings);
this.$emit('settings-changed', this.settings);
},
fixGraphSettings(graphValues) {
const settings = this.settings;
const valuesLength = graphValues.length;
while (settings.drawing.graph.colors.length < valuesLength) {
settings.drawing.graph.colors.push(settings.drawing.graph.defaultColor);
}
while (settings.drawing.graph.fillColors.length < valuesLength) {
settings.drawing.graph.fillColors.push
(settings.drawing.graph.defaultFillColor);
}
while (settings.drawing.graph.titles.length < valuesLength) {
settings.drawing.graph.titles.push
(`Graph ${settings.drawing.graph.titles.length + 1}`);
}
while (settings.drawing.graph.visibility.length < valuesLength) {
settings.drawing.graph.visibility.push(true);
}
}
}
});
@keyframes show-shadow-border {
0% { background: rgba(0, 0, 0, 0); }
100% { background: rgba(0, 0, 0, 0.4); }
}
.sz-graph-settings-panel-border {
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
animation-name: show-shadow-border;
animation-duration: 0.7s;
}
.sz-graph-settings-panel-container {
position: absolute;
top: 0.5em;
left: 0.5em;
right: 0.5em;
bottom: 0.5em;
display: flex;
align-content: center;
justify-content: center;
opacity: 0;
transform: scale(0, 0);
transition: opacity 0.7s, transform 0.7s;
}
.sz-graph-settings-panel-container.opened {
opacity: 1;
transform: scale(1, 1);
transition: opacity 0.6s, transform 0.6s;
transition-timing-function: ease-out;
}
在 fixedValues
计算属性中,我们验证用于图表的值集合是值数组的数组。用户设置的值可以是值数组的数组(如预期)或单个值数组。如果是单个值数组,我们将其转换为包含一个值数组的数组。
在 fixGraphSettings
函数中,我们根据值数组的数量调整图表设置。
settingsPanelComponent
组件显示一个边框,其中包含图表设置并允许编辑它们。

当用户单击 **设置** 按钮时,我们在 onSettingsOpened
函数中将 isSettingsOpened
属性设置为 true
;当用户单击设置面板中的 **关闭** 按钮时,我们在 onSettingsClosed
函数中将其设置为 false
。这样,我们就可以在需要时显示和隐藏设置面板。
为了控制设置面板边框(黑色阴影)的外观,我们使用了 v-if
来在需要时添加和删除 HTML 元素(而不是一个改变其不透明度的元素),因为当设置面板关闭时,我们不希望它隐藏(位于其下方)的其他元素,从而阻止用户交互。因此(因为 CSS 过渡 是针对改变状态的现有元素,而 CSS 动画 在元素显示时开始),为了动画化元素的出现,我们使用了动画而不是过渡(就像我们在图表中使用其他动画一样)。
有时,过多的可能性会导致过多的混淆。在我们的例子中,显示一个完整的图表设置面板可能超出了需要。有时,我们可能希望用户只编辑部分设置(或根本不编辑设置)。为此,我们提供了不显示设置面板的选项,并在图表外部控制图表设置。这可以通过 default_settings
属性来实现。
Vue.component('sz-graph', {
props: {
// ...
'default_settings': { type: Object, default: {} },
'show_settings': { type: Boolean, default: true }
},
// ...
mounted() {
this.applyDefaultSettings();
this.$watch('default_settings',
() => {
if (!this.show_settings) {
this.applyDefaultSettings();
}
});
},
methods: {
// ...
deepCopy(dst, src) {
const isObject = obj => typeof obj === 'object';
for (let p in dst) {
if (dst[p] && isObject(dst[p])) {
if (src && src[p]) {
this.deepCopy(dst[p], src[p]);
}
}
else {
if (src && p in src) {
dst[p] = src[p];
}
}
}
},
applyDefaultSettings() {
const defaultSettings = this.default_settings;
if (defaultSettings) {
this.deepCopy(this.settings, defaultSettings);
this.fixGraphSettings(this.fixedValues);
this.notifySettingsChanged();
}
}
}
});
在 applyDefaultSettings
函数中,我们将 default_settings
复制到图表设置中并通知设置更改。当组件挂载时,我们调用 applyDefaultSettings
函数;如果 show_settings
设置为 false
,我们也会在每次 default_settings
更改时调用它。这样,如果禁用了设置更改,我们将尊重 default_settings
的所有更改。否则,我们仅在首次加载时尊重它。
除了设置属性之外,我们还添加了以下属性:设置突出显示的值集索引、设置暂停状态以及设置显示范围的偏移量。
props: {
// ...
'highlighted_index': { type: Number, default: -1 },
'paused': { type: Boolean, default: false },
'back_offset': { type: Number, default: 0 }
}
暴露图表事件
有时,我们可能希望为图表的某些内部事件定义自己的实现。我们可能希望在图表暂停或恢复时执行某些操作,或者当用户将图表范围滚动到不同偏移量时。我们可能希望显示关于我们图表的外部信息(如用户定义的图例)并相应地更新它(当图表设置更改时)。我们可能希望以不同于图表实现的方式(一个包含点值的边框以及指示刻度中每个点位置的线条)显示点值描述,并相应地更新它(当点值的索引更改时)。为了实现这一目标,我们需要一种方法来通知用户有关图表的事件。这可以通过将事件从内部组件冒泡到根组件来实现。例如,让我们看看 pointed-values-index-changed
事件是如何实现的。
const drawingComponent = {
// ...
computed: {
// ...
absolutePointValuesIndex() {
let res = this.pointValuesIndex;
if (res >= 0) {
res += this.range_offset;
}
if (this.lastAbsolutePointValuesIndex != res) {
this.lastAbsolutePointValuesIndex = res;
this.$emit('pointed-values-index-changed', res);
}
return res;
}
/// ...
}
// ...
};
const contentComponent = {
// ...
template:
`<div class="sz-graph-content">
<!-- ... -->
<sz-graph-drawing :values="visibleValues" :range="visibleRange"
:range_offset="rangeMin" :min="scaleMin"
:max="scaleMax" :settings="settings"
:highlighted_index="highlighted_index"
@pointed-values-index-changed="onPointedValueIndexChanged"
@pointed-values-descriptor-changed="onPointedValueDescriptorChanged">
</sz-graph-drawing>
<!-- ... -->
</div>`,
methods: {
// ...
onPointedValueIndexChanged(newVal) {
this.$emit('pointed-values-index-changed', newVal);
}
// ...
}
};
Vue.component('sz-graph', {
// ...
template: `<div class="sz-graph">
<sz-graph-content :values="fixedValues" :settings="settings"
:show_settings="show_settings" :highlighted_index="highlighted_index"
:paused="isPaused" :back_offset="backOffset"
@settings-clicked="onSettingsOpened"
@paused-changed="onPausedChanged"
@back-offset-changed="onBackOffsetChanged"
@pointed-values-index-changed="onPointedValueIndexChanged"
@pointed-values-descriptor-changed="onPointedValueDescriptorChanged">
</sz-graph-content>
<!-- ... -->
</div>`,
methods: {
// ...
onPointedValueIndexChanged(newVal) {
this.$emit('pointed-values-index-changed', newVal);
},
// ...
}
});
如何使用
示例 1 - 运行趋势
为了第一次演示我们的图表,我们创建了一个显示运行趋势的组件。在此组件中,我们显示了四个值集。这些值集的��值是在运行时生成的(每个值集都有自己的计算方法来计算值),根据可变的刻度边界和更新间隔。这是这样做的。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" type="text/css" href="sz_graph.css" />
<link rel="stylesheet" type="text/css" href="Main.css" />
</head>
<body>
<script src="js/vue.js"></script>
<script src="js/sz_graph.js"></script>
<script src="js/Main.js"></script>
<main id="app1" class="main-frame">
<h1 class="rt-header">Running trend</h1>
<h1 class="ds-header">Daily samples</h1>
<running-trend></running-trend>
<daily-samples></daily-samples>
</main>
<script>
window.onload = function () {
initPage();
};
</script>
</body>
</html>
Vue.component('running-trend', {
template:
`<div class="running-trend">
<sz-graph class="rt-graph"
:values="values" :highlighted_index="highlightedIndex"
:default_settings="defaultSettings"></sz-graph>
</div>`,
data() {
return {
values: [],
minValue: -99,
maxValue: 99,
updateInterval: 100,
graph1Values: [],
graph2Values: [],
graph3Values: [],
graph4Values: [],
highlightedIndex: 1,
defaultSettings: {
drawing: {
graph: {
showCircles: false,
showFill: false
}
},
pointIndication: {
digitsAfterDecimalPoint: 3
}
}
};
},
created() {
this.UpdateGraphValues();
},
methods: {
UpdateGraphValues() {
const min = parseInt(this.minValue), max = parseInt(this.maxValue);
if (min && min !== NaN && max && max !== NaN) {
const scaleSize = max - min;
this.graph1Values.push(Math.random() * scaleSize + min);
this.graph2Values.push(Math.sin(Math.PI / 20 * this.graph2Values.length) *
(scaleSize / 2) + min + (scaleSize / 2));
this.graph3Values.push(Math.cos(Math.PI / 15 * this.graph3Values.length) *
(scaleSize / 2) + min + (scaleSize / 2));
this.graph4Values.push(Math.tan(Math.PI / 25 * this.graph4Values.length) *
(scaleSize / 2) / 16 * Math.random() + min + (scaleSize / 2));
this.values = [this.graph1Values, this.graph2Values,
this.graph3Values, this.graph4Values];
}
setTimeout(() => {
this.UpdateGraphValues();
}, this.updateInterval);
}
}
});
let app1 = new Vue({
el: '#app1'
});
body {
background: rgb(199, 223, 255);
}
main {
position: fixed;
top: 0.5em;
bottom: 0.5em;
left: 0.5em;
right: 0.5em;
}
.main-frame {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0.5em;
grid-template-rows: auto 1fr;
grid-template-areas: "rt-header ds-header" "running-trend daily-samples";
overflow: auto;
}
.main-frame > h1 {
font-size: 1.8rem;
font-weight: normal;
font-family: Arial;
margin: 0.2em;
color: #060;
background-image: radial-gradient(#ccc, #ffffff00 150%);
border-radius: 40%;
text-align: center;
}
.main-frame .rt-header {
position:relative;
grid-area: rt-header;
}
.main-frame .ds-header {
position:relative;
grid-area: ds-header;
}
.main-frame .running-trend {
position: relative;
grid-area: running-trend;
display: grid;
grid-template-columns: 48% 1fr;
grid-template-rows: 1fr auto;
grid-template-areas: "rt-graph rt-graph" "ud hd";
grid-gap: 0.5em;
background: #ccf;
padding: 0.3em;
border: 2px solid #048;
border-radius: 0.5em;
min-height: 15em;
min-width: 30em;
}
.main-frame .daily-samples {
grid-area: daily-samples;
display: grid;
grid-template-columns: 100%;
grid-template-rows: 1fr auto auto;
grid-template-areas: "ds-graph" "ds-legend-header" "ds-legend";
grid-gap: 0.5em;
background: #cfc;
padding: 0.3em;
border: 2px solid #084;
border-radius: 0.5em;
min-height: 15em;
}
为了使用我们的 sz-graph
组件,我们添加了一个 script
元素来加载组件,并添加了一个 link
元素来加载其样式。
在此示例中,我们将图表的默认设置设置为仅显示图表的线条(不显示圆圈和填充),并在显示点值描述时显示小数点后 3 位。用户可以通过单击设置按钮()来编辑 图表设置。
在 UpdateGraphValues
函数中,我们为每个值集生成一个新值,并调用 setTimeout
在更新间隔结束后再次调用该函数。该函数在组件创建时调用。
要为绘图区域应用背景,我们可以按如下方式为图表绘图组件设置样式。
.sz-graph-drawing {
background: #ffc;
}
为了控制生成值的刻度边界及其更新间隔,我们添加了一个边框,允许编辑这些值。
Vue.component('running-trend', {
template:
`<div class="running-trend">
<sz-graph class="rt-graph"
:values="values" :highlighted_index="highlightedIndex"
:default_settings="defaultSettings"></sz-graph>
<div class="update-details">
<header>Generated values:</header>
<div class="ud-min"><span>Min: </span>
<input type="number" v-model="minValue" /></div>
<div class="ud-max"><span>Max: </span>
<input type="number" v-model="maxValue" /></div>
<div class="ud-int"><span>Update interval:
</span><input type="number"
v-model="updateInterval" /></div>
</div>
</div>`
// ...
});
.running-trend .update-details {
position: relative;
grid-area: ud;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
grid-template-areas: "header header" "min max" "int int";
grid-gap: 0.3em;
background: #cfc;
padding: 0.3em;
border: 1px solid #084;
border-radius: 0.2em;
}
.running-trend .update-details > div {
position: relative;
display: flex;
align-items: center;
}
.running-trend .update-details > div input {
position: relative;
flex-grow: 1;
width: 3em;
}
.running-trend .update-details header {
position: relative;
grid-area: header;
color: #060;
}
.running-trend .ud-min {
position: relative;
grid-area: min;
}
.running-trend .ud-max {
position: relative;
grid-area: max;
}
.running-trend .ud-int {
position: relative;
grid-area: int;
}
除了显示图表之外,我们还提供了一个选项来选择突出显示的值集索引,并显示其值集的最小值、最大值、平均值和中位数。为此,我们添加了一个额外的边框。
Vue.component('running-trend', {
template:
`<div class="running-trend">
<sz-graph class="rt-graph" :values="values"
:highlighted_index="highlightedIndex"
:default_settings="defaultSettings"></sz-graph>
<div class="update-details">
<header>Generated values:</header>
<div class="ud-min"><span>Min: </span>
<input type="number" v-model="minValue" /></div>
<div class="ud-max"><span>Max: </span>
<input type="number" v-model="maxValue" /></div>
<div class="ud-int"><span>Update interval:
</span><input type="number"
v-model="updateInterval" /></div>
</div>
<div class="highlighted-data">
<div class="hd-hi">Highlighted graph index:
<select v-model="highlightedIndex">
<option v-for="i in graphsIndexes" :value="i">{{ i }}</option>
</select>
</div>
<div class="hd-min">Min:
<span class="hd-val">{{ minGraphValue }}</span></div>
<div class="hd-max">Max:
<span class="hd-val">{{ maxGraphValue }}</span></div>
<div class="hd-avg">Average:
<span class="hd-val">
{{ averageGraphValue }}</span></div>
<div class="hd-med">Median:
<span class="hd-val">
{{ medianGraphValue }}</span></div>
</div>
</div>`,
computed: {
graphsIndexes() {
return this.values.map((v, i) => i);
},
minGraphValue() {
return Math.min(...this.values[this.highlightedIndex]).toFixed(3);
},
maxGraphValue() {
return Math.max(...this.values[this.highlightedIndex]).toFixed(3);
},
averageGraphValue() {
const valuesLength = this.values[this.highlightedIndex].length;
const sum = this.values[this.highlightedIndex].reduce((acc, v) => acc + v, 0);
const avg = valuesLength > 0 ? sum / valuesLength : 0;
return avg.toFixed(3);
},
medianGraphValue() {
let res = 0;
const sortedValues = [...this.values[this.highlightedIndex]].sort(
(a, b) => a < b ? -1 : (a > b ? 1 : 0));
if (sortedValues.length > 0) {
if (sortedValues.length % 2 == 0) {
// Even length.
const medianIndex = sortedValues.length / 2;
res = (sortedValues[medianIndex] + sortedValues[medianIndex - 1]) / 2;
} else {
// Odd length.
res = sortedValues[(sortedValues.length - 1) / 2];
}
}
return res.toFixed(3);
}
}
// ...
});
.running-trend .highlighted-data {
position: relative;
grid-area: hd;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
grid-template-areas: "hi hi" "min max" "avg med";
background: #cfc;
padding: 0.3em;
border: 1px solid #084;
border-radius: 0.2em;
}
.running-trend .hd-hi {
position: relative;
grid-area: hi;
}
.running-trend .hd-min {
position: relative;
grid-area: min;
}
.running-trend .hd-max {
position: relative;
grid-area: max;
}
.running-trend .hd-val {
color: blue;
}
.running-trend .hd-avg {
position: relative;
grid-area: avg;
}
.running-trend .hd-med {
position: relative;
grid-area: med;
}
结果是:

示例 2 - 每日样本
为了第二次演示我们的图表,我们创建了一个显示带生成样本的图表的组件。在此组件中,我们有六组样本值,这些值可以全部显示在一个图表中,或者分开显示在每个样本集的单独图表中。这是这样做的。
Vue.component('daily-samples', {
template:
`<div class="daily-samples">
<sz-graph v-if="isAllInOne" class="ds-graph" :values="values"
:default_settings="allInOneGraphSettings" :show_settings="false"
:paused="true" :back_offset="backOffset"
@back-offset-changed="onBackOffsetChanged"></sz-graph>
<div v-else class="ds-single-graphs">
<sz-graph v-if="allInOneGraphSettings.drawing.graph.visibility[i]"
v-for="(n, i) in values.length"
:key="i" :values="values[i]"
:default_settings="singleGraphsSettings[i]" :show_settings="false"
:paused="true" :back_offset="backOffset"
@back-offset-changed="onBackOffsetChanged"></sz-graph>
</div>
</div>`,
data() {
return {
values: [],
isAllInOne: true,
backOffset: 10,
allInOneGraphSettings: {
legendPosition: 'none',
scale: {
hasMin: true,
hasMax: true,
min: 0,
max: 100
},
hasGraphRange: true,
graphRange: 24,
drawing: {
graph: {
colors: ['#bb0000', '#00bb00', '#bbbb00',
'#0000bb', '#bb00bb', '#00bbbb'],
fillColors: ['#ffbbbb', '#bbffbb', '#ffffbb',
'#bbbbff', '#ffbbff', '#bbffff'],
titles: ['Aug 16 2020', 'Aug 17 2020', 'Aug 18 2020',
'Aug 19 2020', 'Aug 20 2020', 'Aug 21 2020'],
visibility: [true, true, false, false, true, true]
}
},
pointIndication: {
showValues: false,
digitsAfterDecimalPoint: 3
},
samplesDescriptors: {
type: 'time',
startingValue: new Date(2020, 8, 22, 8),
stepSize: 600000, // 10 minutes
format: 'HH:mm'
},
showPauseButton: false
},
singleGraphsSettings: []
};
},
created() {
for (let i = 0; i < 6; i++) {
const vals = [];
for (let j = 0; j < 73; j++) {
vals.push(5 + 90 * Math.random());
}
this.values.push(vals);
const singleGraphSettings = {
legendPosition: 'none',
scale: {
hasMin: true,
hasMax: true,
min: 0,
max: 100
},
hasGraphRange: true,
graphRange: 24,
drawing: {
graph: {
colors: [this.allInOneGraphSettings.drawing.graph.colors[i]],
fillColors: [this.allInOneGraphSettings.drawing.graph.fillColors[i]],
titles: [this.allInOneGraphSettings.drawing.graph.titles[i]],
}
},
pointIndication: {
showValues: false,
digitsAfterDecimalPoint: 3
},
samplesDescriptors: {
type: 'time',
startingValue: new Date(2020, 8, 22, 8),
stepSize: 600000, // 10 minutes
format: 'HH:mm'
},
showPauseButton: false
};
this.singleGraphsSettings.push(singleGraphSettings);
}
},
methods: {
onBackOffsetChanged(newVal) {
this.backOffset = newVal;
}
}
});
在此组件的模板中,当 isAllInOne
为 true
时,我们有一个 sz-graph
元素用于显示所有样本图表;否则,我们有一组 sz-graph
元素用于显示每个样本集的单独图表。我们通过处理 back-offset-changed
事件来设置所有图表的 back_offset
,从而同步所有图表的范围滚动。当组件创建时,我们初始化每个样本集的值和设置。正如我们所见,
- 我们将 图表设置 的编辑禁用(通过将
show_settings
设置为false
)。 - 在提供的设置中,我们有:
- 固定的刻度(从 0 到 100)。
- 24 个样本的范围大小。
- 点指示设置,仅显示刻度指示线。
- 类型为
time
的样本描述符,格式为HH:mm
,步长为 10 分钟。
除了显示图表之外,我们还提供了一个用户定义的图例,可以控制每个样本集的可见性。在此图例中,我们还显示了被指示位置的图表值。这是通过添加另一个组件来显示图例条目来完成的。
Vue.component('legend-entry', {
props: ['color', 'fill', 'title', 'index',
'checked', 'has_pointed_value', 'pointed_value'],
data() {
return {
isChecked: true
};
},
template:
`<div>
<input type="checkbox" v-model="isChecked" />
<span class="sz-graph-legend-bullet" :style="{ background: color }">
<span :style="{ background: fill }"></span>
</span>
<div class="le-title">
<div>{{ title }}</div>
<div class="le-point-value"
:style="{opacity: has_pointed_value ? 1 : 0}" >
{{ pointed_value }}</div>
</div>
</div>`,
mounted() {
this.isChecked = this.checked;
},
watch: {
checked() {
this.isChecked = this.checked;
},
isChecked() {
this.$emit('checked-changed', { index: this.index, checked: this.isChecked });
}
}
});
并在 daily-samples
组件中使用它,如下所示。
Vue.component('daily-samples', {
template:
`<div class="daily-samples">
<sz-graph v-if="isAllInOne" class="ds-graph" :values="values"
:default_settings="allInOneGraphSettings" :show_settings="false"
:paused="true" :back_offset="backOffset"
@back-offset-changed="onBackOffsetChanged"
@pointed-values-index-changed="onPointedValueIndexChanged"
@pointed-values-descriptor-changed="onPointedValueDescriptorChanged">
</sz-graph>
<div v-else class="ds-single-graphs">
<sz-graph v-if="allInOneGraphSettings.drawing.graph.visibility[i]"
v-for="(n, i) in values.length"
:key="i" :values="values[i]"
:default_settings="singleGraphsSettings[i]" :show_settings="false"
:paused="true" :back_offset="backOffset"
@back-offset-changed="onBackOffsetChanged"
@pointed-values-index-changed="onPointedValueIndexChanged"
@pointed-values-descriptor-changed="onPointedValueDescriptorChanged">
</sz-graph>
</div>
<div class="ds-legend-header">
<div class="ds-lh-desc" :style="{opacity: hasPointedValues ? 1 : 0}">
Pointed values time: <span> {{ pointedValuesDescriptor }} </span></div>
<div><label><input type="checkbox" v-model="isAllInOne" />
All in one</label></div>
</div>
<footer class="ds-legend">
<div class="sz-graph-legend">
<legend-entry v-for="(n, i) in values.length" :key="i" :index="i"
:color="allInOneGraphSettings.drawing.graph.colors[i]"
:fill="allInOneGraphSettings.drawing.graph.fillColors[i]"
:title="allInOneGraphSettings.drawing.graph.titles[i]"
:checked="allInOneGraphSettings.drawing.graph.visibility[i]"
:has_pointed_value="hasPointedValues"
:pointed_value="pointedValues[i]"
@checked-changed="onCheckedChanged"></legend-entry>
</div>
</footer>
</div>`,
data() {
return {
// ...
hasPointedValues: false,
pointedValues: [],
pointedValuesDescriptor: ''
};
},
methods: {
onCheckedChanged(ev) {
this.allInOneGraphSettings.drawing.graph.visibility[ev.index] = ev.checked;
this.allInOneGraphSettings = Object.assign({}, this.allInOneGraphSettings);
},
onPointedValueIndexChanged(newVal) {
if (newVal >= 0) {
this.pointedValues = this.values.map(vals => vals[newVal].toFixed(2));
this.hasPointedValues = true;
} else {
this.hasPointedValues = false;
}
},
onPointedValueDescriptorChanged(newVal) {
this.pointedValuesDescriptor = newVal;
}
}
});
在图例条目中,我们显示:
- 一个复选框,用于控制每个图表的可见性。
- 一个圆点,其样式与默认图例圆点相同(具有相同的类)。
- 一个标题,用于描述样本集。
- 样本集在被指示位置的图表值。
为了控制每个样本集图表的可见性,我们处理 checked-changed
事件来设置相应图表的可见性。
为了显示被指示位置的图表值,我们处理 pointed-values-index-changed
和 pointed-values-descriptor-changed
事件来更新相应的属性。
结果是:

我们的示例的布局是并排显示这两个示例(当视图宽度足够大时)。当视图宽度不够大时,我们一个接一个地显示示例。这是使用 媒体查询 完成的,如下所示。
@media only screen and (max-width: 75rem) {
.main-frame {
grid-template-columns: 1fr auto;
grid-template-rows: 1fr 1fr;
grid-template-areas: "running-trend rt-header" "daily-samples ds-header";
}
.main-frame > h1 {
writing-mode: vertical-lr;
}
}
结果是:

历史
- 2020 年 12 月 4 日:初始版本