/*
 * charts for WeChat small app v1.0
 *
 * https://github.com/xiaolin3303/wx-charts
 * 2016-11-28
 *
 * Designed and built with all the love of Web
 */

const config = {
  yAxisWidth: 15,
  yAxisSplit: 5,
  xAxisHeight: 15,
  xAxisLineHeight: 15,
  legendHeight: 15,
  yAxisTitleWidth: 15,
  padding: 12,
  columePadding: 3,
  fontSize: 10,
  dataPointShape: ['diamond', 'circle', 'triangle', 'rect'],
  colors: ['#7cb5ec', '#f7a35c', '#434348', '#90ed7d', '#f15c80', '#8085e9'],
  pieChartLinePadding: 25,
  pieChartTextPadding: 15,
  xAxisTextPadding: 3,
  titleColor: '#333333',
  titleFontSize: 20,
  subtitleColor: '#999999',
  subtitleFontSize: 15,
  toolTipPadding: 3,
  toolTipBackground: '#000000',
  toolTipOpacity: 0.7,
  toolTipLineHeight: 14,
  radarGridCount: 3,
  radarLabelTextMargin: 15,
}

// Object.assign polyfill
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
function assign(target, varArgs) {
  if (target == null) {
    // TypeError if undefined or null
    throw new TypeError('Cannot convert undefined or null to object')
  }

  const to = Object(target)

  for (let index = 1; index < arguments.length; index++) {
    const nextSource = arguments[index]

    if (nextSource != null) {
      // Skip over if undefined or null
      for (const nextKey in nextSource) {
        // Avoid bugs when hasOwnProperty is shadowed
        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
          to[nextKey] = nextSource[nextKey]
        }
      }
    }
  }
  return to
}

const util = {
  toFixed: function toFixed(num, limit) {
    limit = limit || 2
    if (this.isFloat(num)) {
      num = num.toFixed(limit)
    }
    return num
  },
  isFloat: function isFloat(num) {
    return num % 1 !== 0
  },
  approximatelyEqual: function approximatelyEqual(num1, num2) {
    return Math.abs(num1 - num2) < 1e-10
  },
  isSameSign: function isSameSign(num1, num2) {
    return (
      (Math.abs(num1) === num1 && Math.abs(num2) === num2) ||
      (Math.abs(num1) !== num1 && Math.abs(num2) !== num2)
    )
  },
  isSameXCoordinateArea: function isSameXCoordinateArea(p1, p2) {
    return this.isSameSign(p1.x, p2.x)
  },
  isCollision: function isCollision(obj1, obj2) {
    obj1.end = {}
    obj1.end.x = obj1.start.x + obj1.width
    obj1.end.y = obj1.start.y - obj1.height
    obj2.end = {}
    obj2.end.x = obj2.start.x + obj2.width
    obj2.end.y = obj2.start.y - obj2.height
    const flag =
      obj2.start.x > obj1.end.x ||
      obj2.end.x < obj1.start.x ||
      obj2.end.y > obj1.start.y ||
      obj2.start.y < obj1.end.y

    return !flag
  },
}

function findRange(num, type, limit) {
  if (isNaN(num)) {
    throw new Error('[wxCharts] unvalid series data!')
  }
  limit = limit || 10
  type = type ? type : 'upper'
  let multiple = 1
  while (limit < 1) {
    limit *= 10
    multiple *= 10
  }
  if (type === 'upper') {
    num = Math.ceil(num * multiple)
  } else {
    num = Math.floor(num * multiple)
  }
  while (num % limit !== 0) {
    if (type === 'upper') {
      num++
    } else {
      num--
    }
  }

  return num / multiple
}

function calValidDistance(distance, chartData, config, opts) {
  const dataChartAreaWidth =
    opts.width - config.padding - chartData.xAxisPoints[0]
  const dataChartWidth = chartData.eachSpacing * opts.categories.length
  let validDistance = distance
  if (distance >= 0) {
    validDistance = 0
  } else if (Math.abs(distance) >= dataChartWidth - dataChartAreaWidth) {
    validDistance = dataChartAreaWidth - dataChartWidth
  }
  return validDistance
}

function isInAngleRange(angle, startAngle, endAngle) {
  function adjust(angle) {
    while (angle < 0) {
      angle += 2 * Math.PI
    }
    while (angle > 2 * Math.PI) {
      angle -= 2 * Math.PI
    }

    return angle
  }

  angle = adjust(angle)
  startAngle = adjust(startAngle)
  endAngle = adjust(endAngle)
  if (startAngle > endAngle) {
    endAngle += 2 * Math.PI
    if (angle < startAngle) {
      angle += 2 * Math.PI
    }
  }

  return angle >= startAngle && angle <= endAngle
}

function calRotateTranslate(x, y, h) {
  const xv = x
  const yv = h - y

  let transX = xv + (h - yv - xv) / Math.sqrt(2)
  transX *= -1

  const transY = (h - yv) * (Math.sqrt(2) - 1) - (h - yv - xv) / Math.sqrt(2)

  return {
    transX,
    transY,
  }
}

function createCurveControlPoints(points, i) {
  function isNotMiddlePoint(points, i) {
    if (points[i - 1] && points[i + 1]) {
      return (
        points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) ||
        points[i].y <= Math.min(points[i - 1].y, points[i + 1].y)
      )
    } else {
      return false
    }
  }

  const a = 0.2
  const b = 0.2
  let pAx = null
  let pAy = null
  let pBx = null
  let pBy = null
  if (i < 1) {
    pAx = points[0].x + (points[1].x - points[0].x) * a
    pAy = points[0].y + (points[1].y - points[0].y) * a
  } else {
    pAx = points[i].x + (points[i + 1].x - points[i - 1].x) * a
    pAy = points[i].y + (points[i + 1].y - points[i - 1].y) * a
  }

  if (i > points.length - 3) {
    const last = points.length - 1
    pBx = points[last].x - (points[last].x - points[last - 1].x) * b
    pBy = points[last].y - (points[last].y - points[last - 1].y) * b
  } else {
    pBx = points[i + 1].x - (points[i + 2].x - points[i].x) * b
    pBy = points[i + 1].y - (points[i + 2].y - points[i].y) * b
  }

  // fix issue https://github.com/xiaolin3303/wx-charts/issues/79
  if (isNotMiddlePoint(points, i + 1)) {
    pBy = points[i + 1].y
  }
  if (isNotMiddlePoint(points, i)) {
    pAy = points[i].y
  }

  return {
    ctrA: { x: pAx, y: pAy },
    ctrB: { x: pBx, y: pBy },
  }
}

function convertCoordinateOrigin(x, y, center) {
  return {
    x: center.x + x,
    y: center.y - y,
  }
}

function avoidCollision(obj, target) {
  if (target) {
    // is collision test
    while (util.isCollision(obj, target)) {
      if (obj.start.x > 0) {
        obj.start.y--
      } else if (obj.start.x < 0) {
        obj.start.y++
      } else if (obj.start.y > 0) {
        obj.start.y++
      } else {
        obj.start.y--
      }
    }
  }
  return obj
}

function fillSeriesColor(series, config) {
  let index = 0
  return series.map(item => {
    if (!item.color) {
      item.color = config.colors[index]
      index = (index + 1) % config.colors.length
    }
    return item
  })
}

function getDataRange(minData, maxData) {
  let limit = 0
  const range = maxData - minData
  if (range >= 10000) {
    limit = 1000
  } else if (range >= 1000) {
    limit = 100
  } else if (range >= 100) {
    limit = 10
  } else if (range >= 10) {
    limit = 5
  } else if (range >= 1) {
    limit = 1
  } else if (range >= 0.1) {
    limit = 0.1
  } else {
    limit = 0.01
  }
  return {
    minRange: findRange(minData, 'lower', limit),
    maxRange: findRange(maxData, 'upper', limit),
  }
}

function measureText(text) {
  const fontSize =
    arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10

  // wx canvas 未实现measureText方法, 此处自行实现
  text = String(text)
  var text = text.split('')
  let width = 0
  text.forEach(item => {
    if (/[a-zA-Z]/.test(item)) {
      width += 7
    } else if (/[0-9]/.test(item)) {
      width += 5.5
    } else if (/\./.test(item)) {
      width += 2.7
    } else if (/-/.test(item)) {
      width += 3.25
    } else if (/[\u4e00-\u9fa5]/.test(item)) {
      width += 10
    } else if (/\(|\)/.test(item)) {
      width += 3.73
    } else if (/\s/.test(item)) {
      width += 2.5
    } else if (/%/.test(item)) {
      width += 8
    } else {
      width += 10
    }
  })
  return width * fontSize / 10
}

