用JavaScript无canvas来完成一个柱状图表

前言

提起数据可视化技术,都不免会让人想到echarts,而最简单的入门图表就是柱状图了。它是基于canvas来实现的,而我在想,如果不用canvas,我是否能不用canvas实现柱状图,经过我的探索,终于实现了一款柱状图表。

让我们查看一个已经完成的在线示例,如下图所示:

分析实现思路

首先,我们需要确定柱状图表有哪些部分,第一右上角头部有legend部分,第二有xy轴部分,第三就是柱状图部分了。好了确定了有哪些部分,我们就可以很好的实现了,好了,让我们进入正题吧。

实现静态页面结构

编写html

目前我们完成的成品是已经封装好的,然后页面就只有一个容器元素。但我们最开始肯定不能这样写,我们先写一个写死的结构如下所示:

<div id="weekCost" class="ew-charts">

<ew-charts-body>

<ew-charts-legend>

<i class="leg-1"></i>

<span>直接访问</span>

<i class="leg-2"></i>

<span>邮件营销</span>

<i class="leg-3"></i>

<span>联盟广告</span>

<i class="leg-4"></i>

<span>视频广告</span>

<i class="leg-5"></i>

<span>搜索引擎</span>

</ew-charts-legend>

<ew-charts-x>

<div class="x-1">一月</div>

<div class="x-2">二月</div>

<div class="x-3">三月</div>

<div class="x-4">四月</div>

<div class="x-5">五月</div>

<div class="x-6">六月</div>

<div class="x-7">七月</div>

</ew-charts-x>

<ew-charts-y>

<div class="y-1">500</div>

<div class="y-2">1000</div>

<div class="y-3">1500</div>

<div class="y-4">2000</div>

</ew-charts-y>

<ew-charts-zone>

<div class="zone-1">

<bar class="bar-1 dataId-1-1" data-value="320"></bar>

<bar class="bar-2 dataId-1-2" data-value="120"></bar>

<bar class="bar-3 dataId-1-3" data-value="220"></bar>

<bar class="bar-4 dataId-1-4" data-value="150"></bar>

<bar class="bar-5 dataId-1-5" data-value="862"></bar>

</div>

<div class="zone-2">

<bar class="bar-1 dataId-2-1" data-value="332"></bar>

<bar class="bar-2 dataId-2-2" data-value="132"></bar>

<bar class="bar-3 dataId-2-3" data-value="182"></bar>

<bar class="bar-4 dataId-2-4" data-value="232"></bar>

<bar class="bar-5 dataId-2-5" data-value="1018"></bar>

</div>

<div class="zone-3">

<bar class="bar-1 dataId-3-1" data-value="301"></bar>

<bar class="bar-2 dataId-3-2" data-value="101"></bar>

<bar class="bar-3 dataId-3-3" data-value="191"></bar>

<bar class="bar-4 dataId-3-4" data-value="201"></bar>

<bar class="bar-5 dataId-3-5" data-value="964"></bar>

</div>

<div class="zone-4">

<bar class="bar-1 dataId-4-1" data-value="334"></bar>

<bar class="bar-2 dataId-4-2" data-value="134"></bar>

<bar class="bar-3 dataId-4-3" data-value="234"></bar>

<bar class="bar-4 dataId-4-4" data-value="154"></bar>

<bar class="bar-5 dataId-4-5" data-value="1026"></bar>

</div>

<div class="zone-5">

<bar class="bar-1 dataId-5-1" data-value="390"></bar>

<bar class="bar-2 dataId-5-2" data-value="90"></bar>

<bar class="bar-3 dataId-5-3" data-value="290"></bar>

<bar class="bar-4 dataId-5-4" data-value="190"></bar>

<bar class="bar-5 dataId-5-5" data-value="1679"></bar>

</div>

<div class="zone-6">

<bar class="bar-1 dataId-6-1" data-value="330"></bar>

<bar class="bar-2 dataId-6-2" data-value="230"></bar>

<bar class="bar-3 dataId-6-3" data-value="330"></bar>

<bar class="bar-4 dataId-6-4" data-value="330"></bar>

<bar class="bar-5 dataId-6-5" data-value="1600"></bar>

</div>

<div class="zone-7">

<bar class="bar-1 dataId-7-1" data-value="320"></bar>

<bar class="bar-2 dataId-7-2" data-value="210"></bar>

<bar class="bar-3 dataId-7-3" data-value="310"></bar>

<bar class="bar-4 dataId-7-4" data-value="410"></bar>

<bar class="bar-5 dataId-7-5" data-value="1570"></bar>

