美文网首页
实验楼用户学习记录calendar-heatmap

实验楼用户学习记录calendar-heatmap

作者: Eternal丶星空 | 来源:发表于2019-03-01 16:01 被阅读3次
    • 依赖d3.js 以及 moment.js
    function calendarHeatmap() {
      // defaults
      var width = 750
      var height = 130
      var legendWidth = 150
      var selector = 'body'
      var SQUARE_LENGTH = 12
      var SQUARE_PADDING = 2
      var MONTH_LABEL_PADDING = 6
      var now = moment()
        .endOf('day')
        .toDate()
      var yearAgo = moment()
        .startOf('day')
        .subtract(1, 'year')
        .toDate()
      var startDate = null
      var data = []
      var max = null
      var colorRange = ['#D8E6E7', '#218380']
      var tooltipEnabled = true
      var tooltipUnit = 'contribution'
      var legendEnabled = true
      var onClick = null
      var weekStart = 0 //0 for Sunday, 1 for Monday
      var locale = {
        months: [
          'Jan',
          'Feb',
          'Mar',
          'Apr',
          'May',
          'Jun',
          'Jul',
          'Aug',
          'Sep',
          'Oct',
          'Nov',
          'Dec',
        ],
        days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
        No: 'No',
        on: 'on',
        Less: 'Less',
        More: 'More',
      }
    
      // setters and getters
      chart.data = function(value) {
        if (!arguments.length) {
          return date
        }
        date = value
        return chart
      }
    
      chart.max = function(value) {
        if (!arguments.length) {
          return max
        }
        max = value
        return chart
      }
    
      chart.selector = function(value) {
        if (!arguments.length) {
          return selector
        }
        selector = value
        return chart
      }
    
      chart.startDate = function(value) {
        if (!arguments.length) {
          return startDate
        }
        yearAgo = value
        now = moment(value)
          .endOf('day')
          .add(1, 'year')
          .toDate()
        return chart
      }
    
      chart.colorRange = function(value) {
        if (!arguments.length) {
          return colorRange
        }
        colorRange = value
        return chart
      }
    
      chart.tooltipEnabled = function(value) {
        if (!arguments.length) {
          return tooltipEnabled
        }
        tooltipEnabled = value
        return chart
      }
    
      chart.tooltipUnit = function(value) {
        if (!arguments.length) {
          return tooltipUnit
        }
        tooltipUnit = value
        return chart
      }
    
      chart.legendEnabled = function(value) {
        if (!arguments.length) {
          return legendEnabled
        }
        legendEnabled = value
        return chart
      }
    
      chart.onClick = function(value) {
        if (!arguments.length) {
          return onClick()
        }
        onClick = value
        return chart
      }
    
      chart.locale = function(value) {
        if (!arguments.length) {
          return locale
        }
        locale = value
        return chart
      }
    
      function chart() {
        d3.select(chart.selector())
          .selectAll('svg.calendar-heatmap')
          .remove() // remove the existing chart, if it exists
    
        var dateRange = d3.time.days(yearAgo, now) // generates an array of date objects within the specified range
        var monthRange = d3.time.months(
          moment(yearAgo)
            .startOf('month')
            .toDate(),
          now
        ) // it ignores the first month if the 1st date is after the start of the month
        var firstDate = moment(dateRange[0])
        if (max === null) {
          max = d3.max(chart.data(), function(d) {
            return d.count
          })
        } // max data value
    
        // color range
        var color = d3.scale
          .linear()
          .range(chart.colorRange())
          .domain([0, max])
        var tooltip
        var dayRects
    
        drawChart()
    
        function drawChart() {
          var svg = d3
            .select(chart.selector())
            .style('position', 'relative')
            .append('svg')
            .attr('width', width)
            .attr('class', 'calendar-heatmap')
            .attr('height', height)
            .style('padding', '0px')
    
          dayRects = svg.selectAll('.day-cell').data(dateRange) //  array of days for the last yr
    
          dayRects
            .enter()
            .append('rect')
            .attr('class', 'day-cell')
            .attr('width', SQUARE_LENGTH)
            .attr('height', SQUARE_LENGTH)
            .attr('fill', function(d) {
              let colorNum
              if (countForDate(d) === 0) {
                colorNum = 0
              } else if (30 > countForDate(d) && countForDate(d) > 0) {
                colorNum = 30
              } else if (60 > countForDate(d) && countForDate(d) >= 30) {
                colorNum = 40
              } else if (120 > countForDate(d) && countForDate(d) >= 60) {
                colorNum = 60
              } else if (countForDate(d) >= 120) {
                colorNum = 120
              }
              return color(colorNum)
            })
            .attr('x', function(d, i) {
              var cellDate = moment(d)
              var result =
                cellDate.week() -
                firstDate.week() +
                firstDate.weeksInYear() *
                  (cellDate.weekYear() - firstDate.weekYear())
              return result * (SQUARE_LENGTH + SQUARE_PADDING)
            })
            .attr('y', function(d, i) {
              return (
                MONTH_LABEL_PADDING +
                formatWeekday(d.getDay()) * (SQUARE_LENGTH + SQUARE_PADDING)
              )
            })
          if (typeof onClick === 'function') {
            dayRects.on('click', function(d) {
              var count = countForDate(d)
              onClick({ date: d, count: count })
            })
          }
          if (chart.tooltipEnabled()) {
            dayRects
              .on('mouseenter', function(d, i) {
                tooltip = d3
                  .select(chart.selector())
                  .append('div')
                  .attr('class', 'day-cell-tooltip')
                  .html(tooltipHTMLForDate(d))
                  .style('left', function() {
                    return Math.floor(i / 7) * SQUARE_LENGTH + 'px'
                  })
                  .style('top', function() {
                    return (
                      formatWeekday(d.getDay()) * SQUARE_LENGTH +
                      MONTH_LABEL_PADDING + 15 +
                      'px'
                    )
                  })
              })
              .on('mouseleave', function(d, i) {
                tooltip.remove()
              })
          }
    
          if (chart.legendEnabled()) {
            var colorRange = [color(0)]
            for (var i = 4; i > 0; i--) {
              colorRange.push(color(max / i))
            }
            var legendGroup = svg.append('g')
            legendGroup
              .selectAll('.calendar-heatmap-legend')
              .data(colorRange)
              .enter()
              .append('rect')
              .attr('class', 'calendar-heatmap-legend')
              .attr('width', SQUARE_LENGTH)
              .attr('height', SQUARE_LENGTH)
              .attr('x', function(d, i) {
                return width - legendWidth + (i + 1) * 13
              })
              .attr('y', height - SQUARE_PADDING * 6)
              .attr('fill', function(d) {
                return d
              })
              .on('mouseover', function(d, i) {
                legendTooltip = d3
                  .select(chart.selector())
                  .append('div')
                  .attr('class', 'day-legend-tooltip')
                  .html(legendTooltipHTMLForDate(i))
                  .style('left', function(d, i) {
                    return width - legendWidth + (i + 1) * 13 + 'px'
                  })
                  .style('top', function() {
                    return height - SQUARE_PADDING * 8 + 15 + 'px'
                  })
              })
              .on('mouseout', function(d, i) {
                legendTooltip.remove()
              })
    
            legendGroup
              .append('text')
              .attr(
                'class',
                'calendar-heatmap-legend-text calendar-heatmap-legend-text-less'
              )
              .attr('x', width - legendWidth - 13)
              .attr('y', height - SQUARE_PADDING)
              .text(locale.Less)
    
            legendGroup
              .append('text')
              .attr(
                'class',
                'calendar-heatmap-legend-text calendar-heatmap-legend-text-more'
              )
              .attr(
                'x',
                width - legendWidth + SQUARE_PADDING + (colorRange.length + 1) * 13
              )
              .attr('y', height - SQUARE_PADDING)
              .text(locale.More)
          }
    
          dayRects.exit().remove()
          var monthLabels = svg
            .selectAll('.month')
            .data(monthRange)
            .enter()
            .append('text')
            .attr('class', 'month-name')
            .style()
            .text(function(d) {
              return locale.months[d.getMonth()]
            })
            .attr('x', function(d, i) {
              var matchIndex = 0
              dateRange.find(function(element, index) {
                matchIndex = index + 5
                return (
                  moment(d).isSame(element, 'month') &&
                  moment(d).isSame(element, 'year')
                )
              })
    
              return Math.floor(matchIndex / 7) * (SQUARE_LENGTH + SQUARE_PADDING)
            })
            .attr('y', -5) // fix these to the top
    
          locale.days.forEach(function(day, index) {
            index = formatWeekday(index)
            if (index % 2) {
              svg
                .append('text')
                .attr('class', 'day-initial')
                .attr(
                  'transform',
                  'translate(-16,' +
                    (SQUARE_LENGTH + SQUARE_PADDING) * (index + 1) +
                    ')'
                )
                .style('text-anchor', 'middle')
                .attr('dy', '2')
                .text(day)
            }
          })
        }
    
        function pluralizedTooltipUnit(count) {
          if ('string' === typeof tooltipUnit) {
            return tooltipUnit
          }
          for (var i in tooltipUnit) {
            var _rule = tooltipUnit[i]
            var _min = _rule.min
            var _max = _rule.max || _rule.min
            _max = _max === 'Infinity' ? Infinity : _max
            if (count >= _min && count <= _max) {
              return _rule.unit
            }
          }
        }
    
        function tooltipHTMLForDate(d) {
          var dateStr = moment(d).format('YYYY-MM-DD')
          var count = countForDate(d)
          return (
            '<span><strong>' +
            (count ? count : 0) +
            ' ' +
            pluralizedTooltipUnit(count) +
            '</strong> ' +
            locale.on +
            ' ' +
            dateStr +
            '</span>'
          )
        }
    
        function legendTooltipHTMLForDate(num) {
          switch (num) {
            case 0:
              return '<span><strong>有效学习时间为0</strong></span>'
              break
            case 1:
              return '<span><strong>0min≤有效学习时间<30min</strong></span>'
              break
            case 2:
              return '<span><strong>30min≤有效学习时间<60min</strong></span>'
              break
            case 3:
              return '<span><strong>60min≤有效学习时间<120min</strong></span>'
              break
            case 4:
              return '<span><strong>有效学习时间为≥120min</strong></span>'
              break
            default:
              // statements_def
              break
          }
        }
    
        function countForDate(d) {
          var count = 0
          var match = chart.data().find(function(element, index) {
            return moment(element.date).isSame(d, 'day')
          })
          if (match) {
            count = match.count
          }
          return count
        }
    
        function formatWeekday(weekDay) {
          if (weekStart === 1) {
            if (weekDay === 0) {
              return 6
            } else {
              return weekDay - 1
            }
          }
          return weekDay
        }
    
        var daysOfChart = chart.data().map(function(day) {
          return day.date.toDateString()
        })
    
        dayRects
          .filter(function(d) {
            return daysOfChart.indexOf(d.toDateString()) > -1
          })
          .attr('fill', function(d, i) {
            let colorNum
            if (chart.data()[i].count === 0) {
              colorNum = 0
            } else if (30 > chart.data()[i].count && chart.data()[i].count > 0) {
              colorNum = 30
            } else if (chart.data()[i].count >= 30 && chart.data()[i].count < 60) {
              colorNum = 40
            } else if (120 > chart.data()[i].count && chart.data()[i].count >= 60) {
              colorNum = 60
            } else if (chart.data()[i].count >= 120) {
              colorNum = 120
            }
            return color(colorNum)
          })
      }
    
      return chart
    }
    
    // polyfill for Array.find() method
    /* jshint ignore:start */
    if (!Array.prototype.find) {
      Array.prototype.find = function(predicate) {
        if (this === null) {
          throw new TypeError('Array.prototype.find called on null or undefined')
        }
        if (typeof predicate !== 'function') {
          throw new TypeError('predicate must be a function')
        }
        var list = Object(this)
        var length = list.length >>> 0
        var thisArg = arguments[1]
        var value
    
        for (var i = 0; i < length; i++) {
          value = list[i]
          if (predicate.call(thisArg, value, i, list)) {
            return value
          }
        }
        return undefined
      }
    }
    /* jshint ignore:end */
    

    使用方法:

    <template>
      <div class="user-heatmap">
        <div class="user-record-head">
          <div class="user-record-title">
            我的学习记录
          </div>
          <div class="user-record-desc">
            <span>
              当前连续学习
              <strong>
                {{ userStudyRecord.current_studying_days }}
              </strong>
              天
            </span>
            <span>
              最大连续学习
              <strong>
                {{ userStudyRecord.max_studying_days }}
              </strong>
              天
            </span>
            <span>
              总学习天数
              <strong>
                {{ userStudyRecord.total_study_days }}
              </strong>
              天
            </span>
          </div>
        </div>
        <div
          id="calendar-heatmap"
          class="heatmap"
        />
        <div class="year">
          <i
            v-if="leftBtn"
            class="fa fa-caret-left"
            @click="preYear"
          />
          {{ startYear }}-{{ endYear }}
          <i
            v-if="rightBtn"
            class="fa fa-caret-right"
            @click="nextYear"
          />
        </div>
      </div>
    </template>
    <script>
    import { mapActions } from 'vuex'
    
    export default {
      props: {
        userStudyRecord: {
          type: Object,
          required: true,
        },
      },
      data() {
        return {
          startYear: '',
          endYear: '',
        }
      },
      computed: {
        query() {
          return this.$route.query
        },
      },
      async mounted() {
        this.initYear()
        this.getCalendarHeatmap(this.userStudyRecord.records, this.startYear)
      },
      methods: {
        ...mapActions('user', ['getUserStudyRecord']),
        // 记录表的基本配置
        getCalendarHeatmap(mapData, startTime) {
          const chartData = []
          for (let i = 0; i < mapData.length; i++) {
            mapData[i].date = moment(mapData[i].timestamp * 1000).toDate()
            chartData.push(mapData[i])
            startTime = moment(startTime).toDate()
            const heatmap = calendarHeatmap()
              .data(chartData)
              .selector('#calendar-heatmap')
              .startDate(startTime)
              .tooltipEnabled(true)
              .tooltipUnit('min')
              .max(120)
              .colorRange(['#eee', '#08bf91'])
            heatmap()
          }
        },
        initYear() {
          this.startYear = moment(
            moment(this.userStudyRecord.records[0].timestamp * 1000).toDate()
          ).get('year')
          this.endYear = moment(
            moment(
              this.userStudyRecord.records[this.userStudyRecord.records.length - 1]
                .timestamp * 1000
            ).toDate()
          ).get('year')
        },
      },
    }
    </script>
    <style lang="scss" scoped>
    .user-heatmap {
      position: relative;
      height: 240px;
      padding: 25px 27px;
      background: white;
      .user-record-head {
        .user-record-title {
          float: left;
          font-size: 16px;
          color: #565656;
        }
        .user-record-desc {
          float: right;
          font-size: 14px;
          color: #999;
          span {
            margin-left: 10px;
          }
        }
      }
      .heatmap {
        padding: 46px 0 0 15px;
      }
      .year {
        position: absolute;
        top: 182px;
        left: 44px;
        font-size: 14px;
        color: #a4a4a4;
        i {
          cursor: pointer;
        }
      }
    }
    </style>
    <style lang="scss">
    .user-heatmap {
      text.month-name,
      text.calendar-heatmap-legend-text,
      text.day-initial {
        font-size: 10px;
        fill: inherit;
        font-family: Helvetica, arial, 'Open Sans', sans-serif;
      }
      rect.day-cell:hover {
        stroke: #555555;
        stroke-width: 1px;
      }
      .day-cell-tooltip,
      .day-legend-tooltip {
        position: absolute;
        z-index: 9999;
        padding: 5px 9px;
        color: #bbbbbb;
        font-size: 12px;
        background: rgba(0, 0, 0, 0.85);
        border-radius: 3px;
        text-align: center;
      }
      .day-cell-tooltip > span,
      .day-legend-tooltip > span {
        display: inline-block;
        white-space: nowrap;
        font-family: Helvetica, arial, 'Open Sans', sans-serif;
      }
    
      .calendar-heatmap {
        box-sizing: initial;
      }
    }
    </style>
    

    相关文章

      网友评论

          本文标题:实验楼用户学习记录calendar-heatmap

          本文链接:https://www.haomeiwen.com/subject/pngduqtx.html