function dataCombine(series) {
  return series.reduce((a, b) => (a.data ? a.data : a).concat(b.data), [])
}

function getSeriesDataItem(series, index) {
  const data = []
  series.forEach(item => {
    if (item.data[index] !== null && typeof item.data[index] !== 'undefined') {
      const seriesItem = {}
      seriesItem.color = item.color
      seriesItem.name = item.name
      seriesItem.data = item.format
        ? item.format(item.data[index])
        : item.data[index]
      data.push(seriesItem)
    }
  })

  return data
}

function getMaxTextListLength(list) {
  const lengthList = list.map(item => measureText(item))
  return Math.max.apply(null, lengthList)
}

function getRadarCoordinateSeries(length) {
  const eachAngle = 2 * Math.PI / length
  const CoordinateSeries = []
  for (let i = 0; i < length; i++) {
    CoordinateSeries.push(eachAngle * i)
  }

  return CoordinateSeries.map(item => -1 * item + Math.PI / 2)
}

function getToolTipData(seriesData, calPoints, index, categories) {
  const option =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}

  const textList = seriesData.map(item => ({
    text: option.format
      ? option.format(item, categories[index])
      : `${item.name}: ${item.data}`,
    color: item.color,
  }))
  const validCalPoints = []
  const offset = {
    x: 0,
    y: 0,
  }
  calPoints.forEach(points => {
    if (typeof points[index] !== 'undefined' && points[index] !== null) {
      validCalPoints.push(points[index])
    }
  })
  validCalPoints.forEach(item => {
    offset.x = Math.round(item.x)
    offset.y += item.y
  })

  offset.y /= validCalPoints.length
  return { textList, offset }
}

function findCurrentIndex(currentPoints, xAxisPoints, opts, config) {
  const offset =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0

  let currentIndex = -1
  if (isInExactChartArea(currentPoints, opts, config)) {
    xAxisPoints.forEach((item, index) => {
      if (currentPoints.x + offset > item) {
        currentIndex = index
      }
    })
  }

  return currentIndex
}

function isInExactChartArea(currentPoints, opts, config) {
  return (
    currentPoints.x < opts.width - config.padding &&
    currentPoints.x >
      config.padding + config.yAxisWidth + config.yAxisTitleWidth &&
    currentPoints.y > config.padding &&
    currentPoints.y <
      opts.height - config.legendHeight - config.xAxisHeight - config.padding
  )
}

function findRadarChartCurrentIndex(currentPoints, radarData, count) {
  const eachAngleArea = 2 * Math.PI / count
  let currentIndex = -1
  if (
    isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)
  ) {
    const fixAngle = function fixAngle(angle) {
      if (angle < 0) {
        angle += 2 * Math.PI
      }
      if (angle > 2 * Math.PI) {
        angle -= 2 * Math.PI
      }
      return angle
    }

    let angle = Math.atan2(
      radarData.center.y - currentPoints.y,
      currentPoints.x - radarData.center.x,
    )
    angle = -1 * angle
    if (angle < 0) {
      angle += 2 * Math.PI
    }

    const angleList = radarData.angleList.map(item => {
      item = fixAngle(-1 * item)

      return item
    })

    angleList.forEach((item, index) => {
      const rangeStart = fixAngle(item - eachAngleArea / 2)
      let rangeEnd = fixAngle(item + eachAngleArea / 2)
      if (rangeEnd < rangeStart) {
        rangeEnd += 2 * Math.PI
      }
      if (
        (angle >= rangeStart && angle <= rangeEnd) ||
        (angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd)
      ) {
        currentIndex = index
      }
    })
  }

  return currentIndex
}

function findPieChartCurrentIndex(currentPoints, pieData) {
  let currentIndex = -1
  if (isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) {
    let angle = Math.atan2(
      pieData.center.y - currentPoints.y,
      currentPoints.x - pieData.center.x,
    )
    angle = -angle
    for (let i = 0, len = pieData.series.length; i < len; i++) {
      const item = pieData.series[i]
      if (
        isInAngleRange(
          angle,
          item._start_,
          item._start_ + item._proportion_ * 2 * Math.PI,
        )
      ) {
        currentIndex = i
        break
      }
    }
  }

  return currentIndex
}

function isInExactPieChartArea(currentPoints, center, radius) {
  return (
    Math.pow(currentPoints.x - center.x, 2) +
      Math.pow(currentPoints.y - center.y, 2) <=
    Math.pow(radius, 2)
  )
}

function splitPoints(points) {
  const newPoints = []
  let items = []
  points.forEach((item, index) => {
    if (item !== null) {
      items.push(item)
    } else {
      if (items.length) {
        newPoints.push(items)
      }
      items = []
    }
  })
  if (items.length) {
    newPoints.push(items)
  }

  return newPoints
}

function calLegendData(series, opts, config) {
  if (opts.legend === false) {
    return {
      legendList: [],
      legendHeight: 0,
    }
  }
  const padding = 5
  const marginTop = 8
  const shapeWidth = 15
  const legendList = []
  let widthCount = 0
  let currentRow = []
  series.forEach(item => {
    const itemWidth =
      3 * padding + shapeWidth + measureText(item.name || 'undefined')
    if (widthCount + itemWidth > opts.width) {
      legendList.push(currentRow)
      widthCount = itemWidth
      currentRow = [item]
    } else {
      widthCount += itemWidth
      currentRow.push(item)
    }
  })
  if (currentRow.length) {
    legendList.push(currentRow)
  }

  return {
    legendList,
    legendHeight: legendList.length * (config.fontSize + marginTop) + padding,
  }
}

function calCategoriesData(categories, opts, config) {
  const result = {
    angle: 0,
    xAxisHeight: config.xAxisHeight,
  }

  let _getXAxisPoints = getXAxisPoints(categories, opts, config),
    eachSpacing = _getXAxisPoints.eachSpacing

  // get max length of categories text

  const categoriesTextLenth = categories.map(item => measureText(item))

  const maxTextLength = Math.max.apply(this, categoriesTextLenth)

  if (
    !opts.xAxis.noRotate &&
    maxTextLength + 2 * config.xAxisTextPadding > eachSpacing
  ) {
    result.angle = 45 * Math.PI / 180
    result.xAxisHeight =
      2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle)
  }

  return result
}

function getRadarDataPoints(angleList, center, radius, series, opts) {
  const process =
    arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1

  const radarOption = opts.extra.radar || {}
  radarOption.max = radarOption.max || 0
  const maxData = Math.max(
    radarOption.max,
    Math.max.apply(null, dataCombine(series)),
  )

  const data = []
  series.forEach(each => {
    const listItem = {}
    listItem.color = each.color
    listItem.data = []
    each.data.forEach((item, index) => {
      const tmp = {}
      tmp.angle = angleList[index]

      tmp.proportion = item / maxData
      tmp.position = convertCoordinateOrigin(
        radius * tmp.proportion * process * Math.cos(tmp.angle),
        radius * tmp.proportion * process * Math.sin(tmp.angle),
        center,
      )
      listItem.data.push(tmp)
    })

    data.push(listItem)
  })

  return data
}

function getPieDataPoints(series) {
  const process =
    arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1

  let count = 0
  let _start_ = 0
  series.forEach(item => {
    item.data = item.data === null ? 0 : item.data
    count += item.data
  })
  series.forEach(item => {
    item.data = item.data === null ? 0 : item.data
    item._proportion_ = item.data / count * process
  })
  series.forEach(item => {
    item._start_ = _start_
    _start_ += 2 * item._proportion_ * Math.PI
  })

  return series
}

function getPieTextMaxLength(series) {
  series = getPieDataPoints(series)
  let maxLength = 0
  series.forEach(item => {
    const text = item.format
      ? item.format(Number(item._proportion_.toFixed(2)))
      : `${util.toFixed(item._proportion_ * 100)}%`
    maxLength = Math.max(maxLength, measureText(text))
  })

  return maxLength
}

function fixColumeData(points, eachSpacing, columnLen, index, config, opts) {
  return points.map(item => {
    if (item === null) {
      return null
    }
    item.width = (eachSpacing - 2 * config.columePadding) / columnLen

    if (
      opts.extra.column &&
      opts.extra.column.width &&
      Number(opts.extra.column.width) > 0
    ) {
      // customer column width
      item.width = Math.min(item.width, Number(opts.extra.column.width))
    } else {
      // default width should less tran 25px
      // don't ask me why, I don't know
      item.width = Math.min(item.width, 25)
    }
    item.x += (index + 0.5 - columnLen / 2) * item.width

    return item
  })
}