</div>

</ew-charts-zone>

</ew-charts-body>

</div>

编写css

接下来就需要根据页面元素,一个一个的添加样式了,这是一个慢工细活的过程,需要慢慢来。

/**

* 功能:普通页面样式设置

**/

/*********************************************/

/* 样式初始化部分 */

/*********************************************/

* {

margin: 0;

padding: 0;

}

body,html {

height: 100%;

font: 20px "微软雅黑";

-webkit-user-select: none;

-moz-user-select: none;

-ms-user-select: none;

user-select: none;

overflow: hidden;

}

/* 转换为IE盒子模型 */

*,*::before,*::after {

box-sizing: border-box;

}

/* 手型按钮 */

button,

input[type="button"],

input[type="submit"],

input[type="reset"],

input[type="radio"],

input[type="checkbox"],

a {

cursor: pointer;

}

button,

input,

textarea,

select {

outline: none;

}

@charset "utf-8";

/**

* 功能:统计图表样式

**/

/**** 图表自定义标签初始化部分 ****/

.ew-charts,

ew-charts-body,

ew-charts-x,

ew-charts-y,

ew-charts-zone,

ew-charts-legend {

display: block;

}

ew-charts-x,

ew-charts-x>div,

ew-charts-y,

ew-charts-y>div {

box-sizing: border-box;

position: absolute;

overflow: hidden;

}

ew-charts-zone,

ew-charts-zone>div,

ew-charts-zone>div bar {

box-sizing: border-box;

}

ew-charts-body,

ew-charts-zone>div,

ew-charts-zone>div bar {

position: relative;

}

ew-charts-zone,

ew-charts-zone>div bar,

ew-charts-legend,

ew-charts-zone>div bar>span {

position: absolute;

}

/* 图表容器 */

.ew-charts {

width: 100%;

height: 100%;

color: #f8f5fa;

background: linear-gradient(to right, #234, #789);

margin: auto;

color: #b3b3b3;

}

/*表体*/

ew-charts-body {

width: 100%;

height: 100%;

font-size: 16px;

}

/*X轴*/

ew-charts-x {

width: 90%;

height: 8%;

border-top: 1px solid #fefefe;

left: 6%;

bottom: 0;

}

ew-charts-x>div {

height: 100%;

text-align: center;

line-height: 30px;

top: 0;

}

/*Y轴*/

ew-charts-y {

width: 6%;

height: 80%;

border-right: 1px solid #fefefe;

overflow: visible;

left: 0;

top: 12%;

}

ew-charts-y>div {

width: 100%;

height: 24px;

text-align: right;

padding-right: 6px;

left: 0;

}

/*表格数据区间*/

ew-charts-zone {

width: 90%;

height: 80%;

left: 6%;

top: 12%;

}

ew-charts-zone>div {

height: 100%;

float: right;

}

ew-charts-zone>div bar {

height: 0;

bottom: 0;

border-top-left-radius: 3px;

border-top-right-radius: 3px;

text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);

transition: 0.6s cubic-bezier(.19, .55, .58, 1.3);

/*默认值设置*/

background-color: #606060;

border: 1px solid #cdcdcd;

box-shadow: 0 0 5px #606060;

}

ew-charts-zone>div bar:hover {

z-index: 10;

}

ew-charts-zone>div bar>span {

left: 50%;

top: -40px;

transform: translateX(-50%);

font: 32px "方正姚体", "arial";

opacity: 0;

}

ew-charts-zone>div bar>span.animation {

animation: data-value-show 0.6s forwards;

}

/*图注*/

ew-charts-legend {

top: 10px;

right: 4%;

}

ew-charts-legend i,

ew-charts-legend span {

display: inline-block;

vertical-align: middle;

}

ew-charts-legend i {

width: 34px;

height: 20px;

border-radius: 3px;

margin-left: 12px;

margin-right: 6px;

background-color: #606060;

border: 1px solid #cdcdcd;

}

ew-charts-legend span {

letter-spacing: 2px;

}

/*图表动画部分*/

@keyframes data-value-show {

0% {

opacity: 0;

}

100% {

opacity: 1;

}

}

编写js

首先,我们需要定义一个函数,用于封装。

function ewCharts(options) {

//这里是判断传入的参数中是否含有color属性,从而给予color属性值

if (!Array.isArray(options.color) || options.color.length !== options.data.Y.length) {

let len = options.data.Y.length - options.color.length;

for (let i = 0; i < len; i++) {

options.color.push('#ffffff');

}

}

//为后期的扩展做准备,type类型为bar就是默认的柱状图

options.type = options.type === "bar" ? options.type : "bar";

//将参数赋值到实例上

this.options = options;

//开始初始化

this.init(options);

}

