preface
When it comes to data visualization, echarts comes to mind, and the easiest chart to get started with is the bar chart. It was realized based on Canvas, and I was wondering if I could realize bar chart without canvas. After my exploration, I finally realized a bar chart.
Let’s take a look at a completed online example, as shown below:
Analysis of implementation ideas
First, we need to determine the parts of the bar chart. The first part has legend in the upper right corner, the second part has X and Y axis, and the third part is the bar chart. So now that we’ve identified the pieces, we’re pretty good to go. Okay, let’s get down to business.
Implement a static page structure
Write HTML
So far the finished product is already wrapped, and the page has only one container element. But we can’t write it like this at first. Let’s write a dead structure as follows:
<div id="weekCost" class="ew-charts">
<ew-charts-body>
<ew-charts-legend>
<i class="leg-1"></i>
<span>Direct access to the</span>
<i class="leg-2"></i>
<span>Email marketing</span>
<i class="leg-3"></i>
<span>Union advertising</span>
<i class="leg-4"></i>
<span>Video advertising</span>
<i class="leg-5"></i>
<span>Search engine</span>
</ew-charts-legend>
<ew-charts-x>
<div class="x-1" style="letter-spacing:2px;">January</div>
<div class="x-2" style="letter-spacing:2px;">February</div>
<div class="x-3" style="letter-spacing:2px;">March</div>
<div class="x-4" style="letter-spacing:2px;">April</div>
<div class="x-5" style="letter-spacing:2px;">May</div>
<div class="x-6" style="letter-spacing:2px;">June</div>
<div class="x-7" style="letter-spacing:2px;">July</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>
Copy the code
Write CSS
The next step is to add styles one by one, depending on the page elements. This is a slow process and needs to be done slowly.
/** * function: normal page style Settings **/
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/* Style initialization section */
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
* {
margin: 0;
padding: 0;
}
body.html {
height: 100%;
font: 20px Microsoft Yahei;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
overflow: hidden;
}
/* Convert to IE box model */*, *::before, *::after {
box-sizing: border-box;
}
/* Hand button */
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;
}
Copy the code
@charset "utf-8";
/** * Function: Statistics chart style **/
/**** Chart custom tag initialization section ****/
.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;
}
/* Chart container */
.ew-charts {
width: 100%;
height: 100%;
color: #f8f5fa;
background: linear-gradient(to right, #234, #789);
margin: auto;
color: #b3b3b3;
}
/ * * / table body
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;
}
/* Table data range */
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.6 s cubic-bezier(. 19, 55, 58, 1.3);/* Default value set */
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 "Founder yao body"."arial";
opacity: 0;
}
ew-charts-zone>div bar>span.animation {
animation: data-value-show 0.6 s forwards;
}
/ * note * /
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;
}
/* Graph animation section */
@keyframes data-value-show {
0% {
opacity: 0; 100%} {opacity: 1; }}Copy the code
Write js
First, we need to define a function for encapsulation.
function ewCharts(options) {
// The value of the color attribute is given by checking whether the passed parameter contains the color attribute
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'); }}// For later extensions, type bar is the default bar chart
options.type = options.type === "bar" ? options.type : "bar";
// Assign parameters to the instance
this.options = options;
// Start initialization
this.init(options);
}
Copy the code
Next, we can see that the page effect color is slightly highlighted, and then the highlight tool function that completes the color looks like this:
/** ** color highlights */
ewCharts.prototype.lightColor = function (color) {
// The color passed in is a hexadecimal color mode, such as # FFFFFF
let everyColorLight = function (lightColor) {
// Convert the incoming color to a hexadecimal number, then multiply by 1.6 to make the color 1.6 times brighter
const value = Math.round(parseInt(lightColor, 16) * 1.6);
// The value has a minimum value and a maximum value. If the value exceeds 255, it equals 255 and the minimum value cannot be less than 16
return (value >= 255 ? 255 : value <= 16 ? 16 : value).toString(16);
}
For example, #fef2f3, f2 represents the red range, F2 represents the green range, and F3 represents the blue range
return The '#' + everyColorLight(color.slice(1.3)) + everyColorLight(color.slice(3.5)) + everyColorLight(color.slice(5.7));
}
Copy the code
Then, we need to create a function that sets the style, as follows:
/** * Style rule set */
ewCharts.prototype.setStyle = function () {
// Check if the page contains a link tag, and if so, insert the style rule into the style sheet contained in that tag
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; }}https://www.w3school.com.cn/xmldom/met_cssstylesheet_insertrule.asp / / API documentation
return link[linkIndex].sheet.insertRule.bind(link[linkIndex].sheet);
}
Copy the code
We then wrap a function that gets the DOM element as follows:
/**, * get the DOM element */
ewCharts.prototype.$ = function (selector, isSingle) {
// If the element passed contains #, the only element to perform the querySelector method; otherwise, the method to perform the DOM query is determined by the Boolean passed in
isSingle = selector.indexOf(The '#') > - 1 ? true : typeof isSingle === 'boolean' ? isSingle : true;
return isSingle ? document.querySelector(selector) : document.querySelectorAll(selector);
}
Copy the code
We then complete the initialization function as follows:
/** * initializes */
ewCharts.prototype.init = function (options) {
// Set the style rules
let setStyle = this.setStyle();
// Figure type judgment, for later extension
switch (options.type) {
case "bar":
// Initialize all parts of the page diagram
this.resetAllCharts(this.$(options.el));
// Initialize the X-axis part
this.resetChartsX(options.data.X, setStyle);
// Initialize the Y part
this.resetChartsY(options.data.Y, setStyle);
// Initialize the annotation section
this.resetChartsLegend(options.data, setStyle);
break; }}Copy the code
Then, after completing the initialization of the page diagram structure, the previous page structure and CSS are written only, the page should retain only one container element, as shown below:
<div id="weekCost"></div>
Copy the code
Next, we add structure to the element as follows:
/** * Initializes the chart structure */
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>";
// Add a class name to the container element
el.classList.add('ew-charts');
return el;
}
Copy the code
Continue to initialize the X-axis as follows:
/** * Set X axis * X axis data * set style method */
ewCharts.prototype.resetChartsX = function (dataX, setStyle) {
let chartsX = this$('ew-charts-x'), chartsXHTML = ' ';
let dataXLen = dataX.length;
// Add the X-axis text element
for (let i = 0; i < dataXLen; i++) {
chartsXHTML += "<div class=x-" + (i + 1) + " style='letter-spacing:2px; '>" + dataX[i] + "</div>";
}
chartsX.innerHTML = chartsXHTML;
let chartsXContent = this$('ew-charts-x > div'.false), chartsXContentWidthArr = [];
// Sets the width of each element to the maximum width by getting an array of the elements' widths and finding the maximum width
for (let j = 0; j < dataXLen; j++) {
chartsXContentWidthArr.push(chartsXContent[j].offsetWidth);
}
// Maximum width and unit width and half of unit width
let maxWidth = Math.max.apply(null, chartsXContentWidthArr), unitWidth = parseInt(100 / dataXLen), half = unitWidth / 2;
for (let k = 0; k < dataXLen; k++) {
// Loop to set the element width and left offset of the X-axis data respectively
setStyle('ew-charts-x > div.x-' + (k + 1) + '{width:' + maxWidth + 'px; ' + 'left:calc(' + (unitWidth * (k + 1) - half) + '% - + half + 'px)}', k); }}Copy the code
The X-axis part has been completed, continue to complete the Y-axis part:
/** * set the Y axis */
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++) {
// Merge multiple arrays of values into one array
newDataValue = newDataValue.concat(dataY[i][keyValue]);
}
// Find the maximum value of the value array
let maxValue = Math.max.apply(null, newDataValue);
if (/ /. /.test(String(maxValue))) {
// If the maximum value has a decimal, round up
maxValue = Math.ceil(maxValue);
}
// Define the maximum number of segments and the current Y-axis
let subSections = null, currentMaxValue = null;
// each paragraph is divided according to 1,5,50,500,5000,50000 reference values
// The array that is currently used to judge the base value
let judgeMaxArr = [1000000.100000.10000.1000.100.10];
let currentJudgeValue = null;
for (let l = 0, length = judgeMaxArr.length; l < length; l++) {
// Exit the loop if the condition is met
if (maxValue >= judgeMaxArr[l]) {
currentJudgeValue = judgeMaxArr[l];
break; }}If currentValue is null, the default fragment value is set to 1
if(! currentJudgeValue) currentJudgeValue =1;
// Count the number of segments
subSections = currentJudgeValue > 1 ? Math.ceil(maxValue / (currentJudgeValue / 2)) : Math.ceil(maxValue / currentJudgeValue);
// Calculate the maximum value of the current Y axis
currentMaxValue = currentJudgeValue > 1 ? subSections * (currentJudgeValue / 2) : subSections * currentJudgeValue;
// Generate the Y-axis element based on the number of segments
for (let j = 0; j < subSections; j++) {
chartsYHTML += "<div class='y-" + (j + 1) + "' >" + (currentMaxValue / subSections) * (j + 1) + "</div>";
}
chartsY.innerHTML = chartsYHTML;
// Set CSS rules
for (let k = 0; k < subSections; k++) {
setStyle('ew-charts-y > div.y-' + (k + 1) + '{ bottom:calc(' + parseInt((100 / subSections) * (k + 1)) + '% - 16px); } ');
}
// Set the region
this.resetChartsZone(subSections, keyValue, currentMaxValue, setStyle);
}
Copy the code
The Y-axis part has also been completed, and the next part is to complete the bar chart, that is, the region part, as follows:
/** * sets the region */
ewCharts.prototype.resetChartsZone = function (subSections, keyValue, currentMaxValue, setStyle) {
// The overall background of the region
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 = ' ';
/ / set up a margin - because each 1% left and margin - right, so be minus 2
let series_unit = parseInt(100 / zoneLen) - 2;
// Set the remaining space
let freeSpace = 0;
/ / series number
let series_count = this.options.data.Y.length;
// The width of each data item
let series_width = 0;
// The left value of each data item
let series_left = null;
// Adjust the style according to the number of series
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++) {
// Highlight the border color
let borderColor = this.lightColor(this.options.color[j]);
let left = null;
if (series_left > 0) {
left = series_left * j + freeSpace;
} else {
left = freeSpace * j;
}
// Set the initial style
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);
// Set the hover style
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;
// Delay setting height
setTimeout((a)= > {
for (let k = 0; k < zoneLen; k++) {
for (let l = 0; l < series_count; l++) {
// Get the bar element
const bar = chartsZone.children[k].children[l];
// Set the class name for setting style rules
bar.classList.add('dataId-' + (k + 1) + The '-' + (l + 1));
// Set the value for subsequent hover operations to display the value
bar.setAttribute('data-value', dataY[l][keyValue][k]);
// Set the height
setStyle('ew-charts-zone > div bar.dataId-' + (k + 1) + The '-' + (l + 1) + '{height:' + (dataY[l][keyValue][k]) / currentMaxValue * 100 + '%; } ', l); }}// Bind suspension events
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);
}
Copy the code
Finally, it is time to complete the annotation, as shown below:
/** * set the annotation */
ewCharts.prototype.resetChartsLegend = function (dataLegend, setStyle) {
let legendHTML = "";
// The attribute name of the annotation data
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;
}
Copy the code
Next, call the wrapped function as follows:
/** * function: call statistics chart function **/
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/* Execute after DOM is loaded (multimedia resources have not yet started loading) */
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
document.onreadystatechange = function(){
if(document.readyState == "interactive") {let ewChart = new ewCharts({
el:"#weekCost".color: ["#07bc85"."dd2345"."# 346578"."#ff8654"."# 998213"].data: {X: ['一月'.'二月'.'march'.'in April'.'may'.'June'.'July'].Y:[
{
name: 'Direct access'.data: [320.332.301.334.390.330.320] {},name: 'Email marketing'.data: [120.132.101.134.90.230.210] {},name: 'Affiliate advertising'.data: [220.182.191.234.290.330.310] {},name: 'Video advertising'.data: [150.232.201.154.190.330.410] {},name: 'Search engines'.data: [862.1018.964.1026.1679.1600.1570]},],keyName: ['name'.'data']}});console.log(ewChart); }}Copy the code
Well, a bar chart is done, since I have annotated the functions of each part, so there is no need to elaborate. If you have any questions, please feel free to contact me. If you find any bugs, please also feel free to issue.