Public
Edited
Feb 25, 2023
Importers
Insert cell
Insert cell
Insert cell
class Chart {
constructor(){
this._width = 600;
this._height = 400;
this._margins = {top:30, left:30, right:30, bottom:30};
this._data = [];
this._scaleX = null;
this._scaleY = null;
this._colors = d3.scaleOrdinal(d3.schemeCategory10);
this._box = null;
this._svg = null;
this._body = null;
this._padding = {top:10, left:10, right:10, bottom:10};
}

width(w){
if (arguments.length === 0) return this._width;
this._width = w;
return this;
}

height(h){
if (arguments.length === 0) return this._height;
this._height = h;
return this;
}

margins(m){
if (arguments.length === 0) return this._margins;
this._margins = m;
return this;
}

data(d){
if (arguments.length === 0) return this._data;
this._data = d;
return this;
}

scaleX(x){
if (arguments.length === 0) return this._scaleX;
this._scaleX = x;
return this;
}

scaleY(y){
if (arguments.length === 0) return this._scaleY;
this._scaleY = y;
return this;
}

svg(s){
if (arguments.length === 0) return this._svg;
this._svg = s;
return this;
}

body(b){
if (arguments.length === 0) return this._body;
this._body = b;
return this;
}

box(b){
if (arguments.length === 0) return this._box;
this._box = b;
return this;
}

getBodyWidth(){
let width = this._width - this._margins.left - this._margins.right;
return width > 0 ? width : 0;
}

getBodyHeight(){
let height = this._height - this._margins.top - this._margins.bottom;
return height > 0 ? height : 0;
}

padding(p){
if (arguments.length === 0) return this._padding;
this._padding = p;
return this;
}

defineBodyClip(){

this._svg.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', this.getBodyWidth() + this._padding.left + this._padding.right)
.attr('height', this.getBodyHeight() + this._padding.top + this._padding.bottom)
.attr('x', -this._padding.left)
.attr('y', -this._padding.top);
}

render(){
return this;
}

bodyX(){
return this._margins.left;

}

bodyY(){
return this._margins.top;
}

renderBody(){
if (!this._body){
this._body = this._svg.append('g')
.attr('class', 'body')
.attr('transform', 'translate(' + this.bodyX() + ',' + this.bodyY() + ')')
.attr('clip-path', "url(#clip)");
}

this.render();
}

renderChart(box){
if (!this._box & !box){
this._box =d3.create('div').attr('class','box');
}

if (!this._svg){
this._svg = this._box.append('svg')
.attr('width', this._width)
.attr('height', this._height);
}

this.defineBodyClip();

this.renderBody();
return this
}
show(){
return this._box.node()
}
debounce(fn, time){
let timeId = null;
return function(e){
const context = this;
//const event = d3.event;
timeId && clearTimeout(timeId)
timeId = setTimeout(function(e){
//d3.event = event;
fn.apply(context, arguments);
}, time,e);
}
}

}


Insert cell
Insert cell
Insert cell
text=`date,money
Mon,120
Tue,200
Wed,150
Thu,80
Fri,70
Sat,110
Sun,130`
Insert cell
data=d3.csvParse(text,d3.autoType)
Insert cell
Insert cell
Insert cell
Insert cell
barChart=()=>{

/* ----------------------------配置参数------------------------ */
const chart = new Chart();
const config = {
barPadding: 0.15,
barColor: chart._colors(0),
margins: {top: 80, left: 80, bottom: 50, right: 80},
textColor: 'black',
gridColor: 'gray',
tickShowGrid: [60, 120, 180],
title: '直方图',
hoverColor: 'white',
animateDuration: 1000
}

chart.margins(config.margins);
/* ----------------------------尺度转换------------------------ */
chart.scaleX = d3.scaleBand()
.domain(data.map((d) => d.date))
.range([0, chart.getBodyWidth()])
.padding(config.barPadding);
chart.scaleY = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.money)])
.range([chart.getBodyHeight(), 0])
/* ----------------------------渲染柱形------------------------ */
chart.renderBars = function(){
let bars = chart.body().selectAll('rect')
.data(data);

bars.enter()
.append('rect')
.attr('class','bar')
.merge(bars)
.attr('x', (d) => chart.scaleX(d.date))
.attr('y', chart.scaleY(0))
.attr('width', chart.scaleX.bandwidth())
.attr('height', 0)
.attr('fill', config.barColor)
.transition().duration(config.animateDuration)
.attr('height', (d) => chart.getBodyHeight() - chart.scaleY(d.money))
.attr('y', (d) => chart.scaleY(d.money));
bars.exit()
.remove();
}