function getXAxisPoints(categories, opts, config) {
  const yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
  const spacingValid = opts.justified
    ? opts.width
    : opts.width - 2 * config.padding - yAxisTotalWidth
  const dataCount = opts.enableScroll
    ? Math.min(5, categories.length)
    : categories.length
  const eachSpacing = spacingValid / dataCount

  const xAxisPoints = []
  const startX = opts.justified
    ? config.padding
    : config.padding + yAxisTotalWidth
  const endX = opts.width - config.padding
  categories.forEach((item, index) => {
    xAxisPoints.push(startX + index * eachSpacing)
  })
  if (opts.enableScroll === true) {
    xAxisPoints.push(startX + categories.length * eachSpacing)
  } else {
    xAxisPoints.push(endX)
  }

  return {
    xAxisPoints,
    startX,
    endX,
    eachSpacing,
  }
}

function getDataPoints(
  data,
  minRange,
  maxRange,
  xAxisPoints,
  eachSpacing,
  opts,
  config,
) {
  const process =
    arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1

  const points = []
  const validHeight =
    opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
  data.forEach((item, index) => {
    if (item === null) {
      points.push(null)
    } else {
      const point = {}
      point.x = xAxisPoints[index] + Math.round(eachSpacing / 2)
      let height = validHeight * (item - minRange) / (maxRange - minRange)
      height *= process
      point.y =
        opts.height -
        config.xAxisHeight -
        config.legendHeight -
        Math.round(height) -
        config.padding
      points.push(point)
    }
  })

  return points
}

function getYAxisTextList(series, opts, config) {
  let data = dataCombine(series)
  // remove null from data
  data = data.filter(item => item !== null)
  let minData = Math.min.apply(this, data)
  let maxData = Math.max.apply(this, data)
  if (typeof opts.yAxis.min === 'number') {
    minData = Math.min(opts.yAxis.min, minData)
  }
  if (typeof opts.yAxis.max === 'number') {
    maxData = Math.max(opts.yAxis.max, maxData)
  }

  // fix issue https://github.com/xiaolin3303/wx-charts/issues/9
  if (minData === maxData) {
    const rangeSpan = maxData || 1
    // when all the data is 0 , set minData = 0
    if (!maxData) {
      minData = 0
      maxData = 1
    } else {
      minData -= rangeSpan
      maxData += rangeSpan
    }
  }

  const dataRange = getDataRange(minData, maxData)
  const minRange = dataRange.minRange
  const maxRange = dataRange.maxRange

  const range = []
  const eachRange = (maxRange - minRange) / config.yAxisSplit

  for (let i = 0; i <= config.yAxisSplit; i++) {
    range.push(minRange + eachRange * i)
  }
  return range.reverse()
}

function calYAxisData(series, opts, config) {
  const ranges = getYAxisTextList(series, opts, config)
  let yAxisWidth = config.yAxisWidth
  const rangesFormat = ranges.map(item => {
    item = util.toFixed(item, 2)
    item = opts.yAxis.format ? opts.yAxis.format(Number(item)) : item
    yAxisWidth = Math.max(yAxisWidth, measureText(item) + 5)
    return item
  })
  if (opts.yAxis.disabled === true) {
    yAxisWidth = 0
  }

  return { rangesFormat, ranges, yAxisWidth }
}

function drawPointShape(points, color, shape, context) {
  context.beginPath()
  context.setStrokeStyle('#ffffff')
  context.setLineWidth(1)
  context.setFillStyle(color)

  if (shape === 'diamond') {
    points.forEach((item, index) => {
      if (item !== null) {
        context.moveTo(item.x, item.y - 4.5)
        context.lineTo(item.x - 4.5, item.y)
        context.lineTo(item.x, item.y + 4.5)
        context.lineTo(item.x + 4.5, item.y)
        context.lineTo(item.x, item.y - 4.5)
      }
    })
  } else if (shape === 'circle') {
    points.forEach((item, index) => {
      if (item !== null) {
        context.moveTo(item.x + 3.5, item.y)
        context.arc(item.x, item.y, 4, 0, 2 * Math.PI, false)
      }
    })
  } else if (shape === 'rect') {
    points.forEach((item, index) => {
      if (item !== null) {
        context.moveTo(item.x - 3.5, item.y - 3.5)
        context.rect(item.x - 3.5, item.y - 3.5, 7, 7)
      }
    })
  } else if (shape === 'triangle') {
    points.forEach((item, index) => {
      if (item !== null) {
        context.moveTo(item.x, item.y - 4.5)
        context.lineTo(item.x - 4.5, item.y + 4.5)
        context.lineTo(item.x + 4.5, item.y + 4.5)
        context.lineTo(item.x, item.y - 4.5)
      }
    })
  }
  context.closePath()
  context.fill()
  context.stroke()
}

function drawRingTitle(opts, config, context) {
  const titlefontSize = opts.title.fontSize || config.titleFontSize
  const subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize
  const title = opts.title.name || ''
  const subtitle = opts.subtitle.name || ''
  const titleFontColor = opts.title.color || config.titleColor
  const subtitleFontColor = opts.subtitle.color || config.subtitleColor
  const titleHeight = title ? titlefontSize : 0
  const subtitleHeight = subtitle ? subtitlefontSize : 0
  const margin = 5
  if (subtitle) {
    const textWidth = measureText(subtitle, subtitlefontSize)
    const startX = (opts.width - textWidth) / 2 + (opts.subtitle.offsetX || 0)
    let startY = (opts.height - config.legendHeight + subtitlefontSize) / 2
    if (title) {
      startY -= (titleHeight + margin) / 2
    }
    context.beginPath()
    context.setFontSize(subtitlefontSize)
    context.setFillStyle(subtitleFontColor)
    context.fillText(subtitle, startX, startY)
    context.stroke()
    context.closePath()
  }
  if (title) {
    const _textWidth = measureText(title, titlefontSize)
    const _startX = (opts.width - _textWidth) / 2 + (opts.title.offsetX || 0)
    let _startY = (opts.height - config.legendHeight + titlefontSize) / 2
    if (subtitle) {
      _startY += (subtitleHeight + margin) / 2
    }
    context.beginPath()
    context.setFontSize(titlefontSize)
    context.setFillStyle(titleFontColor)
    context.fillText(title, _startX, _startY)
    context.stroke()
    context.closePath()
  }
}

function drawPointText(points, series, config, context) {
  // 绘制数据文案
  const data = series.data

  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle('#666666')
  points.forEach((item, index) => {
    if (item !== null) {
      const formatVal = series.format ? series.format(data[index]) : data[index]
      context.fillText(
        formatVal,
        item.x - measureText(formatVal) / 2,
        item.y - 2,
      )
    }
  })
  context.closePath()
  context.stroke()
}

function drawRadarLabel(
  angleList,
  radius,
  centerPosition,
  opts,
  config,
  context,
) {
  const radarOption = opts.extra.radar || {}
  radius += config.radarLabelTextMargin
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle(radarOption.labelColor || '#666666')
  angleList.forEach((angle, index) => {
    const pos = {
      x: radius * Math.cos(angle),
      y: radius * Math.sin(angle),
    }
    const posRelativeCanvas = convertCoordinateOrigin(
      pos.x,
      pos.y,
      centerPosition,
    )
    let startX = posRelativeCanvas.x
    const startY = posRelativeCanvas.y
    if (util.approximatelyEqual(pos.x, 0)) {
      startX -= measureText(opts.categories[index] || '') / 2
    } else if (pos.x < 0) {
      startX -= measureText(opts.categories[index] || '')
    }
    context.fillText(
      opts.categories[index] || '',
      startX,
      startY + config.fontSize / 2,
    )
  })
  context.stroke()
  context.closePath()
}

