Html structure
<! DOCTYPEhtml>
<html lang="zh_CN">
<head>
<meta charset="UTF-8" />
<title>A histogram</title>
<style>
html {
height: 100%;
}
body {
height: 100%;
margin: 0;
}
#main {
width: 100%;
height: 100%;
background-color: antiquewhite;
}
#tip {
position: absolute;
margin-left: 10px;
margin-top: 30px;
line-height: 22px;
background-color: rgba(0.0.0.0.6);
padding: 4px 9px;
font-size: 13px;
color: #fff;
border-radius: 3px;
pointer-events: none;
display: none;
}
</style>
</head>
<body>
<div id="main"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script>
// ...
</script>
</body>
</html>
Copy the code
1. Necessary data
// X-axis data source
const categories=['html'.'css'.'js'];
// Y-axis data source
const source = [
[30.20.40].// The number of students
[40.30.50].// Employment number
];
/*dimensions Dimensions */
const dimensions=['Number of students'.'Employment'];
/ / palette
const color=['#c23531'.'#2f4554'.'#61a0a8'.'#d48265'.'#91c7ae'.'#749f83'.'#ca8622'.'#bda29a'.'#6e7074'.'# 546570'.'#c4ccd3'];
const width=600
const height=600
/* Optimize the data source * use the map method to traverse the data source * convert the data into objects: * {* rectData: column data, * rectInd: column index, * rectName: column name, * seriesInd: series index, * seriesName: seriesName *} **/
const source2=source.map((seriesData,seriesInd) = >{
const seriesName=dimensions[seriesInd]
return seriesData.map((rectData,rectInd) = >{
const rectName=categories[rectInd]
return {rectData,rectInd,rectName,seriesInd,seriesName}
})
})
Copy the code
2. Create SVG
const main=d3.select('#main')
const svg=main.append('svg')
.attr('version'.1.2)
.attr('xmlns'.'http://www.w3.org/2000/svg')
.attr('width'.'100%')
.attr('height'.'100%')
.attr('viewBox'.` 0 0${width} ${height}`)
Copy the code
3. Draw the X-axis
-
Create basic X-axis data
/* Count the number of categories len*/ const len=categories.length /* use range() to get the X-axis xChartData in the charting coordinate system based on the number of categories, such as [0,1,2]*/ const xChartData=d3.range(len) /* The starting and ending points of the X-axis in the pixel coordinates are xPixelRange, offset by 50*/ const xPixelRange=[50,width-50] Copy the code
-
Create the X scale
/* * scaleBand() : xScale * Domain () : xChartData * rangeRound() : rangeRound() : That is, the start and end bits of the pixel xPixelRange * */ const xScale=d3.scaleBand() .domain(xChartData) .rangeRound(xPixelRange) Copy the code
-
Create an X-axis object
/* Create a scale-down axis generator xAxisGenerator*/ using the axisBottom() method based on the scale xScale const xAxisGenerator=d3.axisBottom(xScale) Copy the code
-
Draw the X axis
/** Use the axis generator to draw the axis * Add the G object to the SVG append * Set the y position of the X axis with the transform attribute translate * Call the xAxisGenerator axis generator with the call() method, Generate axes * selectAll text text with selectAll() * set chart data to category data with text() * set font size with attr() * */ svg.append('g') .attr('transform'.`translate(0,${height-50}) `) .call(xAxisGenerator) .selectAll('text') .text(d= >categories[d]) .style('font-size'.'12px') Copy the code
4. Draw the Y-axis
-
Create Y-axis data
/** Calculate the extreme value of all data in the data source maxY * expand the data source with JS native method flat(), and then use Max () method to get the extreme value ** / const maxY=Math.max(... source.flat())/* Declare the starting and ending points of the Y-axis in the charting coordinate system yChartRange*/ const yChartRange=[0,maxY] /* Declare the starting and ending points of the Y-axis data in the pixel coordinate system yPixelRange*/ const yPixelRange=[height-50.50] Copy the code
-
Create the y-scale
YScale scaleLinear() yChartRange () scaleLinear() yScale That is, the start and end bits of the pixel, yPixelRange * */ const yScale=d3.scaleLinear() .domain(yChartRange) .range(yPixelRange) Copy the code
-
Create the Y-axis object
/* Create a left-graduated axis generator yAxisGenerator*/ using the axisLeft() method based on the scale yScale const yAxisGenerator=d3.axisLeft(yScale) Copy the code
-
To draw Y
/** Generate axis with axis generator * add g object to SVG append * set y axis x position with transform property translate * call xAxisGenerator axis generator with call() method, Generate the axes * set the font size with the style() method * */ svg.append('g') .attr('transform'.'translate(50 0)') .call(yAxisGenerator) .style('font-size'.'12px') Copy the code
5. Drawing area (histogram drawing)
-
Establish basic data
/* Get a class pixel width xBandW*/ from xScale's bandwidth() const xBandW=xScale.bandwidth() /* Get the number of series n*/ const n=source.length /* Divide the category width by the number of series to get the width of each series element in a category. ColW */ const colW=xBandW/n /* Count the number of colors in the palette colorLen*/ const colorLen=color.length Copy the code
-
Building a drawing area
/* create seriesObjs in SVG, create seriesObjs in SVG * add g object to append in SVG * selectAll() selectAll g elements. Instead, create a selection set object * bind the data source with the series of information to the series with the data() method * use join() to batch create g elements based on the data sources, one G represents a series, Each G element is then filled with three columns of a different class. * Set the series x pixel position using the Transform attribute Translate -- the column width multiplied by the series index * Based on the series index, take the color from the palette and use it as the fill color for all shapes in the series **/ const seriesObjs=svg.append('g') .selectAll('g') .data(source2)// Use the optimized data source .join('g') .attr('transform'.(seriesData,seriesInd) = >{ const seriesX=colW*seriesInd return `translate(${seriesX}`, 0) }) .attr('fill'.(seriesData,seriesInd) = >color[seriesInd%colorLen]) /* selectAll rect elements with seriesObjs selectAll(), Use to create selection set objects * bind data previously bound in each collection to the cylinder collection using the data() method * batch create rect elements based on each collection of data using join() * add item attributes to them using classed() method **/ const rects=seriesObjs.selectAll('rect') .data(seriesData= >seriesData) .join('rect') .classed('item'.true) /* Set the x pixel level of the cylinder * From the callback parameter to get the index of the cylinder in the current series rectInd, series index seriesInd * Based on the index of the cylinder in the current series rectInd, * Set column width to column width colW * Set column y pixel * Deconstruct column data from callback parameters * rectData based on column data rectData, RectData * rectData is deconstructed from the callback parameter. RectData * rectData is the height of the column **/ by subtracting the number of pixels calibrated to the actual data on the column from the 0 scale on the y axis rects .attr("x".({ rectData, rectInd }) = > xScale(rectInd)) .attr("width", colW) .attr("y".({ rectData }) = > yScale(rectData)) .attr("height".({ rectData }) = > yScale(0) - yScale(rectData)); Copy the code
-
Create the animation
/* The first keyframe - the initial state of the cylinder * y the pixel bit of the scale 0 on the y axis * height 0 * */ rects.attr('y'.() = >yScale(0)) .attr('height'.0) /* Second keyword - complete state of cylinder * Transition () to create tween animation * duration() animation time * delay animation delay * ease interpolation algorithm for tween animation, e.g. D3.easebounce, See https://github.com/d3/d3-ease * */ rects.transition() .duration(1000) .delay(({rectInd,seriesInd}) = >(seriesInd+rectInd)*300) .ease(d3.easeBounce) .attr('y'.({rectData}) = >yScale(rectData)) .attr('height'.({rectData}) = >yScale(0)-yScale(rectData)) Copy the code
6. Mouse event (mouse passing prompt)
-
Creating a prompt object
const tip = main.append("div").attr("id"."tip"); Copy the code
-
Add the mouse event to the bar chart
/* The mouseover event resolves the target object and mouse position from the first callback parameter in the event * the mouse position clientX,clientY resolves the data for the current cylinder from the second callback parameter in the event * the cylinder data rectData * the cylinder name RectName * seriesName * Based on mouse position and column information display prompt * style() set display to block * style() set left, top position * HTML () set the HTML content of the element **/ rects.on("mouseover".({ clientX, clientY }, { rectData, rectName, seriesName }) = > { tip.style("display"."block") .style("left".`${clientX}px`) .style("top".`${clientY}px`).html(` <div>${rectName}</div> <div>${seriesName}:${rectData}</div> `); });/* Mousemove * Settings prompt left, top positions **/ rects.on("mousemove".({ clientX, clientY }) = > { tip.style("left".`${clientX}px`).style("top".`${clientY}px`); }); /* Mouse over event mouseout * hide hint **/ rects.on("mouseout".() = > { tip.style("display"."none"); }); Copy the code
7. Slow follow
-
Create a class
/* EaseObj EaseObj * target EaseObj * FM current animation frame * POS drawing position * endPos target position * ratio movement ratio, e.g. 0.1 * _play whether to start to ease following **/ class EaseObj{ /* Constructor */ constructor(target){ this.target=target this.fm=0 this.pos={x:0.y:0} this.endPos={x:0.y:0} this.ratio=0.1 this._play=false } /* The value of the play property */ get play() {return play } /* The present value of the play property is not equal to the past value * When the present value is true * slow follow * update target object * continuous render * When the present value is false * remove animation frames, cancel continuous render **/ set play(val) {if(val! = =this._play){ if(val){ this.render() }else{ this.cancel() } this._play=val } } /* endPos * Update the style of the target object * continuous render **/ render(){ const {pos,endPos,ratio,target}=this pos.x+=(endPos.x-pos.x)*ratio pos.y+=(endPos.y-pos.y)*ratio target.style('left'.`${pos.x}px`) .style('top'.`${pos.y}px`) this.fm=requestAnimationFrame(() = >{ this.render() }) } /*cancel removes animation frames and cancels continuous rendering */ cancel(){ cancelAnimationFrame(this.fm) } } EaseTip instantiates the easeTip object */ const easeTip=new EaseObj(tip) Copy the code
-
Modifying mouse Events
/* The mouseover event resolves the target object and mouse position from the first callback parameter in the event * the mouse position clientX,clientY resolves the data for the current cylinder from the second callback parameter in the event * the cylinder data rectData * the cylinder name RectName * seriesName * Based on mouse position and column information display prompt * style() set display to block * HTML () Set the HTML content of the element * Set slow follow **/ rects.on('mouseover'.({clientX,clientY},{seriesName,rectName,rectData}) = >{ tip.style('display'.'block') .html(` <div>${seriesName}</div> <div>${rectName}:${rectData}</div> `) easeTip.endPos={x:clientX,y:clientY} easeTip.play=true }) /* Mousemove * Settings prompt left, top positions **/ rects.on("mousemove".({ clientX, clientY }) = > { easeTip.endPos={x:clientX,y:clientY} }); /* Mouse over event mouseout * hide hint **/ rects.on("mouseout".() = > { tip.style("display"."none"); easeTip.play=false }); Copy the code