Files

450 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 对Date的扩展,将 Date 转化为指定格式的String
* 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
* 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* 例子:
* (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
* (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
* @param fmt 日期格式
*/
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
/**
* 字符串转换为日期对象
* @param dateStr Date 格式为yyyy-MM-dd HH:mm:ss,必须按年月日时分秒的顺序,中间分隔符不限制
*/
Date.prototype.strToDate = function (dateStr) {
var data = dateStr
var reCat = /(\d{1,4})/gm
var t = data.match(reCat)
t[1] = t[1] - 1
eval('var d = new Date(' + t.join(',') + ')')
return d
}
/**
* 获取日期列表,不传参默认当前时间为截止日期,去年的今天为起始日期
* @param dateStart 起始日期 格式为yyyy-MM-dd,必须按年月日的顺序,中间分隔符不限制
* @param dateEnd 截止日期 格式为yyyy-MM-dd,必须按年月日的顺序,中间分隔符不限制
*/
Date.prototype.getDateList = function (dateStart, dateEnd) {
try {
var date, diff, list = {}
// 缺一不可,缺任何一个都采取默认时间
if (!dateStart || !dateEnd) {
// 当前时间
date = new Date()
// 当前时间往前推一年
date.setFullYear(date.getFullYear() - 1)
// 如果去年的今天不是星期天的话,补充天数直到起始日期是星期天为止
while (date.getDay() > 0) {
date.setDate(date.getDate() - 1)
}
// 计算相差的天数
diff = parseInt((new Date().getTime() - date.getTime()) / (1000 * 60 * 60 * 24))
} else {
// 转换为日期对象
var start = this.strToDate(dateStart)
var end = this.strToDate(dateEnd)
// 补充天数直到起始日期是星期天为止
while (start.getDay() > 0) {
start.setDate(start.getDate() - 1)
}
// 计算相差的天数
diff = parseInt((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
date = start
}
// 实际上连最后一天也要算上,所以diff加2
for (i = 1; i < diff + 2; i++) {
list[date.format("yyyy-MM-dd")] = date.getDay()
date.setDate(date.getDate() + 1)
}
return list
} catch(e) {
return {}
}
}
var weekMap = ['周日','周一', '周二', '周三', '周四', '周五', '周六']
var monthMap = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
var colorMap = {
'blue': ['#003C9D', '#409EFF', '#87CEFA', '#E0FFFF', '#EBEDF0'],
'pink': ['#990099', '#CC00CC', '#FF88C2', '#FFB7DD', '#EBEDF0'],
'green': ['#196127', '#239A3B', '#7BC96F', '#C6E48B', '#EBEDF0'],
'orange': ['#A42D00', '#CC6600', '#EE7700', '#FFAA33', '#EBEDF0'],
'gray': ['#303133', '#444444', '#808080', '#C0C0C0', '#EBEDF0']
}
// 合并两个object,以mainObj为基准
function matchObj(mainObj, obj) {
var resultObj = {}
for (var key in mainObj) {
if (!obj.hasOwnProperty(key)) {
resultObj[key] = mainObj[key]
} else if (Object.prototype.toString.call(mainObj[key]) === '[Object Object]' && key !== 'data') {
resultObj[key] = matchObj(mainObj[key], obj[key])
} else {
resultObj[key] = obj[key]
}
}
return resultObj
}
function HeatMapDate() {
this.option = {
type: 'date', // 类型:date-日历型,custom-自定义型
xAxis: [], // 横坐标的labeltype=custom起作用
yAxis: [], // 纵坐标的labeltype=custom起作用
gap: 3, // 方格之间的间隔
/* 数据
* 如果type是date。那data是Object类型,格式是{ 'yyyy-MM-dd' : value }
* 如果type是custom。那data是Array类型,格式是[[x, y, value], ..., [x, y, value]]
*/
data: {},
rect: {
stroke: {
show: false,
background: '#333', // 正方形的边框颜色
opacity: 0.6 // 正方形的边框透明度
},
colourMatching: '', //配色方案,有custom-自定义和reen、pink、blue、orange、gray五种渐变色
backgroundArr: [] // 自定义配色方案,程度由重到轻
},
dateStart: '',
dateEnd: '',
min: 0, // 分级最低值,总共五个等级,不传默认值是0
max: 0, // 分级最高值,总共五个等级,不传默认值是
tip: { // 顶部鼠标悬浮小气泡
show: true, // 是否展示
/** 文本内容
* type为date的时候{a}表示日期,{b}表示数值
* type为custom的时候表示{x}x轴对应的值,{y}y轴对应的值,{b}表示数值
* 如果在替换字符串前加反斜杠(例如/{b}),则不会替换该字符串
*/
formatter: ''
}
}
// 初始化参数,没传的就使用默认值
this.setOption = (obj) => {
this.option = matchObj(this.option, obj)
if (!this.option.max) {
this.option.max = Object.values(obj.data).sort(function(a, b) {
return b - a
})[0] || 0
}
}
this.init = (dom) => {
// 初始化dom的样式
dom.setAttribute('style', 'width:170%;height:100%;position:relative;')
// 获取父级dom的宽度
var parentWidth = dom.offsetWidth
// 通过createElementNS创建svg元素并设置属性
var svg = document.createElementNS('http://www.w3.org/2000/svg','svg')
svg.setAttribute('version', '1.1')
svg.setAttribute('class', 'svg-container')
dom.appendChild(svg) // 挂载元素。SVG元素添加到页面内显示
// 显示顶部提示小气泡
if (this.option.tip.show) {
// 创建tip容器并设置属性
var tip = document.createElement('div')
tip.setAttribute('class', 'svg-tip svg-tip-one-line')
var title = document.createElement('strong')
tip.appendChild(title) // 挂载到父节点上
dom.appendChild(tip) // 挂载元素。挂载顶部提示气泡
}
// 创建svg的group元素并设置属性
var group = document.createElementNS('http://www.w3.org/2000/svg', 'g')
var translateX = 20 // 横轴方向的偏移值
var translateY = 40 // 纵轴方向的偏移值
// var translateX = 10 // 横轴方向的偏移值
// var translateY = 20 // 纵轴方向的偏移值
group.setAttribute('transform', 'translate(' + translateX + ',' + translateY + ')')
svg.appendChild(group) // 挂载到父节点上
var maxYLabelFontSize = 12
var labelPadding = 10
var maxStrLength = 0
if (this.option.type === 'date') {
// 获取全部天数列表
var dateList = (new Date()).getDateList(this.option.dateStart, this.option.dateEnd)
var columnCount = Math.ceil(Object.keys(dateList).length / 7)
var size = Math.floor((parentWidth - translateX / 2 - maxYLabelFontSize * 2 - labelPadding - this.option.gap * columnCount) / columnCount)
var section = size + this.option.gap
// 纵轴的label值,这里是星期值
for (var w = 0; w < 7; w++) {
//创建矩形元素并设置属性
var yText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
yText.style.fontSize = section * 0.7 > maxYLabelFontSize ? maxYLabelFontSize : section * 0.7 // 字体大小响应,最大是12px
yText.setAttribute('dx', -labelPadding)
yText.setAttribute('dy', w * section + size / 2 + 4)
yText.setAttribute('class', 'wday')
yText.innerHTML = weekMap[w]
group.appendChild(yText)
if (maxStrLength < yText.getBBox().width) {
maxStrLength = yText.getBBox().width
}
}
var index = 0 // 天数列表索引
var column = 0 // 分组索引,完整的一周为一组
for (var dateKey in dateList) {
// 完整的一周为一组
if (index === 0 || index % 7 === 0) {
// 创建svg的group元素并设置属性,一周为一组
var xGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g')
// 设置偏移值
xGroup.setAttribute('transform', 'translate(' + (column * section + maxStrLength) + ', 0)')
group.appendChild(xGroup) // 挂载到父节点
column++ // 递增组数索引
// 判断在哪个分组的上方增加横轴的label值,在这里是月份
if (index > 0) {
var startMonth = dateKey.split('-')[1]
var preStartMonth = Object.keys(dateList)[index - 7].split('-')[1]
if (Math.abs(Number(startMonth) - Number(preStartMonth)) > 0) {
//创建text元素并设置属性
var fontSize = section * 0.8 > maxYLabelFontSize ? maxYLabelFontSize : section * 0.8 // 字体大小响应,最大是14px
var xText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
xText.style.fontSize = fontSize
xText.setAttribute('x', column * section)
xText.setAttribute('y', -labelPadding)
xText.setAttribute('class', 'month')
xText.innerHTML = monthMap[Number(startMonth) - 1]
group.appendChild(xText)
}
}
}
if(this.option.data.hasOwnProperty(dateKey)) {
// 开始画正方形啦~创建矩形元素并设置属性
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', 0)
rect.setAttribute('y', dateList[dateKey] * section)
rect.setAttribute('id', dateKey) // 设置日期为id值
rect.setAttribute('week', dateList[dateKey]) // 设置星期几属性
rect.setAttribute('column', column) // 设置分组的组索引属性
rect.setAttribute('width', size)
rect.setAttribute('height', size)
color = '#fff' // 默认颜色是白色,就是啥也没有时候的颜色,以你的背景色为准
var colorSelect = ''
// 选择的颜色系列
if (this.option.rect.colourMatching === 'custom') {
colorSelect = this.option.rect.backgroundArr
} else {
colorSelect = colorMap[this.option.rect.colourMatching]
}
// 默认绿色为基本配色方案
colorSelect = colorSelect ? colorSelect : colorMap['green']
// 分为五个等级,小于最小值最低等级,大于最大值最高等级。中间还应该有三个等级(可自定义)
var levelGap = (this.option.max - this.option.min) / (colorSelect.length - 2)
if (this.option.data.hasOwnProperty(dateKey)) {
var differ = this.option.max - this.option.data[dateKey]
// 小正方形的颜色决定于他的值处在哪个level里头
switch(true) {
case (this.option.data[dateKey] >= this.option.max) :
color = colorSelect[0]
break
case (differ < levelGap) :
color = colorSelect[1]
break
case (differ < levelGap * 2) :
color = colorSelect[2]
break
case (differ < levelGap * 3) :
color = colorSelect[3]
break
default:
color = colorSelect[4]
}
// 设置小矩形的颜色
rect.setAttribute('style', 'fill:' + color)
// 显示顶部提示小气泡
if (this.option.tip.show) {
//矩形元素绑定鼠标事件实现动态效果
// 鼠标移入
rect.onmouseover = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 1)
e.srcElement.setAttribute('stroke', this.option.rect.stroke.background)
e.srcElement.setAttribute('stroke-opacity', this.option.rect.stroke.opacity)
}
// 提示小气泡的文本内容,默认==>日期:值
if (this.option.tip.formatter) {
var tipText = this.option.tip.formatter.replace(/(?<!\/){a}/g, e.target.id).replace(/(?<!\/){b}/g, this.option.data[e.target.id])
tip.innerHTML = tipText
} else {
tip.innerHTML = e.target.id + '' + this.option.data[e.target.id]
}
tip.style.display = 'block'
tip.style.top = (e.target.attributes.week.value * section + dom.querySelector('.svg-tip').offsetHeight / 2 - translateY / 2 - labelPadding) + 'px'
tip.style.left = ((e.target.attributes.column.value - 1) * section + size / 2 + translateX / 2 + maxStrLength + labelPadding - dom.querySelector('.svg-tip').offsetWidth / 2) + 'px'
}
// 鼠标移出
rect.onmouseout = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 0)
}
tip.style.display = 'none'
}
}
}
xGroup.appendChild(rect) //挂载矩形元素添加到小分组元素内
}
index++
}
// 设置svg元素的宽高
svg.style.width = section * columnCount + translateX / 2 + maxStrLength + labelPadding
svg.style.height = section * 7 + translateY
}
if (this.option.type === 'custom') {
var size = parseInt(parentWidth / this.option.xAxis.length) - this.option.gap * this.option.xAxis.length
var section = size + this.option.gap
var fontSize = section * 0.7 > maxYLabelFontSize ? maxYLabelFontSize : section * 0.7 // 字体大小响应,最大是12px
this.option.xAxis.forEach((xItem,xIndex) => {
var xGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g')
group.appendChild(xGroup)
this.option.yAxis.forEach((yItem,yIndex) => {
if (xIndex === 0) {
//创建矩形元素并设置属性
var yText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
yText.style.fontSize = fontSize
yText.setAttribute('dx', -labelPadding)
yText.setAttribute('dy', yIndex * section + size / 2 + 4)
yText.setAttribute('class', 'wday')
yText.innerHTML = yItem
group.appendChild(yText)
if (maxStrLength < yText.getBBox().width) {
maxStrLength = yText.getBBox().width
}
}
this.option.data.some((elem, i) => {
if (elem[0] === xIndex && elem[1] === yIndex) {
color = 'rgb(255, 255, 255)' // 默认颜色是白色,就是啥也没有时候的颜色,以你的背景色为准
var colorSelect = ''
// 选择的颜色系列
if (this.option.rect.colourMatching === 'custom') {
colorSelect = this.option.rect.backgroundArr
} else {
colorSelect = colorMap[this.option.rect.colourMatching]
}
// 默认绿色为基本配色方案
colorSelect = colorSelect ? colorSelect : colorMap['green']
// 分为五个等级,小于最小值最低等级,大于最大值最高等级。中间还应该有三个等级(可自定义)
var levelGap = (this.option.max - this.option.min) / (colorSelect.length - 2)
var differ = this.option.max - elem[2]
// 小正方形的颜色决定于他的值处在哪个level里头
switch(true) {
case (elem[2] >= this.option.max) :
color = colorSelect[0]
break
case (differ < levelGap) :
color = colorSelect[1]
break
case (differ < levelGap * 2) :
color = colorSelect[2]
break
case (differ < levelGap * 3) :
color = colorSelect[3]
break
default:
color = colorSelect[4]
}
//创建矩形元素并设置属性
var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', 0)
rect.setAttribute('y', elem[1] * section)
rect.setAttribute('id', elem[0] + '_' + elem[1]) // 设置日期为id值
rect.setAttribute('row', yIndex)
rect.setAttribute('column', xIndex)
rect.setAttribute('width', size)
rect.setAttribute('height', size)
rect.setAttribute('style', 'fill:' + color)
// 显示顶部提示小气泡
if (this.option.tip.show) {
//矩形元素绑定鼠标事件实现动态效果
// 鼠标移入
rect.onmouseover = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 1)
e.srcElement.setAttribute('stroke', this.option.rect.stroke.background)
e.srcElement.setAttribute('stroke-opacity', this.option.rect.stroke.opacity)
}
var xyArr = e.target.id.split('_')
// 提示小气泡的文本内容,默认==>x_y:值
if (this.option.tip.formatter) {
var tipText = this.option.tip.formatter.replace(/(?<!\/){x}/g, this.option.xAxis[xyArr[0]]).replace(/(?<!\/){y}/g, this.option.yAxis[xyArr[1]]).replace(/(?<!\/){b}/g, elem[2])
tip.innerHTML = tipText
} else {
tip.innerHTML = e.target.id + '' + elem[2]
}
tip.style.display = 'block'
tip.style.top = (e.target.attributes.row.value * section + dom.querySelector('.svg-tip').offsetHeight / 2 - translateY / 2 - labelPadding) + 'px'
tip.style.left = (e.target.attributes.column.value * section + size / 2 + translateX / 2 + maxStrLength + labelPadding - dom.querySelector('.svg-tip').offsetWidth / 2) + 'px'
}
// 鼠标移出
rect.onmouseout = (e) => {
if (this.option.rect.stroke.show) {
e.srcElement.setAttribute('stroke-width', 0)
}
tip.style.display = 'none'
}
}
//将矩形元素添加到SVG元素内
xGroup.appendChild(rect)
return true
} else {
return false
}
})
})
xGroup.setAttribute('transform', 'translate(' + (xIndex * section + maxStrLength) + ', 0)')
//创建text元素并设置属性
var xText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
xText.style.fontSize = fontSize
xText.setAttribute('x', xIndex * section + maxStrLength)
xText.setAttribute('y', -labelPadding)
xText.setAttribute('class', 'month')
xText.innerHTML = xItem
group.appendChild(xText)
xText.setAttribute('textLength', size < xText.getBBox().width ? size : xText.getBBox().width)
})
// 设置svg元素的宽高
svg.style.width = section * this.option.xAxis.length + translateX / 2 + labelPadding + maxStrLength
svg.style.height = section * this.option.yAxis.length + translateY
}
}
}