function drawPieText(series, opts, config, context, radius, center) {
  const lineRadius = radius + config.pieChartLinePadding
  const textObjectCollection = []
  let lastTextObject = null

  const seriesConvert = series.map(item => {
    const arc =
      2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2)
    const text = item.format
      ? item.format(Number(item._proportion_.toFixed(2)))
      : `${util.toFixed(item._proportion_ * 100)}%`
    const color = item.color
    return { arc, text, color }
  })
  seriesConvert.forEach(item => {
    // line end
    const orginX1 = Math.cos(item.arc) * lineRadius
    const orginY1 = Math.sin(item.arc) * lineRadius

    // line start
    const orginX2 = Math.cos(item.arc) * radius
    const orginY2 = Math.sin(item.arc) * radius

    // text start
    let orginX3 =
      orginX1 >= 0
        ? orginX1 + config.pieChartTextPadding
        : orginX1 - config.pieChartTextPadding
    const orginY3 = orginY1

    const textWidth = measureText(item.text)
    let startY = orginY3

    if (
      lastTextObject &&
      util.isSameXCoordinateArea(lastTextObject.start, { x: orginX3 })
    ) {
      if (orginX3 > 0) {
        startY = Math.min(orginY3, lastTextObject.start.y)
      } else if (orginX1 < 0) {
        startY = Math.max(orginY3, lastTextObject.start.y)
      } else if (orginY3 > 0) {
        startY = Math.max(orginY3, lastTextObject.start.y)
      } else {
        startY = Math.min(orginY3, lastTextObject.start.y)
      }
    }

    if (orginX3 < 0) {
      orginX3 -= textWidth
    }

    const textObject = {
      lineStart: {
        x: orginX2,
        y: orginY2,
      },
      lineEnd: {
        x: orginX1,
        y: orginY1,
      },
      start: {
        x: orginX3,
        y: startY,
      },
      width: textWidth,
      height: config.fontSize,
      text: item.text,
      color: item.color,
    }

    lastTextObject = avoidCollision(textObject, lastTextObject)
    textObjectCollection.push(lastTextObject)
  })

  textObjectCollection.forEach(item => {
    const lineStartPoistion = convertCoordinateOrigin(
      item.lineStart.x,
      item.lineStart.y,
      center,
    )
    const lineEndPoistion = convertCoordinateOrigin(
      item.lineEnd.x,
      item.lineEnd.y,
      center,
    )
    const textPosition = convertCoordinateOrigin(
      item.start.x,
      item.start.y,
      center,
    )
    context.setLineWidth(1)
    context.setFontSize(config.fontSize)
    context.beginPath()
    context.setStrokeStyle(item.color)
    context.setFillStyle(item.color)
    context.moveTo(lineStartPoistion.x, lineStartPoistion.y)
    const curveStartX =
      item.start.x < 0 ? textPosition.x + item.width : textPosition.x
    const textStartX =
      item.start.x < 0 ? textPosition.x - 10 : textPosition.x + 10
    context.quadraticCurveTo(
      lineEndPoistion.x,
      lineEndPoistion.y,
      curveStartX,
      textPosition.y,
    )
    context.moveTo(lineStartPoistion.x, lineStartPoistion.y)
    context.stroke()
    context.closePath()
    context.beginPath()
    context.moveTo(textPosition.x + item.width, textPosition.y)
    context.arc(curveStartX, textPosition.y, 2, 0, 2 * Math.PI)
    context.closePath()
    context.fill()
    context.beginPath()
    context.setFontSize(opts.fontSize)
    context.setFillStyle(item.color || '#666666')
    context.fillText(item.text, textStartX - 6, textPosition.y + 5)
    context.closePath()
    context.stroke()

    context.closePath()
  })
}

function drawToolTipSplitLine(offsetX, opts, config, context) {
  const startY = config.padding
  const endY =
    opts.height - config.padding - config.xAxisHeight - config.legendHeight
  context.beginPath()
  context.setStrokeStyle('#cccccc')
  context.setLineWidth(1)
  context.moveTo(offsetX, startY)
  context.lineTo(offsetX, endY)
  context.stroke()
  context.closePath()
}

function drawToolTip(textList, offset, opts, config, context) {
  const legendWidth = 4
  const legendMarginRight = 5
  const arrowWidth = 8
  let isOverRightBorder = false
  offset = assign(
    {
      x: 0,
      y: 0,
    },
    offset,
  )
  offset.y -= 8
  const textWidth = textList.map(item => measureText(item.text))

  const toolTipWidth =
    legendWidth +
    legendMarginRight +
    4 * config.toolTipPadding +
    Math.max.apply(null, textWidth)
  const toolTipHeight =
    2 * config.toolTipPadding + textList.length * config.toolTipLineHeight

  // if beyond the right border
  if (
    offset.x - Math.abs(opts._scrollDistance_) + arrowWidth + toolTipWidth >
    opts.width
  ) {
    isOverRightBorder = true
  }

  // draw background rect
  context.beginPath()
  // hacker tooltip grd
  const grd = context.createLinearGradient(0, 150, 0, 50)
  grd.addColorStop(0, config.toolTipBackground)
  grd.addColorStop(1, config.toolTipBackground)
  context.setFillStyle(grd)
  // context.setFillStyle(
  //   opts.tooltip.option.background || config.toolTipBackground,
  // )
  context.setGlobalAlpha(config.toolTipOpacity)
  if (isOverRightBorder) {
    context.moveTo(offset.x, offset.y + 10)
    context.lineTo(offset.x - arrowWidth, offset.y + 10 - 5)
    context.lineTo(offset.x - arrowWidth, offset.y + 10 + 5)
    context.moveTo(offset.x, offset.y + 10)
    context.fillRect(
      offset.x - toolTipWidth - arrowWidth,
      offset.y,
      toolTipWidth,
      toolTipHeight,
    )
  } else {
    context.moveTo(offset.x, offset.y + 10)
    context.lineTo(offset.x + arrowWidth, offset.y + 10 - 5)
    context.lineTo(offset.x + arrowWidth, offset.y + 10 + 5)
    context.moveTo(offset.x, offset.y + 10)
    context.fillRect(
      offset.x + arrowWidth,
      offset.y,
      toolTipWidth,
      toolTipHeight,
    )
  }

  context.closePath()
  context.fill()
  context.setGlobalAlpha(1)

  // draw legend
  textList.forEach((item, index) => {
    context.beginPath()
    const tooltipTextGrd = context.createLinearGradient(0, 150, 0, 50)
    tooltipTextGrd.addColorStop(0, item.color)
    tooltipTextGrd.addColorStop(1, item.color)
    context.setFillStyle(tooltipTextGrd)
    let startX = offset.x + arrowWidth + 2 * config.toolTipPadding
    const startY =
      offset.y +
      (config.toolTipLineHeight - config.fontSize) / 2 +
      config.toolTipLineHeight * index +
      config.toolTipPadding
    if (isOverRightBorder) {
      startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding
    }
    context.fillRect(startX, startY, legendWidth, config.fontSize)
    context.closePath()
  })

  // draw text list
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle('#ffffff')
  textList.forEach((item, index) => {
    let startX =
      offset.x +
      arrowWidth +
      2 * config.toolTipPadding +
      legendWidth +
      legendMarginRight
    if (isOverRightBorder) {
      startX =
        offset.x -
        toolTipWidth -
        arrowWidth +
        2 * config.toolTipPadding +
        Number(legendWidth) +
        legendMarginRight
    }
    const startY =
      offset.y +
      (config.toolTipLineHeight - config.fontSize) / 2 +
      config.toolTipLineHeight * index +
      config.toolTipPadding
    context.fillText(item.text, startX, startY + config.fontSize)
  })
  context.stroke()
  context.closePath()
}

function drawYAxisTitle(title, opts, config, context) {
  const startX =
    config.xAxisHeight +
    (opts.height - config.xAxisHeight - measureText(title)) / 2
  context.save()
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle(opts.yAxis.titleFontColor || '#333333')
  context.translate(0, opts.height)
  context.rotate(-90 * Math.PI / 180)
  context.fillText(title, startX, config.padding + 0.5 * config.fontSize)
  context.stroke()
  context.closePath()
  context.restore()
}

function drawColumnDataPoints(series, opts, config, context) {
  const process =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  let _calYAxisData = calYAxisData(series, opts, config),
    ranges = _calYAxisData.ranges

  let _getXAxisPoints = getXAxisPoints(opts.categories, opts, config),
    xAxisPoints = _getXAxisPoints.xAxisPoints,
    eachSpacing = _getXAxisPoints.eachSpacing

  const minRange = ranges.pop()
  const maxRange = ranges.shift()
  context.save()
  if (
    opts._scrollDistance_ &&
    opts._scrollDistance_ !== 0 &&
    opts.enableScroll === true
  ) {
    context.translate(opts._scrollDistance_, 0)
  }

  series.forEach((eachSeries, seriesIndex) => {
    const data = eachSeries.data
    let points = getDataPoints(
      data,
      minRange,
      maxRange,
      xAxisPoints,
      eachSpacing,
      opts,
      config,
      process,
    )
    points = fixColumeData(
      points,
      eachSpacing,
      series.length,
      seriesIndex,
      config,
      opts,
    )

    // 绘制柱状数据图
    context.beginPath()
    context.setFillStyle(eachSeries.color)
    points.forEach((item, index) => {
      if (item !== null) {
        const startX = item.x - item.width / 2 + 1
        const height =
          opts.height -
          item.y -
          config.padding -
          config.xAxisHeight -
          config.legendHeight
        context.moveTo(startX, item.y)
        context.rect(startX, item.y, item.width - 2, height)
      }
    })
    context.closePath()
    context.fill()
  })
  series.forEach((eachSeries, seriesIndex) => {
    const data = eachSeries.data
    let points = getDataPoints(
      data,
      minRange,
      maxRange,
      xAxisPoints,
      eachSpacing,
      opts,
      config,
      process,
    )
    points = fixColumeData(
      points,
      eachSpacing,
      series.length,
      seriesIndex,
      config,
      opts,
    )
    if (opts.dataLabel !== false && process === 1) {
      drawPointText(points, eachSeries, config, context)
    }
  })
  context.restore()
  return {
    xAxisPoints,
    eachSpacing,
  }
}

