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.