sparkline.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // 提取自 https://github.com/fnando/sparkline
  2. function getY(max: number, height: number, diff: number, value: number) {
  3. return parseFloat((height - (value * height / max) + diff).toFixed(2))
  4. }
  5. function removeChildren(svg: any) {
  6. [...svg.querySelectorAll('*')].forEach(element => svg.removeChild(element))
  7. }
  8. function defaultFetch(entry: any) {
  9. return entry.value
  10. }
  11. function buildElement(tag: string, attrs: Record<string, any>) {
  12. const element = document.createElementNS('http://www.w3.org/2000/svg', tag)
  13. for (const name in attrs) {
  14. element.setAttribute(name, attrs[name])
  15. }
  16. return element
  17. }
  18. export function sparkline(svg: any, entries: any, options: any) {
  19. removeChildren(svg)
  20. if (entries.length <= 1) {
  21. return
  22. }
  23. options = options || {}
  24. if (typeof (entries[0]) === 'number') {
  25. entries = entries.map((entry: any) => {
  26. return { value: entry }
  27. })
  28. }
  29. // This function will be called whenever the mouse moves
  30. // over the SVG. You can use it to render something like a
  31. // tooltip.
  32. const onmousemove = options.onmousemove
  33. // This function will be called whenever the mouse leaves
  34. // the SVG area. You can use it to hide the tooltip.
  35. const onmouseout = options.onmouseout
  36. // Should we run in interactive mode? If yes, this will handle the
  37. // cursor and spot position when moving the mouse.
  38. const interactive = ('interactive' in options) ? options.interactive : !!onmousemove
  39. // Define how big should be the spot area.
  40. const spotRadius = options.spotRadius || 2
  41. const spotDiameter = spotRadius * 2
  42. // Define how wide should be the cursor area.
  43. const cursorWidth = options.cursorWidth || 2
  44. // Get the stroke width; this is used to compute the
  45. // rendering offset.
  46. const strokeWidth = parseFloat(svg.attributes['stroke-width'].value)
  47. // By default, db must be formatted as an array of numbers or
  48. // an array of objects with the value key (like `[{value: 1}]`).
  49. // You can set a custom function to return db for a different
  50. // db structure.
  51. const fetch = options.fetch || defaultFetch
  52. // Retrieve only values, easing the find for the maximum value.
  53. const values = entries.map((entry: any) => fetch(entry))
  54. // The rendering width will account for the spot size.
  55. const width = parseFloat(svg.attributes.width.value) - spotDiameter * 2
  56. // Get the SVG element's full height.
  57. // This is used
  58. const fullHeight = parseFloat(svg.attributes.height.value)
  59. // The rendering height accounts for stroke width and spot size.
  60. const height = fullHeight - (strokeWidth * 2) - spotDiameter
  61. // The maximum value. This is used to calculate the Y coord of
  62. // each sparkline datapoint.
  63. const max = Math.max(...values)
  64. // Some arbitrary value to remove the cursor and spot out of
  65. // the viewing canvas.
  66. const offscreen = -1000
  67. // Cache the last item index.
  68. const lastItemIndex = values.length - 1
  69. // Calculate the X coord base step.
  70. const offset = width / lastItemIndex
  71. // Hold all datapoints, which is whatever we got as the entry plus
  72. // x/y coords and the index.
  73. const datapoints: any[] = []
  74. // Hold the line coordinates.
  75. const pathY = getY(max, height, strokeWidth + spotRadius, values[0])
  76. let pathCoords = `M${spotDiameter} ${pathY}`
  77. values.forEach((value: number, index: number) => {
  78. const x = index * offset + spotDiameter
  79. const y = getY(max, height, strokeWidth + spotRadius, value)
  80. datapoints.push(Object.assign({}, entries[index], {
  81. index,
  82. x,
  83. y,
  84. }))
  85. pathCoords += ` L ${x} ${y}`
  86. })
  87. const path = buildElement('path', {
  88. class: 'sparkline--line',
  89. d: pathCoords,
  90. fill: 'none',
  91. })
  92. const fillCoords = `${pathCoords} V ${fullHeight} L ${spotDiameter} ${fullHeight} Z`
  93. const fill = buildElement('path', {
  94. class: 'sparkline--fill',
  95. d: fillCoords,
  96. stroke: 'none',
  97. })
  98. svg.appendChild(fill)
  99. svg.appendChild(path)
  100. if (!interactive) {
  101. return
  102. }
  103. const cursor = buildElement('line', {
  104. 'class': 'sparkline--cursor',
  105. 'x1': offscreen,
  106. 'x2': offscreen,
  107. 'y1': 0,
  108. 'y2': fullHeight,
  109. 'stroke-width': cursorWidth,
  110. })
  111. const spot = buildElement('circle', {
  112. class: 'sparkline--spot',
  113. cx: offscreen,
  114. cy: offscreen,
  115. r: spotRadius,
  116. })
  117. svg.appendChild(cursor)
  118. svg.appendChild(spot)
  119. const interactionLayer = buildElement('rect', {
  120. width: svg.attributes.width.value,
  121. height: svg.attributes.height.value,
  122. style: 'fill: transparent; stroke: transparent',
  123. class: 'sparkline--interaction-layer',
  124. })
  125. svg.appendChild(interactionLayer)
  126. interactionLayer.addEventListener('mouseout', (event: MouseEvent) => {
  127. cursor.setAttribute('x1', offscreen.toString())
  128. cursor.setAttribute('x2', offscreen.toString())
  129. spot.setAttribute('cx', offscreen.toString())
  130. if (onmouseout) {
  131. onmouseout(event)
  132. }
  133. })
  134. interactionLayer.addEventListener('mousemove', (event: MouseEvent) => {
  135. const mouseX = event.offsetX
  136. let nextDataPoint = datapoints.find((entry) => {
  137. return entry.x >= mouseX
  138. })
  139. if (!nextDataPoint) {
  140. nextDataPoint = datapoints[lastItemIndex]
  141. }
  142. const previousDataPoint = datapoints[datapoints.indexOf(nextDataPoint) - 1]
  143. let currentDataPoint
  144. let halfway
  145. if (previousDataPoint) {
  146. halfway = previousDataPoint.x + ((nextDataPoint.x - previousDataPoint.x) / 2)
  147. currentDataPoint = mouseX >= halfway ? nextDataPoint : previousDataPoint
  148. }
  149. else {
  150. currentDataPoint = nextDataPoint
  151. }
  152. const x = currentDataPoint.x
  153. const y = currentDataPoint.y
  154. spot.setAttribute('cx', x)
  155. spot.setAttribute('cy', y)
  156. cursor.setAttribute('x1', x)
  157. cursor.setAttribute('x2', x)
  158. if (onmousemove) {
  159. onmousemove(event, currentDataPoint)
  160. }
  161. })
  162. }
  163. export default sparkline