function drawAreaDataPoints(series, opts, config, context) {
  const process =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  let _calYAxisData2 = calYAxisData(series, opts, config),
    ranges = _calYAxisData2.ranges

  let _getXAxisPoints2 = getXAxisPoints(opts.categories, opts, config),
    xAxisPoints = _getXAxisPoints2.xAxisPoints,
    eachSpacing = _getXAxisPoints2.eachSpacing

  const minRange = ranges.pop()
  const maxRange = ranges.shift()
  const endY =
    opts.height - config.padding - config.xAxisHeight - config.legendHeight
  const calPoints = []

  context.save()
  if (
    opts._scrollDistance_ &&
    opts._scrollDistance_ !== 0 &&
    opts.enableScroll === true
  ) {
    context.translate(opts._scrollDistance_, 0)
  }

  if (
    opts.tooltip &&
    opts.tooltip.textList &&
    opts.tooltip.textList.length &&
    process === 1
  ) {
    drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context)
  }

  series.forEach((eachSeries, seriesIndex) => {
    const data = eachSeries.data
    const points = getDataPoints(
      data,
      minRange,
      maxRange,
      xAxisPoints,
      eachSpacing,
      opts,
      config,
      process,
    )
    calPoints.push(points)

    const splitPointList = splitPoints(points)

    splitPointList.forEach(points => {
      // 绘制区域数据
      context.beginPath()
      context.setStrokeStyle(eachSeries.color)
      // 添加线性渐变
      if (eachSeries.gradient) {
        const grd = context.createLinearGradient(0, 150, 0, 50)
        grd.addColorStop(0, eachSeries.color)
        grd.addColorStop(1, eachSeries.endColor)
        context.setFillStyle(grd)
      } else {
        context.setFillStyle(eachSeries.color)
      }
      context.setGlobalAlpha(eachSeries.globalAlpha || 0.6)
      context.setLineWidth(2)
      if (points.length > 1) {
        const firstPoint = points[0]
        const lastPoint = points[points.length - 1]

        context.moveTo(firstPoint.x, firstPoint.y)
        if (opts.extra.lineStyle === 'curve') {
          points.forEach((item, index) => {
            if (index > 0) {
              const ctrlPoint = createCurveControlPoints(points, index - 1)
              context.bezierCurveTo(
                ctrlPoint.ctrA.x,
                ctrlPoint.ctrA.y,
                ctrlPoint.ctrB.x,
                ctrlPoint.ctrB.y,
                item.x,
                item.y,
              )
            }
          })
        } else {
          points.forEach((item, index) => {
            if (index > 0) {
              context.lineTo(item.x, item.y)
            }
          })
        }

        if (eachSeries.outlineColor) {
          context.setGlobalAlpha(1)
          context.setStrokeStyle(eachSeries.outlineColor)
          context.stroke()
          context.setGlobalAlpha(eachSeries.globalAlpha || 0.6)
          context.setStrokeStyle(eachSeries.color)
        }

        context.lineTo(lastPoint.x, endY)
        context.lineTo(firstPoint.x, endY)
        context.lineTo(firstPoint.x, firstPoint.y)
      } else {
        const item = points[0]
        context.moveTo(item.x - eachSpacing / 2, item.y)
        context.lineTo(item.x + eachSpacing / 2, item.y)
        context.lineTo(item.x + eachSpacing / 2, endY)
        context.lineTo(item.x - eachSpacing / 2, endY)
        context.moveTo(item.x - eachSpacing / 2, item.y)
      }
      context.closePath()
      context.fill()
      context.setGlobalAlpha(1)
      // hacker grd
      const grd = context.createLinearGradient(0, 150, 0, 50)
      grd.addColorStop(0, opts.background)
      grd.addColorStop(1, opts.background)
      context.setFillStyle(grd)
    })

    if (opts.dataPointShape !== false) {
      const shape =
        config.dataPointShape[seriesIndex % config.dataPointShape.length]
      drawPointShape(points, eachSeries.color, shape, context)
    }
  })
  if (opts.dataLabel !== false && process === 1) {
    series.forEach((eachSeries, seriesIndex) => {
      const data = eachSeries.data
      const points = getDataPoints(
        data,
        minRange,
        maxRange,
        xAxisPoints,
        eachSpacing,
        opts,
        config,
        process,
      )
      drawPointText(points, eachSeries, config, context)
    })
  }

  context.restore()

  return {
    xAxisPoints,
    calPoints,
    eachSpacing,
  }
}

function drawLineDataPoints(series, opts, config, context) {
  const process =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  let _calYAxisData3 = calYAxisData(series, opts, config),
    ranges = _calYAxisData3.ranges

  let _getXAxisPoints3 = getXAxisPoints(opts.categories, opts, config),
    xAxisPoints = _getXAxisPoints3.xAxisPoints,
    eachSpacing = _getXAxisPoints3.eachSpacing

  const minRange = ranges.pop()
  const maxRange = ranges.shift()
  const calPoints = []

  context.save()
  if (
    opts._scrollDistance_ &&
    opts._scrollDistance_ !== 0 &&
    opts.enableScroll === true
  ) {
    context.translate(opts._scrollDistance_, 0)
  }

  if (
    opts.tooltip &&
    opts.tooltip.textList &&
    opts.tooltip.textList.length &&
    process === 1
  ) {
    drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context)
  }

  series.forEach((eachSeries, seriesIndex) => {
    const data = eachSeries.data
    const points = getDataPoints(
      data,
      minRange,
      maxRange,
      xAxisPoints,
      eachSpacing,
      opts,
      config,
      process,
    )
    calPoints.push(points)
    const splitPointList = splitPoints(points)

    splitPointList.forEach((points, index) => {
      context.beginPath()
      context.setStrokeStyle(eachSeries.color)
      context.setLineWidth(2)
      if (points.length === 1) {
        context.moveTo(points[0].x, points[0].y)
        context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI)
      } else {
        context.moveTo(points[0].x, points[0].y)
        if (opts.extra.lineStyle === 'curve') {
          points.forEach((item, index) => {
            if (index > 0) {
              const ctrlPoint = createCurveControlPoints(points, index - 1)
              context.bezierCurveTo(
                ctrlPoint.ctrA.x,
                ctrlPoint.ctrA.y,
                ctrlPoint.ctrB.x,
                ctrlPoint.ctrB.y,
                item.x,
                item.y,
              )
            }
          })
        } else {
          points.forEach((item, index) => {
            if (index > 0) {
              context.lineTo(item.x, item.y)
            }
          })
        }
        context.moveTo(points[0].x, points[0].y)
      }
      context.closePath()
      context.stroke()
    })

    if (opts.dataPointShape !== false) {
      const shape =
        config.dataPointShape[seriesIndex % config.dataPointShape.length]
      drawPointShape(points, eachSeries.color, shape, context)
    }
  })
  if (opts.dataLabel !== false && process === 1) {
    series.forEach((eachSeries, seriesIndex) => {
      const data = eachSeries.data
      const points = getDataPoints(
        data,
        minRange,
        maxRange,
        xAxisPoints,
        eachSpacing,
        opts,
        config,
        process,
      )
      drawPointText(points, eachSeries, config, context)
    })
  }

  context.restore()

  return {
    xAxisPoints,
    calPoints,
    eachSpacing,
  }
}

function drawToolTipBridge(opts, config, context, process) {
  context.save()
  if (
    opts._scrollDistance_ &&
    opts._scrollDistance_ !== 0 &&
    opts.enableScroll === true
  ) {
    context.translate(opts._scrollDistance_, 0)
  }
  if (
    opts.tooltip &&
    opts.tooltip.textList &&
    opts.tooltip.textList.length &&
    process === 1
  ) {
    drawToolTip(
      opts.tooltip.textList,
      opts.tooltip.offset,
      opts,
      config,
      context,
    )
  }
  context.restore()
}

