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