接着,我们可以看到页面效果颜色有点高亮,接下来就是完成颜色的高亮效果工具函数,如下所示:

/**

* 颜色高亮

*/

ewCharts.prototype.lightColor = function (color) {

// 传入的颜色为16进制颜色模式,如:#ffffff

let everyColorLight = function (lightColor) {

// 将传入的颜色转换成16进制数字,然后再乘以1.6相当于将颜色变亮1.6倍

const value = Math.round(parseInt(lightColor, 16) * 1.6);

// 值有一个最小值与最大值,当超过255则等于255,最小值不能小于16

return (value >= 255 ? 255 : value <= 16 ? 16 : value).toString(16);

}

// 相当于处理每一区间的颜色代码,除了#之外的,每2位代表一种颜色,如#fef2f3,则f2代表红色区间,f2代表绿色区间,f3代表蓝色区间

return '#' + everyColorLight(color.slice(1, 3)) + everyColorLight(color.slice(3, 5)) + everyColorLight(color.slice(5, 7));

}

然后,我们需要创建一个设置样式的函数,如下所示:

/**

* 样式规则设置

*/

ewCharts.prototype.setStyle = function () {

//这里的操作无非就是判断页面中是否含有link标签,如果含有,就将样式规则插入到该标签所包含的样式表中

let link = this.$('link', false), linkIndex = 0;

for (let i = 0, len = link.length; i < len; i++) {

if (/w+.css/.test(link[i].getAttribute('href'))) {

linkIndex = i;

}

}

//api文档https://www.w3school.com.cn/xmldom/met_cssstylesheet_insertrule.asp

return link[linkIndex].sheet.insertRule.bind(link[linkIndex].sheet);

}

然后,我们再来完成一个获取DOM元素的函数封装,如下所示:

/**,

* 获取DOM元素

*/

ewCharts.prototype.$ = function (selector, isSingle) {

// 如果传入的包含#,则是唯一的元素执行querySelector方法,否则根据传入的布尔值来判断执行哪个方法查询DOM

isSingle = selector.indexOf('#') > -1 ? true : typeof isSingle === 'boolean' ? isSingle : true;

return isSingle ? document.querySelector(selector) : document.querySelectorAll(selector);

}

然后,我们就来完成初始化函数,如下所示:

/**

* 初始化

*/

ewCharts.prototype.init = function (options) {

// 设置样式规则

let setStyle = this.setStyle();

//图表类型判断,为后期做扩展

switch (options.type) {

case "bar":

//初始化页面图表所有部分

this.resetAllCharts(this.$(options.el));

//初始化X轴部分

this.resetChartsX(options.data.X, setStyle);

//初始化Y轴部分

this.resetChartsY(options.data.Y, setStyle);

//初始化图注部分

this.resetChartsLegend(options.data, setStyle);

break;

}

}

然后,完成初始化页面图表的结构,前面页面结构和css写好只有,页面应该只保留一个容器元素,如下所示:

<div id="weekCost"></div>

接下来,我们就往该元素添加结构,如下所示:

/**

* 初始化图表结构

*/

ewCharts.prototype.resetAllCharts = function (el) {

el.innerHTML = "<ew-charts-body>" +

"<ew-charts-legend></ew-charts-legend>" +

"<ew-charts-x></ew-charts-x>" +

"<ew-charts-y></ew-charts-y>" +

"<ew-charts-zone></ew-charts-zone>" +

"</ew-charts-body>";

//为容器元素添加一个类名

el.classList.add('ew-charts');

return el;

}

继续初始化X轴,如下所示:

/**

* 设置X轴

* x轴的数据

* 设置样式的方法

*/

ewCharts.prototype.resetChartsX = function (dataX, setStyle) {

let chartsX = this.$('ew-charts-x'), chartsXHTML = '';

let dataXLen = dataX.length;

// 添加x轴的文本元素

for (let i = 0; i < dataXLen; i++) {

chartsXHTML += "<div class=x-" + (i + 1) + ">" + dataX[i] + "</div>";

}

chartsX.innerHTML = chartsXHTML;

let chartsXContent = this.$('ew-charts-x > div', false), chartsXContentWidthArr = [];

// 获取元素的宽度数组,并找到最大宽度,从而设置每个元素的宽度为最大宽度

for (let j = 0; j < dataXLen; j++) {

chartsXContentWidthArr.push(chartsXContent[j].offsetWidth);

}

//最大宽度与单位宽度以及单位宽度的一半

let maxWidth = Math.max.apply(null, chartsXContentWidthArr), unitWidth = parseInt(100 / dataXLen), half = unitWidth / 2;

for (let k = 0; k < dataXLen; k++) {

//循环分别设置x轴上的坐标数据的元素宽度与left偏移量

setStyle('ew-charts-x > div.x-' + (k + 1) + '{width:' + maxWidth + 'px;' + 'left:calc(' + (unitWidth * (k + 1) - half) + '% - ' + half + 'px)}', k);

}

}