function drawXAxis(categories, opts, config, context) {
  let _getXAxisPoints4 = getXAxisPoints(categories, opts, config),
    xAxisPoints = _getXAxisPoints4.xAxisPoints,
    startX = _getXAxisPoints4.startX,
    endX = _getXAxisPoints4.endX,
    eachSpacing = _getXAxisPoints4.eachSpacing

  const startY =
    opts.height - config.padding - config.xAxisHeight - config.legendHeight
  const endY = startY + config.xAxisLineHeight

  context.save()
  if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) {
    context.translate(opts._scrollDistance_, 0)
  }

  context.beginPath()
  context.setStrokeStyle(opts.xAxis.gridColor || '#cccccc')

  if (opts.xAxis.disableGrid !== true) {
    if (opts.xAxis.type === 'calibration') {
      xAxisPoints.forEach((item, index) => {
        if (index > 0) {
          context.moveTo(item - eachSpacing / 2, startY)
          context.lineTo(item - eachSpacing / 2, startY + 4)
        }
      })
    } else {
      xAxisPoints.forEach((item, index) => {
        context.moveTo(item, startY)
        context.lineTo(item, endY)
      })
    }
  }
  context.closePath()
  context.stroke()

  // 对X轴列表做抽稀处理
  const validWidth =
    opts.width - 2 * config.padding - config.yAxisWidth - config.yAxisTitleWidth
  const maxXAxisListLength = Math.min(
    categories.length,
    Math.ceil(validWidth / config.fontSize / 1.5),
  )

  const ratio = Math.ceil(
    categories.length /
      (opts.xAxis.maxPoints && opts.xAxis.maxPoints < maxXAxisListLength - 1
        ? opts.xAxis.maxPoints
        : maxXAxisListLength),
  )

  categories = categories.map(
    (item, index) =>
      index % ratio !== 0 && index !== categories.length - 1 ? '' : item,
  )

  if (opts.xAxis.noRotate || config._xAxisTextAngle_ === 0) {
    context.beginPath()
    context.setFontSize(config.fontSize)
    context.setFillStyle(opts.xAxis.fontColor || '#666666')
    categories.forEach((item, index) => {
      const offset =
        eachSpacing / 2 -
        measureText(item) / 2 -
        (index === categories.length - 1 ? 4 : 0)
      context.fillText(
        item,
        xAxisPoints[index] + offset,
        startY + config.fontSize + 5,
      )
    })
    context.closePath()
    context.stroke()
  } else {
    categories.forEach((item, index) => {
      context.save()
      context.beginPath()
      context.setFontSize(config.fontSize)
      context.setFillStyle(opts.xAxis.fontColor || '#666666')
      const textWidth = measureText(item)
      const offset = eachSpacing / 2 - textWidth

      let _calRotateTranslate = calRotateTranslate(
          xAxisPoints[index] + eachSpacing / 2,
          startY + config.fontSize / 2 + 5,
          opts.height,
        ),
        transX = _calRotateTranslate.transX,
        transY = _calRotateTranslate.transY

      context.rotate(-1 * config._xAxisTextAngle_)
      context.translate(transX, transY)
      // 不绘制x轴
      if (!opts.xAxis.disabled) {
        context.fillText(
          item,
          xAxisPoints[index] + offset,
          startY + config.fontSize + 5,
        )
      }
      context.closePath()
      context.stroke()
      context.restore()
    })
  }

  context.restore()
}

function drawYAxisGrid(opts, config, context) {
  const spacingValid =
    opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
  const eachSpacing = Math.floor(spacingValid / config.yAxisSplit)
  const yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
  const startX = config.padding + yAxisTotalWidth
  const endX = opts.width - config.padding

  const points = []
  for (let i = 0; i < config.yAxisSplit; i++) {
    points.push(config.padding + eachSpacing * i)
  }
  points.push(config.padding + eachSpacing * config.yAxisSplit + 2)

  context.beginPath()
  context.setStrokeStyle(opts.yAxis.gridColor || '#cccccc')
  context.setLineWidth(1)
  points.forEach((item, index) => {
    context.moveTo(startX, item)
    context.lineTo(endX, item)
  })
  context.closePath()
  context.stroke()
  // to draw the yAxis the first line
  context.beginPath()
  context.setStrokeStyle(opts.yAxis.gridColor || '#cccccc')
  context.moveTo(startX, points[5])
  context.lineTo(endX, points[5])
  context.closePath()
  context.stroke()
}

function drawYAxis(series, opts, config, context) {
  if (opts.yAxis.disabled === true) {
    return
  }

  let _calYAxisData4 = calYAxisData(series, opts, config),
    rangesFormat = _calYAxisData4.rangesFormat

  const yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth

  const spacingValid =
    opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
  const eachSpacing = Math.floor(spacingValid / config.yAxisSplit)
  const startX = config.padding + yAxisTotalWidth
  const endX = opts.width - config.padding
  const endY =
    opts.height - config.padding - config.xAxisHeight - config.legendHeight

  // set YAxis background
  context.setFillStyle(opts.background || '#ffffff')
  if (opts._scrollDistance_ < 0) {
    context.fillRect(0, 0, startX, endY + config.xAxisHeight + 5)
  }
  context.fillRect(endX, 0, opts.width, endY + config.xAxisHeight + 5)

  const points = []
  for (let i = 0; i <= config.yAxisSplit; i++) {
    points.push(config.padding + eachSpacing * i)
  }

  context.stroke()
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle(opts.yAxis.fontColor || '#666666')
  rangesFormat.forEach((item, index) => {
    const pos = points[index] ? points[index] : endY
    context.fillText(
      item,
      config.padding + config.yAxisTitleWidth,
      pos + config.fontSize / 2,
    )
  })
  context.closePath()
  context.stroke()

  if (opts.yAxis.title) {
    drawYAxisTitle(opts.yAxis.title, opts, config, context)
  }
}

function drawLegend(series, opts, config, context) {
  if (!opts.legend) {
    return
  }
  // each legend shape width 15px
  // the spacing between shape and text in each legend is the `padding`
  // each legend spacing is the `padding`
  // legend margin top `config.padding`

  let _calLegendData = calLegendData(series, opts, config),
    legendList = _calLegendData.legendList

  const padding = 5
  const marginTop = 8
  const shapeWidth = 15
  legendList.forEach((itemList, listIndex) => {
    let width = 0
    itemList.forEach(item => {
      item.name = item.name || 'undefined'
      width += 3 * padding + measureText(item.name) + shapeWidth
    })
    let startX = (opts.width - width) / 2 + padding
    const startY =
      opts.height -
      config.padding -
      config.legendHeight +
      listIndex * (config.fontSize + marginTop) +
      padding +
      marginTop

    context.setFontSize(config.fontSize)
    itemList.forEach(item => {
      switch (opts.type) {
        case 'line':
          context.beginPath()
          context.setLineWidth(1)
          context.setStrokeStyle(item.color)
          context.moveTo(startX - 2, startY + 5)
          context.lineTo(startX + 17, startY + 5)
          context.stroke()
          context.closePath()
          context.beginPath()
          context.setLineWidth(1)
          context.setStrokeStyle('#ffffff')
          context.setFillStyle(item.color)
          context.moveTo(startX + 7.5, startY + 5)
          context.arc(startX + 7.5, startY + 5, 4, 0, 2 * Math.PI)
          context.fill()
          context.stroke()
          context.closePath()
          break
        case 'pie':
        case 'ring':
          context.beginPath()
          context.setFillStyle(item.color)
          context.moveTo(startX + 7.5, startY + 5)
          context.arc(startX + 7.5, startY + 5, 7, 0, 2 * Math.PI)
          context.closePath()
          context.fill()
          break
        default:
          context.beginPath()
          context.setFillStyle(item.color)
          context.moveTo(startX, startY)
          context.rect(startX, startY, 15, 10)
          context.closePath()
          context.fill()
      }
      startX += padding + shapeWidth
      context.beginPath()
      context.setFillStyle(opts.extra.legendTextColor || '#333333')
      context.fillText(item.name, startX, startY + 9)
      context.closePath()
      context.stroke()
      startX += measureText(item.name) + 2 * padding
    })
  })
}
function drawPieDataPoints(series, opts, config, context) {
  const process =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  const pieOption = opts.extra.pie || {}
  series = getPieDataPoints(series, process)
  const centerPosition = {
    x: opts.width / 2,
    y: (opts.height - config.legendHeight) / 2,
  }
  let radius = Math.min(
    centerPosition.x -
      config.pieChartLinePadding -
      config.pieChartTextPadding -
      config._pieTextMaxLength_,
    centerPosition.y - config.pieChartLinePadding - config.pieChartTextPadding,
  )
  if (opts.dataLabel) {
    radius -= 10
  } else {
    radius -= 2 * config.padding
  }
  series = series.map(eachSeries => {
    eachSeries._start_ += (pieOption.offsetAngle || 0) * Math.PI / 180
    return eachSeries
  })
  series.forEach(eachSeries => {
    context.beginPath()
    context.setLineWidth(2)
    context.setStrokeStyle('#ffffff')
    context.setFillStyle(eachSeries.color)
    context.moveTo(centerPosition.x, centerPosition.y)
    context.arc(
      centerPosition.x,
      centerPosition.y,
      radius,
      eachSeries._start_,
      eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI,
    )
    context.closePath()
    context.fill()
    if (opts.disablePieStroke !== true) {
      context.stroke()
    }
  })

  if (opts.type === 'ring') {
    let innerPieWidth = radius * 0.6
    if (typeof opts.extra.ringWidth === 'number' && opts.extra.ringWidth > 0) {
      innerPieWidth = Math.max(0, radius - opts.extra.ringWidth)
    }
    context.beginPath()
    context.setFillStyle(opts.background || '#ffffff')
    context.moveTo(centerPosition.x, centerPosition.y)
    context.arc(
      centerPosition.x,
      centerPosition.y,
      innerPieWidth,
      0,
      2 * Math.PI,
    )
    context.closePath()
    context.fill()
  }

  if (opts.dataLabel !== false && process === 1) {
    // fix https://github.com/xiaolin3303/wx-charts/issues/132
    let valid = false
    for (let i = 0, len = series.length; i < len; i++) {
      if (series[i].data > 0) {
        valid = true
        break
      }
    }

    if (valid) {
      drawPieText(series, opts, config, context, radius, centerPosition)
    }
  }

  if (process === 1 && opts.type === 'ring') {
    drawRingTitle(opts, config, context)
  }

  return {
    center: centerPosition,
    radius,
    series,
  }
}

