https://github.com/thu-vis/MutualDetector
Raw File
Tip revision: c1059af41f4a6627cec3b5094096e6635f57673c authored by unknown on 11 January 2022, 04:44:40 UTC
remove video
Tip revision: c1059af
wordcloud.js
// import * as d3 from "d3";

function wordcloud() {
    let size = [256, 256],
      center = null,
      text = cloudText,
      font = cloudFont,
      fontSize = cloudFontSize,
      fontStyle = cloudFontNormal,
      fontWeight = cloudFontNormal,
      padding = cloudPadding,
      rangex = cloudRangex,
      rangey = cloudRangey,
      expandx = cloudExpandx,
      expandy = cloudExpandy,
      flexible = cloudFlexible,
      barriers = [],
      words = [],
      grid_step = 4,
      grid_size = 20
  
    const self = {}
  
    const align = (x) => x - x % grid_size
    let rows = [], cols = [], rects = [], nFixedRect = 0
    self.addRect = function (top, left, bottom, right) {
      let rect = [top, left, bottom, right]
      for (let i = align(top); i < bottom; i += grid_size) {
        if (!rows[i]) rows[i] = []
        rows[i].push(rect)
      }
      for (let j = align(left); j < right; j += grid_size) {
        if (!cols[j]) cols[j] = []
        cols[j].push(rect)
      }
      rects.push(rect)
    }
  
    // function forceCollide() {
    //   let nodes
  
    //   function force() {
    //     const quad = d3.quadtree(nodes, d => d.x, d => d.y);
    //     for (const d of nodes) {
    //       quad.visit((q) => {
    //         let updated = false;
    //         if (q.data && q.data !== d) {
    //           let x = d.x - q.data.x,
    //             y = d.y - q.data.y,
    //             xSpacing = padding + (q.data.width + d.width) / 2,
    //             ySpacing = padding + (q.data.height + d.height) / 2,
    //             absX = Math.abs(x),
    //             absY = Math.abs(y),
    //             l,
    //             lx,
    //             ly;
  
    //           if (absX < xSpacing && absY < ySpacing) {
    //             l = Math.sqrt(x * x + y * y);
  
    //             lx = (absX - xSpacing) / l;
    //             ly = (absY - ySpacing) / l;
  
    //             // the one that's barely within the bounds probably triggered the collision
    //             if (Math.abs(lx) > Math.abs(ly)) {
    //               lx = 0;
    //             } else {
    //               ly = 0;
    //             }
    //             d.x -= x *= lx;
    //             d.y -= y *= ly;
    //             q.data.x += x;
    //             q.data.y += y;
  
    //             updated = true;
    //           }
    //         }
    //         return updated;
    //       });
    //     }
    //   }
  
    //   force.initialize = _ => nodes = _;
  
    //   return force;
    // }
  
    self.checkVectical = function (lines) {
      if (!Array.isArray(lines)) {
        lines = [lines]
      } else {
        lines = lines.sort()
      }
  
      let s = []
      for (let i = 0; i < lines.length; ++i) {
        let x = lines[i], x0 = align(lines[i])
        if (cols[x0]) {
          for (let rect of cols[x0]) {
            if (rect[1] < x && x < rect[3]) {
              s.push([rect[0], rect[2]])
            }
          }
        }
      }
      s = s.sort((a, b) => a[0] - b[0])
  
      let ret = [], top = 0, bottom = 0
      for (let i = 0; i < s.length; ++i) {
        if (s[i][0] <= bottom) {
          if (s[i][1] > bottom) {
            bottom = s[i][1]
          }
        } else {
          ret.push(bottom)
          top = s[i][0]
          bottom = s[i][1]
          ret.push(top)
        }
      }
      ret.push(bottom)
      ret.push(size[1])
      return ret
    }
  
    self.checkHorizontal = function (lines) {
      if (!Array.isArray(lines)) {
        lines = [lines]
      } else {
        lines = lines.sort()
      }
  
      let s = []
      for (let i = 0; i < lines.length; ++i) {
        let y = lines[i], y0 = align(lines[i])
        if (rows[y0]) {
          for (let rect of rows[y0]) {
            if (rect[0] < y && y < rect[2]) {
              s.push([rect[1], rect[3]])
            }
          }
        }
      }
      s = s.sort((a, b) => a[0] - b[0])
  
      let ret = [], left = 0, right = 0
      for (let i = 0; i < s.length; ++i) {
        if (s[i][0] <= right) {
          if (s[i][1] > right) {
            right = s[i][1]
          }
        } else {
          ret.push(right)
          left = s[i][0]
          right = s[i][1]
          ret.push(left)
        }
      }
      ret.push(right)
      ret.push(size[0])
      return ret
    }
  
    // self.start = async () => {
    self.start =  () => {
      let cw = 1 << 11 >> 5, ch = 1 << 11, canvas
      if (typeof document !== "undefined") {
        canvas = document.createElement("canvas")
        canvas.width = 1
        canvas.height = 1
        canvas.width = (cw << 5)
        canvas.height = ch
      } else {
        canvas = new canvas(cw << 5, ch)
      }
  
      const ctx = canvas.getContext("2d")
      ctx.fillStyle = ctx.strokeStyle = "red"
      ctx.textAlign = "center"
  
      function measure(d) {
        ctx.save()
        ctx.font = `${d.style} ${d.weight} ${d.size}px ${d.font}`
        let ret = ctx.measureText(d.text)
        const width = ret.actualBoundingBoxRight + ret.actualBoundingBoxLeft
        let height = ret.actualBoundingBoxAscent + 1 //ret.actualBoundingBoxDescent
        const dx = ret.actualBoundingBoxRight - ret.actualBoundingBoxLeft
        let dy = ret.actualBoundingBoxDescent / 2
        ctx.restore()
        // TODO: hack the wordcloud
        if (d.text.indexOf('p') != -1 || d.text.indexOf('g') != -1 || d.text.indexOf('q') != -1 || d.text.indexOf('y') != -1) {
          dy -= height * 0.2
          height *= 1.2
          console.log(d.text)
        }
        /*
        if (d.text.indexOf('l') != -1 || d.text.indexOf('h') != -1 || d.text.indexOf('t') != -1 || d.text.indexOf('b') != -1 || d.text.indexOf('f') != -1) {
          dy -= d.height * 0.1
          height *= 1.1
        }
        */
        return [width, height, dx, dy]
      }
  
      words.forEach((d, index) => {
        d.text = text(d, index)
        d.font = font(d, index)
        d.size = fontSize(d, index)
        d.style = fontStyle(d, index)
        d.weight = fontWeight(d, index)
        d.padding = padding(d, index)
        d.rangex = rangex(d, index)
        d.rangey = rangey(d, index)
        d.expandx = expandx(d, index)
        d.expandy = expandy(d, index)
        d.flexible = flexible(d, index)
      })
  
      let data = words
  
      data.forEach(d => {
        const x = measure(d)
        d.width = x[0] + d.padding + d.expandx
        d.height = x[1] + d.padding + d.expandy
        if (d.flexible && d.width > size[0] * 0.95) {
          let scale = (x[0] * 0.95) / (size[0] - d.padding - d.expandx)
          d.width = x[0] / scale + d.padding + d.expandx
          d.height = x[1] / scale + d.padding + d.expandy
          d.size = d.size / scale
        }
        d.dx = d.padding / 2 - x[2]
        d.dy = d.padding / 2 + x[3]
        d.display = false
      })
      data.sort((a, b) => { return b.size - a.size })
  
      if (!center) {
        center = [size[0] / 2, size[1] / 2]
      }
      for (let rect of barriers) {
        self.addRect(rect.y0, rect.x0, rect.y1, rect.x1)
      }
      nFixedRect = barriers.length
  
      for (let i = 0; i < data.length; ++i) {
        let d = data[i]
        if (d.text.length <= 1) {
          d.display = false
          continue
        }
        let mid = d.rangey[2] * size[1]
        let top_range = d.rangey[0] * size[1]
        let bottom_range = d.rangey[1] * size[1]
        let left_range = d.rangex[0] * size[0]
        let right_range = d.rangex[1] * size[0]
        let flag = 0, x, y
        for (let delta = 0; mid - delta >= top_range || mid + delta <= bottom_range; delta += grid_step) {
          if (flag) break
          for (let direction = -1; direction <= 1; direction += 2) {
            if (flag) break
            y = mid + delta * direction
            if (y < top_range || y > bottom_range
              || y - d.height / 2 < 0 || y + d.height / 2 > size[1]
              || delta == 0 && direction == 1) {
              continue
            }
            let s = self.checkHorizontal([y - d.height / 2, y - d.height / 4, y, y + d.height / 4, y + d.height / 2])
            let x0 = -1
            for (let i = 0; i < s.length; i += 2) {
              let left = Math.max(s[i], left_range), right = Math.min(s[i + 1], right_range)
              left += d.width / 2
              right -= d.width / 2
              if (right < left) {
                continue
              }
              if (left <= center[0] && center[0] <= right) {
                x0 = center[0]
                flag = 1
                break
              } else if (right <= center[0]) {
                x0 = right
              } else if (left >= center[0]) {
                if (Math.abs(center[0] - left) < Math.abs(center[0] - x0)) {
                  x0 = left
                }
              }
            }
            if (x0 != -1) {
              flag = 1
              let s = self.checkVectical([x0 - d.width / 2, x0 - d.width / 4, x0, x0 + d.width / 4, x0 + d.width / 2])
              for (let i = 0; i < s.length; i += 2) {
                let top = Math.max(s[i], top_range), bottom = Math.min(s[i + 1], bottom_range)
                top += d.height / 2
                bottom -= d.height / 2
                if (y < top || y > bottom) {
                  continue
                }
              }
              x = x0 - d.width / 2
              y = y + d.height / 2
              d.display = true
              self.addRect(y - d.height, x, y, x + d.width)
            }
          }
        }
      }
      for (let i = 0, j = nFixedRect; i < data.length; ++i) {
        if (data[i].display) {
          let pd = data[i].padding / 2
          data[i].width -= data[i].padding
          data[i].height -= data[i].padding
          rects[j][0] += data[i].dx
          rects[j][1] += data[i].dy
          rects[j][2] -= pd
          rects[j][3] -= pd
          ++j
        }
      }
  
      let previous_data = data
      data = data.filter(d => d.display)
  
      // const simulation = d3.forceSimulation(data)
      //   .force("x", d3.forceX(center[0] / 2).strength(0.05))
      //   .force("y", d3.forceY(center[1] / 2).strength(0.05))
      //   .force("collide", forceCollide())
  
      // for (let i = 0; i < 5; ++i) {
      //   // await simulation.tick()
      //   simulation.tick()
      // }
  
      data = previous_data
  
      for (let i = 0, j = nFixedRect; i < data.length; ++i) {
        if (data[i].display) {
          data[i].x = rects[j][1] + data[i].expandx // + data[i].width / 2
          data[i].y = rects[j][0] + data[i].height + data[i].expandy
          ++j
        } else {
          data[i].x = center[0] - data[i].width / 2
          data[i].y = size[1] * data[i].rangey[2] + data[i].height / 2
        }
      }
      // callback(data)
      return data;
    }
  
    self.data = function (x) {
      return arguments.length ? (words = x, self) : words
    }
  
    self.center = function (x) {
      if (arguments.length) {
        center = [+x[0], +x[1]]
        return self
      } else {
        return center
      }
    }
  
    self.size = function (x) {
      if (arguments.length) {
        size = [+x[0], +x[1]]
        rows = []
        cols = []
        return self
      } else {
        return size
      }
    }
  
    self.font = function (x) {
      return arguments.length ? (font = functor(x), self) : font
    }
  
    self.fontStyle = function (x) {
      return arguments.length ? (fontStyle = functor(x), self) : fontStyle
    }
  
    self.fontWeight = function (x) {
      return arguments.length ? (fontWeight = functor(x), self) : fontWeight
    }
  
    self.barriers = function (x) {
      return arguments.length ? (barriers = x, self) : barriers
    }
  
    self.text = function (x) {
      return arguments.length ? (text = functor(x), self) : text
    }
  
    self.expandx = function (x) {
      return arguments.length ? (expandx = functor(x), self) : expandx
    }
  
    self.flexible = function (x) {
      return arguments.length ? (flexible = functor(x), self) : flexible
    }
  
  
    self.expandy = function (x) {
      return arguments.length ? (expandy = functor(x), self) : expandy
    }
  
  
    self.rangex = function (x) {
      return arguments.length ? (rangex = functor(x), self) : rangex
    }
  
    self.rangey = function (x) {
      return arguments.length ? (rangey = functor(x), self) : rangey
    }
  
    self.flexible = function (x) {
      return arguments.length ? (flexible = functor(x), self) : flexible
    }
  
    self.fontSize = function (x) {
      return arguments.length ? (fontSize = functor(x), self) : fontSize
    }
  
    self.padding = function (x) {
      return arguments.length ? (padding = functor(x), self) : padding
    }
  
    function functor(x) {
      return typeof x === "function" ? x : function () { return x }
    }
  
    function cloudFlexible() {
      return 0
    }
  
    function cloudExpandx() {
      return 0
    }
  
    function cloudExpandy() {
      return 0
    }
  
    function cloudText(d) {
      return d.text
    }
  
    function cloudFont() {
      return "Arial"
    }
  
    function cloudFontNormal() {
      return "normal"
    }
  
    function cloudFontSize(d) {
      return Math.sqrt(d.value)
    }
  
    function cloudRangex() {
      return [0, 1, 0.5]
    }
  
    function cloudRangey() {
      return [0, 1, 0.5]
    }
  
    function cloudPadding() {
      return 1
    }
  
    return self
  }

  export {wordcloud}
back to top