x轴部分已经完成,继续完成y轴部分:

/**

* 设置Y轴

*/

ewCharts.prototype.resetChartsY = function (dataY, setStyle) {

let newDataValue = [], chartsY = this.$('ew-charts-y'), chartsYHTML = '';

let keyNameArr = this.options.data.keyName;

let keyValue = Array.isArray(keyNameArr) && keyNameArr.length === 2 ? keyNameArr[1] : 'value';

for (let i = 0, len = dataY.length; i < len; i++) {

// 将多个value值数组合并成一个数组

newDataValue = newDataValue.concat(dataY[i][keyValue]);

}

// 求value数组的最大值

let maxValue = Math.max.apply(null, newDataValue);

if (/./.test(String(maxValue))) {

// 如果最大值有小数,则向上取整

maxValue = Math.ceil(maxValue);

}

// 定义分段数与当前Y轴的最大值

let subSections = null, currentMaxValue = null;

// 按照每段为1,5,50,500,5000,50000基准值来分段的

// 当前作为基准值判断的依据数组

let judgeMaxArr = [1000000, 100000, 10000, 1000, 100, 10];

let currentJudgeValue = null;

for (let l = 0, length = judgeMaxArr.length; l < length; l++) {

// 如果满足条件就跳出循环

if (maxValue >= judgeMaxArr[l]) {

currentJudgeValue = judgeMaxArr[l];

break;

}

}

// 如果currentValue的值为null,则默认分段值设为1

if (!currentJudgeValue) currentJudgeValue = 1;

// 计算分段数

subSections = currentJudgeValue > 1 ? Math.ceil(maxValue / (currentJudgeValue / 2)) : Math.ceil(maxValue / currentJudgeValue);

// 计算当前Y轴最大值

currentMaxValue = currentJudgeValue > 1 ? subSections * (currentJudgeValue / 2) : subSections * currentJudgeValue;

// 根据分段数来生成Y轴元素

for (let j = 0; j < subSections; j++) {

chartsYHTML += "<div class='y-" + (j + 1) + "'>" + (currentMaxValue / subSections) * (j + 1) + "</div>";

}

chartsY.innerHTML = chartsYHTML;

// 设置CSS规则

for (let k = 0; k < subSections; k++) {

setStyle('ew-charts-y > div.y-' + (k + 1) + '{ bottom:calc(' + parseInt((100 / subSections) * (k + 1)) + '% - 16px);}');

}

// 设置区域

this.resetChartsZone(subSections, keyValue, currentMaxValue, setStyle);

}

y轴部分也已经完成,接下来是完成柱状图部分,也就是区域部分,如下:

/**

* 设置区域

*/

ewCharts.prototype.resetChartsZone = function (subSections, keyValue, currentMaxValue, setStyle) {

// 区域整体背景

setStyle("ew-charts-zone { background:repeating-linear-gradient(180deg,#535456 0%,#724109 " + 100 / subSections + "%,#334455 calc(" + 100 / subSections + "% + 1px),#e0e1e5 " + 100 / subSections * 2 + "%)}", subSections + 1);

let zoneLen = this.options.data.X.length;

let chartsZone = this.$('ew-charts-zone'), chartsZoneHTML = '';

// 因为设置了margin-left与margin-right各1%,所以要减去2

let series_unit = parseInt(100 / zoneLen) - 2;

// 设置剩余空间

let freeSpace = 0;

// 系列数

let series_count = this.options.data.Y.length;

// 每一条数据的宽度

let series_width = 0;

// 每一条数据的left值

let series_left = null;

// 根据系列数来调整样式

if (series_count < 3) {

series_width = 28;

freeSpace = (100 - (series_count * 30)) / 2;

series_left = 30;

} else if (series_count >= 3 && series_count < 6) {

series_width = 18;

freeSpace = (100 - (series_count * 20)) / 2;

series_left = 20;

} else {

series_width = 100 / (series_count - 1);

freeSpace = 100 / series_count;

series_left = 0;

}

let seriesHTML = '';

for (let j = 0; j < series_count; j++) {

// 边框颜色高亮

let borderColor = this.lightColor(this.options.color[j]);

let left = null;

if (series_left > 0) {

left = series_left * j + freeSpace;

} else {

left = freeSpace * j;

}

// 设置初始样式

setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + "{width:" + series_width + '%;background-color:' + this.options.color[j] + ';border-color:' + borderColor + ';left:' + left + '%;box-shadow:0 0 5px ' + this.options.color[j] + ';}', j);

// 设置悬浮样式

setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + ':hover{box-shadow:0 0 15px ' + this.options.color[j] + ';}');