function drawRadarDataPoints(series, opts, config, context) {
  const process =
    arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  const radarOption = opts.extra.radar || {}
  const coordinateAngle = getRadarCoordinateSeries(opts.categories.length)
  const centerPosition = {
    x: opts.width / 2,
    y: (opts.height - config.legendHeight) / 2,
  }

  let radius = Math.min(
    centerPosition.x -
      (getMaxTextListLength(opts.categories) + config.radarLabelTextMargin),
    centerPosition.y - config.radarLabelTextMargin,
  )

  radius -= config.padding

  // draw grid
  context.beginPath()
  context.setLineWidth(1)
  context.setStrokeStyle(radarOption.gridColor || '#cccccc')
  coordinateAngle.forEach(angle => {
    const pos = convertCoordinateOrigin(
      radius * Math.cos(angle),
      radius * Math.sin(angle),
      centerPosition,
    )
    context.moveTo(centerPosition.x, centerPosition.y)
    context.lineTo(pos.x, pos.y)
  })
  context.stroke()
  context.closePath()

  // draw split line grid

  const _loop = function _loop(i) {
    let startPos = {}
    context.beginPath()
    context.setLineWidth(1)
    context.setStrokeStyle(radarOption.gridColor || '#cccccc')
    coordinateAngle.forEach((angle, index) => {
      const pos = convertCoordinateOrigin(
        radius / config.radarGridCount * i * Math.cos(angle),
        radius / config.radarGridCount * i * Math.sin(angle),
        centerPosition,
      )
      if (index === 0) {
        startPos = pos
        context.moveTo(pos.x, pos.y)
      } else {
        context.lineTo(pos.x, pos.y)
      }
    })
    context.lineTo(startPos.x, startPos.y)
    context.stroke()
    context.closePath()
  }

  for (let i = 1; i <= config.radarGridCount; i++) {
    _loop(i)
  }

  const radarDataPoints = getRadarDataPoints(
    coordinateAngle,
    centerPosition,
    radius,
    series,
    opts,
    process,
  )
  radarDataPoints.forEach((eachSeries, seriesIndex) => {
    // 绘制区域数据
    context.beginPath()
    context.setFillStyle(eachSeries.color)
    context.setGlobalAlpha(0.6)
    eachSeries.data.forEach((item, index) => {
      if (index === 0) {
        context.moveTo(item.position.x, item.position.y)
      } else {
        context.lineTo(item.position.x, item.position.y)
      }
    })
    context.closePath()
    context.fill()
    context.setGlobalAlpha(1)

    if (opts.dataPointShape !== false) {
      const shape =
        config.dataPointShape[seriesIndex % config.dataPointShape.length]
      const points = eachSeries.data.map(item => item.position)
      drawPointShape(points, eachSeries.color, shape, context)
    }
  })
  // draw label text
  drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context)

  return {
    center: centerPosition,
    radius,
    angleList: coordinateAngle,
  }
}

function drawCanvas(opts, context) {
  context.draw()
}

const Timing = {
  easeIn: function easeIn(pos) {
    return Math.pow(pos, 3)
  },

  easeOut: function easeOut(pos) {
    return Math.pow(pos - 1, 3) + 1
  },

  easeInOut: function easeInOut(pos) {
    if ((pos /= 0.5) < 1) {
      return 0.5 * Math.pow(pos, 3)
    } else {
      return 0.5 * (Math.pow(pos - 2, 3) + 2)
    }
  },

  linear: function linear(pos) {
    return pos
  },
}

function Animation(opts) {
  this.isStop = false
  opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration
  opts.timing = opts.timing || 'linear'

  const delay = 17

  const createAnimationFrame = function createAnimationFrame() {
    if (typeof requestAnimationFrame !== 'undefined') {
      return requestAnimationFrame
    } else if (typeof setTimeout !== 'undefined') {
      return function(step, delay) {
        setTimeout(() => {
          const timeStamp = Number(new Date())
          step(timeStamp)
        }, delay)
      }
    } else {
      return function(step) {
        step(null)
      }
    }
  }
  const animationFrame = createAnimationFrame()
  let startTimeStamp = null
  var _step = function step(timestamp) {
    if (timestamp === null || this.isStop === true) {
      opts.onProcess && opts.onProcess(1)
      opts.onAnimationFinish && opts.onAnimationFinish()
      return
    }
    if (startTimeStamp === null) {
      startTimeStamp = timestamp
    }
    if (timestamp - startTimeStamp < opts.duration) {
      let process = (timestamp - startTimeStamp) / opts.duration
      const timingFunction = Timing[opts.timing]
      process = timingFunction(process)
      opts.onProcess && opts.onProcess(process)
      animationFrame(_step, delay)
    } else {
      opts.onProcess && opts.onProcess(1)
      opts.onAnimationFinish && opts.onAnimationFinish()
    }
  }
  _step = _step.bind(this)

  animationFrame(_step, delay)
}

// stop animation immediately
// and tigger onAnimationFinish
Animation.prototype.stop = function() {
  this.isStop = true
}