/* ----------------------------渲染坐标轴------------------------ */
chart.renderX = function(){
chart.svg().insert('g','.body')
.attr('transform', 'translate(' + chart.bodyX() + ',' + (chart.bodyY() + chart.getBodyHeight()) + ')')
.attr('class', 'xAxis')
.call(d3.axisBottom(chart.scaleX));
}

chart.renderY = function(){
chart.svg().insert('g','.body')
.attr('transform', 'translate(' + chart.bodyX() + ',' + chart.bodyY() + ')')
.attr('class', 'yAxis')
.call(d3.axisLeft(chart.scaleY));
}

chart.renderAxis = function(){
chart.renderX();
chart.renderY();
}

/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
this._svg.select('.xAxis').append('text')
.attr('class', 'axisText')
.attr('x', chart.getBodyWidth())
.attr('y', 0)
.attr('fill', config.textColor)
.attr('dy', 30)
.text('日期');

this._svg.select('.yAxis').append('text')
.attr('class', 'axisText')
.attr('x', 0)
.attr('y', 0)
.attr('fill', config.textColor)
.attr('transform', 'rotate(-90)')
.attr('dy', -40)
.attr('text-anchor','end')
.text('每日收入(元)');
}

/* ----------------------------渲染网格线------------------------ */
chart.renderGrid = function(){
this._svg.selectAll('.yAxis .tick')
.each(function(d){
if (config.tickShowGrid.indexOf(d) > -1){
d3.select(this).append('line')
.attr('class','grid')
.attr('stroke', config.gridColor)
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', chart.getBodyWidth())
.attr('y2', 0);
}
});
}

/* ----------------------------渲染图标题------------------------ */
chart.renderTitle = function(){
chart.svg().append('text')
.classed('title', true)
.attr('x', chart.width()/2)
.attr('y', 0)
.attr('dy', '2em')
.text(config.title)
.attr('fill', config.textColor)
.attr('text-anchor', 'middle')
.attr('stroke', config.textColor);

}

/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
//防抖函数

this._svg.selectAll('.bar')
//.on('mouseover', function(d){
.on('mouseover', function(e,d){
//const e = d3.event;
//const position = d3.mouse(chart.svg().node());
// const position=d3.pointer(e)
const position=[e.offsetX,e.offsetY]
d3.select(this)
.attr('fill', config.hoverColor);
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text('收入:' + d.money + '元');
})
.on('mouseleave', function(){
// const e = d3.event;
// d3.select(e.target)
d3.select(this)
.attr('fill', chart._colors(0));
d3.select('.tip').remove();
})
.on('mousemove', this.debounce(function(e){
// const position = d3.mouse(chart.svg().node());
// const position=d3.pointer(e)
const position=[e.offsetX,e.offsetY]
console.log(position)
d3.select('.tip')
.attr('x', position[0]+5)
.attr('y', position[1]-5);
},6)
);
}
chart.render = function(){

chart.renderAxis();

chart.renderText();

chart.renderGrid();

chart.renderBars();

chart.addMouseOn();

chart.renderTitle();
}

chart.renderChart();
return chart

}
Insert cell
Insert cell
function debounce(fn, time){
let timeId = null;
return function(e){
const context = this;
//const event = d3.event;
timeId && clearTimeout(timeId)
timeId = setTimeout(function(e){
//d3.event = event;
fn.apply(context, arguments);
}, time,e);
}
}
Insert cell
d3=require('d3@7')
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more