seriesHTML += '<bar class="bar-' + (j + 1) + '"></bar>'

}

setStyle("ew-charts-zone > div[class*='zone-']{ width:" + series_unit + "%;margin-left:1%;margin-right:1%;}");

for (let i = 0; i < zoneLen; i++) {

chartsZoneHTML += "<div class='zone-" + (i + 1) + "'>" + seriesHTML + "</div>";

}

chartsZone.innerHTML = chartsZoneHTML;

let dataY = this.options.data.Y;

// 延迟设置高度

setTimeout(() => {

for (let k = 0; k < zoneLen; k++) {

for (let l = 0; l < series_count; l++) {

// 获取bar元素

const bar = chartsZone.children[k].children[l];

// 设置class类名,方便设置样式规则

bar.classList.add('dataId-' + (k + 1) + '-' + (l + 1));

// 设置值,方便后续的悬浮操作显示值

bar.setAttribute('data-value', dataY[l][keyValue][k]);

// 设置高度

setStyle('ew-charts-zone > div bar.dataId-' + (k + 1) + '-' + (l + 1) + '{height:' + (dataY[l][keyValue][k]) / currentMaxValue * 100 + '%;}', l);

}

}

// 绑定悬浮事件

let bar = this.$('ew-charts-zone div bar', false);

[].slice.call(bar).forEach((item) => {

item.onmouseenter = function () {

let value = this.getAttribute('data-value');

this.innerHTML = "<span class='animation'>" + value + '</span>';

}

item.onmouseleave = function () {

this.innerHTML = '';

}

})

}, 0);

}

最后就是完成图注部分了,如下所示:

/**

* 设置图注

*/

ewCharts.prototype.resetChartsLegend = function (dataLegend, setStyle) {

let legendHTML = "";

//图注数据的属性名

let keyName = Array.isArray(dataLegend.keyName) && dataLegend.keyName.length === 2 ? dataLegend.keyName[0] : 'label';

for (let i = 0, len = dataLegend.Y.length; i < len; i++) {

let borderColor = this.lightColor(this.options.color[i]);

setStyle("ew-charts-legend > i.leg-" + (i + 1) + "{ background:" + this.options.color[i] + ";border-color:" + borderColor + ";}", i);

legendHTML += "<i class='leg-" + (i + 1) + "'></i><span>" + dataLegend.Y[i][keyName] + "</span>";

}

this.$('ew-charts-legend').innerHTML = legendHTML;

}

接下来,调用这个封装好的函数,如下所示:

/**

* 功能:调用统计图表功能

**/

/************************************************/

/* DOM加载完毕后执行(多媒体资源尚未开始加载) */

/************************************************/

document.onreadystatechange = function(){

if(document.readyState == "interactive"){

let ewChart = new ewCharts({

el:"#weekCost",

color:["#07bc85","dd2345","#346578","#ff8654","#998213"],

data:{

X:['一月', '二月', '三月', '四月', '五月', '六月', '七月'],

Y:[

{

name: '直接访问',

data: [320, 332, 301, 334, 390, 330, 320]

},

{

name: '邮件营销',

data: [120, 132, 101, 134, 90, 230, 210]

},

{

name: '联盟广告',

data: [220, 182, 191, 234, 290, 330, 310]

},

{

name: '视频广告',

data: [150, 232, 201, 154, 190, 330, 410]

},

{

name: '搜索引擎',

data: [862, 1018, 964, 1026, 1679, 1600, 1570]

},

],

keyName:['name','data']

}

});

console.log(ewChart);

}

}

嗯,一款柱状图表就大功告成了,由于每一部分的功能我都做了注释,所以不需要做详解,如有问题欢迎联系我,如果发现bug,也欢迎提issue,项目地址为:my-web-projects,如有帮助,望不吝啬star

以上是 用JavaScript无canvas来完成一个柱状图表 的全部内容, 来源链接: utcz.com/a/31323.html

回到顶部