function drawCharts(type, opts, config, context) {
  // context.setFillStyle(opts.background)
  // context.fillRect(0, 0, opts.width, opts.height)

  const _this = this

  let series = opts.series
  const categories = opts.categories
  series = fillSeriesColor(series, config)

  let _calLegendData = calLegendData(series, opts, config),
    legendHeight = _calLegendData.legendHeight

  config.legendHeight = legendHeight

  let _calYAxisData = calYAxisData(series, opts, config),
    yAxisWidth = _calYAxisData.yAxisWidth

  config.yAxisWidth = yAxisWidth
  if (categories && categories.length) {
    let _calCategoriesData = calCategoriesData(categories, opts, config),
      xAxisHeight = _calCategoriesData.xAxisHeight,
      angle = _calCategoriesData.angle

    config.xAxisHeight = xAxisHeight
    config._xAxisTextAngle_ = angle
  }
  if (type === 'pie' || type === 'ring') {
    config._pieTextMaxLength_ =
      opts.dataLabel === false ? 0 : getPieTextMaxLength(series)
  }

  const duration = opts.animation ? 1000 : 0
  this.animationInstance && this.animationInstance.stop()
  switch (type) {
    case 'line':
      this.animationInstance = new Animation({
        timing: 'easeIn',
        duration,
        onProcess: function onProcess(process) {
          drawYAxisGrid(opts, config, context)

          let _drawLineDataPoints = drawLineDataPoints(
              series,
              opts,
              config,
              context,
              process,
            ),
            xAxisPoints = _drawLineDataPoints.xAxisPoints,
            calPoints = _drawLineDataPoints.calPoints,
            eachSpacing = _drawLineDataPoints.eachSpacing

          _this.chartData.xAxisPoints = xAxisPoints
          _this.chartData.calPoints = calPoints
          _this.chartData.eachSpacing = eachSpacing
          // drawXAxis(categories, opts, config, context)
          drawLegend(opts.series, opts, config, context)
          drawYAxis(series, opts, config, context)
          drawToolTipBridge(opts, config, context, process)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish() {
          _this.event.trigger('renderComplete')
        },
      })
      break
    case 'column':
      this.animationInstance = new Animation({
        timing: 'easeIn',
        duration,
        onProcess: function onProcess(process) {
          drawYAxisGrid(opts, config, context)

          let _drawColumnDataPoints = drawColumnDataPoints(
              series,
              opts,
              config,
              context,
              process,
            ),
            xAxisPoints = _drawColumnDataPoints.xAxisPoints,
            eachSpacing = _drawColumnDataPoints.eachSpacing

          _this.chartData.xAxisPoints = xAxisPoints
          _this.chartData.eachSpacing = eachSpacing
          drawXAxis(categories, opts, config, context)
          drawLegend(opts.series, opts, config, context)
          drawYAxis(series, opts, config, context)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish() {
          _this.event.trigger('renderComplete')
        },
      })
      break
    case 'area':
      this.animationInstance = new Animation({
        timing: 'easeIn',
        duration,
        onProcess: function onProcess(process) {
          drawYAxisGrid(opts, config, context)

          let _drawAreaDataPoints = drawAreaDataPoints(
              series,
              opts,
              config,
              context,
              process,
            ),
            xAxisPoints = _drawAreaDataPoints.xAxisPoints,
            calPoints = _drawAreaDataPoints.calPoints,
            eachSpacing = _drawAreaDataPoints.eachSpacing

          _this.chartData.xAxisPoints = xAxisPoints
          _this.chartData.calPoints = calPoints
          _this.chartData.eachSpacing = eachSpacing
          drawXAxis(categories, opts, config, context)
          drawLegend(opts.series, opts, config, context)
          drawYAxis(series, opts, config, context)
          drawToolTipBridge(opts, config, context, process)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish() {
          _this.event.trigger('renderComplete')
        },
      })
      break
    case 'ring':
    case 'pie':
      this.animationInstance = new Animation({
        timing: 'easeInOut',
        duration,
        onProcess: function onProcess(process) {
          _this.chartData.pieData = drawPieDataPoints(
            series,
            opts,
            config,
            context,
            process,
          )
          drawLegend(opts.series, opts, config, context)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish() {
          _this.event.trigger('renderComplete')
        },
      })
      break
    case 'radar':
      this.animationInstance = new Animation({
        timing: 'easeInOut',
        duration,
        onProcess: function onProcess(process) {
          _this.chartData.radarData = drawRadarDataPoints(
            series,
            opts,
            config,
            context,
            process,
          )
          drawLegend(opts.series, opts, config, context)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish() {
          _this.event.trigger('renderComplete')
        },
      })
      break
  }
}

// simple event implement

function Event() {
  this.events = {}
}

Event.prototype.addEventListener = function(type, listener) {
  this.events[type] = this.events[type] || []
  this.events[type].push(listener)
}

Event.prototype.trigger = function() {
  for (
    var _len = arguments.length, args = Array(_len), _key = 0;
    _key < _len;
    _key++
  ) {
    args[_key] = arguments[_key]
  }

  const type = args[0]
  const params = args.slice(1)
  if (this.events[type]) {
    this.events[type].forEach(listener => {
      try {
        listener(...params)
      } catch (e) {
        console.error(e)
      }
    })
  }
}

const Charts = function Charts(opts) {
  opts.title = opts.title || {}
  opts.subtitle = opts.subtitle || {}
  opts.yAxis = opts.yAxis || {}
  opts.xAxis = opts.xAxis || {}
  opts.extra = opts.extra || {}
  opts.legend = opts.legend !== false
  opts.animation = opts.animation !== false
  const config$$1 = assign({}, config)
  config$$1.yAxisTitleWidth =
    opts.yAxis.disabled !== true && opts.yAxis.title
      ? config$$1.yAxisTitleWidth
      : 0
  config$$1.pieChartLinePadding =
    opts.dataLabel === false ? 0 : config$$1.pieChartLinePadding
  config$$1.pieChartTextPadding =
    opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding

  this.opts = opts
  this.config = config$$1
  this.context = wx.createCanvasContext(opts.canvasId)

  // this.context.setFillStyle(grd)
  // this.context.fillRect(10, 10, 150, 80)
  // store calcuated chart data
  // such as chart point coordinate
  this.chartData = {}
  this.event = new Event()
  this.scrollOption = {
    currentOffset: 0,
    startTouchX: 0,
    distance: 0,
  }

  drawCharts.call(this, opts.type, opts, config$$1, this.context)
}

Charts.prototype.updateData = function() {
  const data =
    arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}

  this.opts.series = data.series || this.opts.series
  this.opts.categories = data.categories || this.opts.categories
  this.opts.height = data.height || this.opts.height

  this.opts.title = assign({}, this.opts.title, data.title || {})
  this.opts.subtitle = assign({}, this.opts.subtitle, data.subtitle || {})

  drawCharts.call(this, this.opts.type, this.opts, this.config, this.context)
}

Charts.prototype.stopAnimation = function() {
  this.animationInstance && this.animationInstance.stop()
}

Charts.prototype.addEventListener = function(type, listener) {
  this.event.addEventListener(type, listener)
}

Charts.prototype.getCurrentDataIndex = function(e) {
  const touches = e.touches && e.touches.length ? e.touches : e.changedTouches
  if (touches && touches.length) {
    let _touches$ = touches[0],
      x = _touches$.x,
      y = _touches$.y

    if (this.opts.type === 'pie' || this.opts.type === 'ring') {
      return findPieChartCurrentIndex({ x, y }, this.chartData.pieData)
    } else if (this.opts.type === 'radar') {
      return findRadarChartCurrentIndex(
        { x, y },
        this.chartData.radarData,
        this.opts.categories.length,
      )
    } else {
      return findCurrentIndex(
        { x, y },
        this.chartData.xAxisPoints,
        this.opts,
        this.config,
        Math.abs(this.scrollOption.currentOffset),
      )
    }
  }
  return -1
}

Charts.prototype.showToolTip = function(e) {
  const option =
    arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}

  if (this.opts.type === 'line' || this.opts.type === 'area') {
    const index = this.getCurrentDataIndex(e)
    const currentOffset = this.scrollOption.currentOffset

    const opts = assign({}, this.opts, {
      _scrollDistance_: currentOffset,
      animation: false,
    })
    if (index > -1) {
      const seriesData = getSeriesDataItem(this.opts.series, index)
      if (seriesData.length !== 0) {
        let _getToolTipData = getToolTipData(
            seriesData,
            this.chartData.calPoints,
            index,
            this.opts.categories,
            option,
          ),
          textList = _getToolTipData.textList,
          offset = _getToolTipData.offset
        opts.tooltip = {
          textList,
          offset,
          option,
        }
      }
    }
    drawCharts.call(this, opts.type, opts, this.config, this.context)
  }
}

Charts.prototype.scrollStart = function(e) {
  if (e.touches[0] && this.opts.enableScroll === true) {
    this.scrollOption.startTouchX = e.touches[0].x
  }
}

Charts.prototype.scroll = function(e) {
  // TODO throtting...
  if (e.touches[0] && this.opts.enableScroll === true) {
    let _distance = e.touches[0].x - this.scrollOption.startTouchX
    const currentOffset = this.scrollOption.currentOffset

    const validDistance = calValidDistance(
      currentOffset + _distance,
      this.chartData,
      this.config,
      this.opts,
    )

    this.scrollOption.distance = _distance = validDistance - currentOffset
    const opts = assign({}, this.opts, {
      _scrollDistance_: currentOffset + _distance,
      animation: false,
    })

    drawCharts.call(this, opts.type, opts, this.config, this.context)
  }
}

Charts.prototype.scrollEnd = function(e) {
  if (this.opts.enableScroll === true) {
    let _scrollOption = this.scrollOption,
      currentOffset = _scrollOption.currentOffset,
      distance = _scrollOption.distance

    this.scrollOption.currentOffset = currentOffset + distance
    this.scrollOption.distance = 0
  }
}

module.exports = Charts
