Делала визуализацию данных для Китая 🇨🇳Олимпийские игры🏅

внешний интерфейс Canvas
Делала визуализацию данных для Китая 🇨🇳Олимпийские игры🏅

предисловие

Олимпийские игры 2020 года в Токио были открыты уже много дней.Я до сих пор помню, что когда я был ребенком, я смотрел Олимпийские игры на Олимпийских играх 2008 года в Пекине.Заглавная песняПекин приветствует вас,я тогда только в начальную школу ходил.Практически каждую игру сборной Китая надо смотреть.В то время кровь кипела.Мгновением ока был 2021 год,и я тоже из первоклассника изменился программисту, который каждый день набирает код 👩‍💻, смотрите До олимпиады не так много времени, но я хочу разделить свои усилия.Так как я программист, я думаю о том, что я могу сделать для олимпиады? Первое, что пришло в голову, это визуализировать количество олимпийских медалей 🏅, ведь просто глядя на табличные данные нельзя отразить офигенность нашего Китая 🐂, вчера Су Шен даже сотворил чудо, азиатская скорость, не много чепухи , просто начните писать.

получение данных

Давайте сначала посмотрим на таблицу олимпийских медалей.Эта вещь должна быть получена через интерфейс.Я не могу написать ее от руки,и она каждый день обновляется.Я должен ее каждый день менять?Собирается быть гадом, собираюсь использоватьpuppeteerЧтобы имитировать поведение браузера, получить нативный DOM страницы, а затем получить данные таблицы, и тогда мне очень захотелось это сделать, и я написал следующий код:

const puppeteer = require('puppeteer')
​
async function main() {
  // 启动chrome浏览器
  const browser = await puppeteer.launch({
    // // 指定该浏览器的路径
    // executablePath: chromiumPath,
    // 是否为无头浏览器模式,默认为无头浏览器模式
    headless: false,
  })
​
  // 在一个默认的浏览器上下文中被创建一个新页面
  const page1 = await browser.newPage()
​
  // 空白页刚问该指定网址
  await page1.goto(
    'https://tiyu.baidu.com/tokyoly/home/tab/%E5%A5%96%E7%89%8C%E6%A6%9C/from/pc'
  )
​
  // 等待title节点出现
  await page1.waitForSelector('title')
​
  // 用page自带的方法获取节点
​
  // 用js获取节点
  const titleDomText2 = await page1.evaluate(() => {
    const titleDom = document.querySelectorAll('#kw')
    return titleDom
  })
  console.log(titleDomText2, '查看数据---')
  // 截图
  //await page1.screenshot({ path: 'google.png' })
  //   await page1.pdf({
  //     path: './baidu.pdf',
  //   })
  browser.close()
}
main()
​

Потом, когда я был очень взволнован, чтобы пойти на результат, он оказался пустым. Создал ли Baidu протокол для защиты от сканеров? До сих пор не разобрался. Если есть собрание старшего брата, пожалуйста, дайте мне указания!

image-20210731112152170

Но этоpuppeteer, Эта библиотека немного корова. Она может делать скриншоты веб-страниц, создавать PDF-файлы и перехватывать запросы. На самом деле, это немного похоже на автоматическое тестирование. Заинтересованные студенты могут узнать об этом самостоятельно, что не является предметом данной статьи.

интерфейс получить

Потом в это время я начал с ума сходить с Baidu, и начал искать готовые.api, Действительно трудно найти место, чтобы прорваться через железные башмаки, и это не требует усилий. Это было найдено мной.Оказывается, что большой парень уже начал это делать.В это время для меня проблема напрямую запросить этот интерфейс локально. домен. У меня болит голова, когда я смотрю на вещи, но это не имеет значения, я напрямую запускаю сервер с узлом, я создаю узел для запроса этого интерфейса, а затем выполняю междоменную настройку в фоновом режиме, я получаю данные интерфейса напрямую, и я используйте экспресс для создания фонового сервиса.Сервер просто возился с ним. код показывает, как показано ниже:

const axios = require('axios')
const express = require('express')
const request = require('request')
const app = express()
​
const allowCrossDomain = function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  res.header('Access-Control-Allow-Credentials', 'true')
  next()
}
app.use(allowCrossDomain)
​
app.get('/data', (req, res) => {
  request(
    {
      url: 'http://apia.yikeapi.com/olympic/?appid=43656176&appsecret=I42og6Lm',
      method: 'GET',
      headers: { 'Content-Type': 'application/json' },
    },
    function (error, response, body) {
      if (error) {
        res.send(error)
      } else {
        res.send(response)
      }
    }
  )
})
app.listen(3030)
​

Таким образом, я реализовал переадресацию интерфейса и решил междоменную проблему.На стойке регистрации я напрямую использовал выборку для запроса данных, а затем выполнял слой преобразования данных, но этот интерфейс нельзя запрашивать часто, и он падает каждый раз. время, которое действительно раздражает. Хорошо, поэтому я делаю это напрямую.Выполняется операция, данные сохраняются в локальном хранилище, а затем выполняется обычное обновление, которое примерно раз в день. Это гарантирует достоверность данных. код показывает, как показано ниже:

getData() {
  let curTime = Date.now()
  if (localStorage.getItem('aoyun')) {
    let { list, time } = JSON.parse(localStorage.getItem('aoyun'))
    console.log(curTime - time, '查看时间差')
    if (curTime - time <= 24 * 60 * 60 * 60) {
      this.data = list
    } else {
      this.fetchData()
    }
  } else {
    this.fetchData()
  }
}
​
fetchData() {
  fetch('http://localhost:3030/data')
    .then((res) => res.json())
    .then((res) => {
      const { errcode, list } = JSON.parse(res.body)
      if (errcode === 100) {
        alert('接口请求太频繁')
      } else if (errcode === 0) {
        this.data = list
        const obj = {
          list,
          time: Date.now(),
        }
        localStorage.setItem('aoyun', JSON.stringify(obj))
      }
    })
    .catch((err) => {
      console.log(err)
    })
}

Данные показаны на следующем рисунке:

image-20210731114644399

Представление гистограммы

На самом деле, я думал о многих способах выразить количество китайских золотых медалей, но в конце концов решил использовать двумерную гистограмму для ее представления и в то же время создал анимационные эффекты. Я все еще использую родную рукописную гистограмму вместо нее.EchartsБиблиотека, давайте сначала посмотрим на гистограмму:

柱状图

Некоторые элементы можно проанализировать по рисунку

  1. Ось X, ось Y и несколько прямых линий, поэтому я просто инкапсулирую метод рисования прямых линий.
  2. Есть много прямоугольников, инкапсулируйте метод рисования прямоугольников
  3. И какие-то галочки и линейки
  4. Последнее - анимационный эффект входа

Инициализация холста

Создайте холст на странице и получите некоторые свойства холста, а также привяжите событие движения к холсту. код показывает, как показано ниже:

get2d() {
    this.canvas = document.getElementById('canvas')
    this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
    this.ctx = this.canvas.getContext('2d')
    this.width = canvas.width
    this.height = canvas.height
  }

рисовать оси

Координатная ось также по сути является прямой линией.Две точки, соответствующие прямой линии, на самом деле являются разными соответствующими конечными точками, поэтому я напрямую инкапсулировал метод рисования прямой линии:

  // 画线的方法
  drawLine(x, y, X, Y) {
    this.ctx.beginPath()
    this.ctx.moveTo(x, y)
    this.ctx.lineTo(X, Y)
    this.ctx.stroke()
    this.ctx.closePath()
  }

Некоторые люди могут быть незнакомы с холстом, здесь я кратко скажу, откройте путь, переместите кисть в начальную точку, затем проведите прямую линию в конечную точку, а затем обведите этот шаг холст для рендеринга, очень важно, много small Если не писать белым, прямой линии не получится, и тогда путь будет закрыт. конец!

Чтобы нарисовать ось координат, мы сначала определяем, где находится начало координат.Сначала мы сжимаем холст внутрь на расстояние отступа, а затем вычисляем фактическую ширину и высоту холста.

код показывает, как показано ниже:

initChart() {
  // 留一个内边距
  this.padding = 50
  // 算出画布实际的宽度和高度
  this.cHeight = this.height - this.padding * 2
  this.cWidth = this.width - this.padding * 2
  // 计算出原点
  this.originX = this.padding
  this.originY = this.padding + this.cHeight
}

С исходной точкой мы можем нарисовать оси X и Y, просто добавимфактический холстСоответствующая ширина и высота просто прекрасны. код показывает, как показано ниже:

 //设置canvas 样式
  this.setCanvasStyle()
  // 画x轴
  this.drawLine(
    this.originX,
    this.originY,
    this.originX,
    this.originY - this.cHeight
  )
  // 画Y轴
  this.drawLine(
    this.originX,
    this.originY,
    this.originX + this.cWidth,
    this.originY
  )

Первая функция — задать стиль кисти холста, по сути, эта штука ни о чем. Посмотрим на эффект:

X轴和Y轴

Многие думают, что это конец хахаха, тогда вы слишком много думаете.Ширина холста, которую я установил для линии рисования, составляет 1px.Почему ширина линии на картинке выглядит как 2px? Эту проблему нельзя обнаружить без внимательного наблюдения, поэтому мы должны научиться думать о том, в чем проблема? Собственно, это то, что я вижуEchartsИсходный код найден, учиться без размышлений бесполезно, думать без обучения опасно!

Пасхальное яйцо — как CANVAS рисует прямую линию 1PX

Здесь я привожу пример, вы поймете, предположим, я хочу провести прямую линию от (50, 10) до (200, 10). Чтобы нарисовать линию, браузер сначала достигает начальной начальной точки (50, 10). Эта линия имеет ширину 1 пиксель, поэтому оставьте по 0,5 пикселя с каждой стороны. Таким образом, в основном начальная точка простирается от (50, 9,5) до (50, 10,5). В настоящее время браузеры не могут отображать 0,5 пикселя на экране — минимальный порог составляет 1 пиксель. У браузера нет другого выбора, кроме как расширить границы начальной точки до фактических границ пикселей на экране. Он добавляет в 0,5 раза больше «мусора» с обеих сторон. Так что теперь начальная точка расширяется от (50, 9) до (50, 11), так что она кажется шириной 2 пикселя. детали следующим образом:

实际效果图

Теперь вы должны понятьБраузеры не могут отображать 0,5 пикселя, вау, округлил вверх, зная проблему, мы должны иметь решение

Пан ХОЛСТ

ctx.translate(x,y) Этот метод:

translate()метод, переместите холст в горизонтальном направлении исходной точки x и вертикальном направлении исходной точки yпреобразование перевода

Как показано на рисунке:

canvas平移

Проще говоря, после того, как вы внесете изменения в холст, все точки, которые вы нарисовали ранее, будут смещены относительно друг друга. Итак, вернемся к нашему вопросу, каково решение? Поскольку я смещаю холст в целом на 0,5, исходные координаты (50, 10) становятся (50,5, 10,5) и (200,5, 10,5) в порядке, и тогда браузеру все еще нужно резервировать пиксели для рисования, поэтому это чтобы нарисовать OK от (50,5, 10) до (50,5, 11), что составляет 1 пиксель. Давай попробуем.

код показывает, как показано ниже:

this.ctx.translate(0.5, 0.5)
// 画x轴
this.drawLine(
  this.originX,
  this.originY,
  this.originX,
  this.originY - this.cHeight
)
// 画Y轴
this.drawLine(
  this.originX,
  this.originY,
  this.originX + this.cWidth,
  this.originY
)
this.ctx.translate(-0.5, -0.5)

После того, как зачет завершен, еще нужно восстановить прошлое, и на это нужно еще уделить большое внимание. Нарисовал две картинки для сравнения:

Aпосле смещенияBДо смещения

Нечего сказать, смотрите здесь, если вы считаете, что это полезно для вас, или если вы чему-то научились, я надеюсь, вам понравится, прокомментируйте и добавьте в избранное.

рисовать линейку

Теперь у нас есть только ось X и ось Y, которые пусты. Я добавлю несколько шкал внизу оси X и оси Y. Масштаб, соответствующий оси X, должен быть названием каждой страны. Общая идея состоит в том, чтобы сделать сегмент по количеству данных, а затем перейти к заполнению просто отлично.

код показывает, как показано ниже:

drawXlabel() {
  const length = this.data.slice(0, 10).length
  this.ctx.textAlign = 'center'
  for (let i = 0; i < length; i++) {
    const { country } = this.data[i]
    const totalWidth = this.cWidth - 20
    const xMarker = parseInt(
      this.originX + totalWidth * (i / length) + this.rectWidth
    )
    const yMarker = this.originY + 15
    this.ctx.fillText(country, xMarker, yMarker, 40) // 文字
  }
}

Здесь я перехватил первые 10 стран, и идея разделения состоит в том, чтобы оставить пустыми 20 пикселей с обеих сторон.Сначала мы определяем ширину каждой гистограммы, которая предполагается равной 30, что соответствует этому.rectWidth выше, и тогда координаты каждого текста на самом деле это легко вычислить, начальный х + количество секущихся концов + ширину прямоугольника можно нарисовать

Как показано на рисунке:

X轴标尺

После того, как ось X нарисована, мы начинаем рисовать ось Y. Общая идея оси Y состоит в том, чтобы использовать максимальное количество медалей для выполнения сегментации. Здесь я разделю ее на 6 сегментов.

// 定义Y轴的分段数
this.ySegments = 6
//定义字体最大宽度
this.fontMaxWidth = 40

Далее, давайте начнем вычислять координату Y каждой точки на оси Y. Координату X на самом деле очень легко вычислить. вычисляем координату оси Y, на что здесь надо обратить внимание.Да, наши вторичные координаты относятся к верхнему левому углу, поэтому координаты оси Y должны уменьшаться вверх.

drawYlabel() {
  const { jin: maxValue } = this.data[0]
  this.ctx.textAlign = 'right'
  for (let i = 1; i <= this.ySegments; i++) {
    const markerVal = parseInt(maxValue * (i / this.ySegments))
    const xMarker = this.originX - 5
    const yMarker =
      parseInt((this.cHeight * (this.ySegments - i)) / this.ySegments) +
      this.padding +
      20
    this.ctx.fillText(markerVal, xMarker, yMarker) // 文字
  }
}

Самые большие данные - это первые данные массива, а затем каждый масштаб - это доля масштаба. Поскольку координаты оси Y уменьшаются, соответствующие координаты должны быть долей 1-, потому что это только расчет .Реальная высота значка преобразуется в холст, и необходимо добавить отступ, который мы установили изначально.Поскольку текст добавляется, текст также занимает определенный пиксель, поэтому добавляется 20. OK Рисование оси Y завершено, с координатами каждого деления оси Y, и в то же время нарисуйте соответствующие сплошные линии позади него.

код показывает, как показано ниже:

this.drawLine(
  this.originX,
  yMarker - 4,
  this.originX + this.cWidth,
  yMarker - 4
)

Окончательный рендеринг выглядит следующим образом:

xy轴

нарисовать прямоугольник

все готово, начинаем рисовать прямоугольники, все так же, сначала инкапсулируем метод рисования прямоугольников, а потом нам просто нужно передать соответствующие данные и все ок.

Здесь используется собственный прямоугольный метод холста. Под параметрами понимают следующее:

rect语法

Ширина прямоугольника определяется нами.Высота прямоугольника - это высота соответствующего количества медалей на холсте, поэтому нам нужно только определить начальную точку прямоугольника.Здесь (x, y) из прямоугольник на самом деле является левым верхним углом.

код показывает, как показано ниже:

//绘制方块
drawRect(x, y, width, height) {
  this.ctx.beginPath()
  this.ctx.rect(x, y, width, height)
  this.ctx.fill()
  this.ctx.closePath()
}

Первый шаг — сделать сопоставление точек.Когда мы рисуем ось Y, мы помещаем все точки холста на ось Y в массив и не забываем поместить в него Y начала координат. Так что просто выясните, какой процент каждой медали приходится на штаб-квартиру? Затем выполните вычитание со значением Y начала координат, чтобы получить реальную координату оси Y. Координаты оси X относительно просты: добавляется координата X начала координат (доля от общей длины), а затем добавляется половина ширины прямоугольника. Этот принцип такой же, как и при рисовании текста, но текст должен располагаться по центру.

код показывает, как показано ниже:

drawBars() {
  const length = this.data.slice(0, 10).length
  const { jin: max } = this.data[0]
  const diff = this.yPoints[0] - this.yPoints[this.yPoints.length - 1]
  for (let i = 0; i < length; i++) {
    const { jin: count } = this.data[i]
    const barH = (count / max) * diff
    const y = this.originY - barH
    const totalWidth = this.cWidth - 20
    const x = parseInt(
      this.originX + totalWidth * (i / length) + this.rectWidth / 2
    )
    this.drawRect(x, y, this.rectWidth, barH)
  }
}

Нарисованная диаграмма эффекта выглядит следующим образом:

奖牌数

Оптимизация взаимодействия с прямоугольником

Черный и лысый тоже некрасиво, человек, который не знает, понятия не имеет, какая страна завоевала сколько золотых медалей.

  1. Добавьте градиент к прямоугольнику
  2. добавить немного текста

Теперь добавьте текст на основе рисования прямоугольника, код выглядит следующим образом:

this.ctx.save()
this.ctx.textAlign = 'center'
this.ctx.fillText(count, x + this.rectWidth / 2, y - 5)
this.ctx.restore()

Градиент разработан в API Canvas, createLinearGradient

createLinearGradient()Метод должен указать четыре параметра, которые представляют начальную и конечную точки сегмента линии градиента.

Затем я начал с создания градиента:

getGradient() {
  const gradient = this.ctx.createLinearGradient(0, 0, 0, 300)
  gradient.addColorStop(0, 'green')
  gradient.addColorStop(1, 'rgba(67,203,36,1)')
  return gradient
}

Затем мы преобразуем drawReact и используем здесь методы восстановления и сохранения, чтобы предотвратить загрязнение стиля текста.

//绘制方块
drawRect(x, y, width, height) {
  this.ctx.save()
  this.ctx.beginPath()
  const gradient = this.getGradient()
  this.ctx.fillStyle = gradient
  this.ctx.strokeStyle = gradient
  this.ctx.rect(x, y, width, height)
  this.ctx.fill()
  this.ctx.closePath()
  this.ctx.restore()
}

как показано на рисунке:

渐变图

Добавьте анимационные эффекты

Просто статичный не видит нашу воловью кожу🐂, поэтому эффект анимации надо постепенно увеличивать, да? На самом деле, мы можем думать обо всем процессе анимации, на самом деле есть два изменения, высота и текст гистограммы, на самом деле ось координат и координата x гистограммы неизменны, поэтому мне нужно определить только два переменных, одно начальное значение и общее значение, высота и размер текста фактически умножаются на соответствующую высоту в каждом кадре.

код показывает, как показано ниже:

// 运动相关
this.ctr = 1
this.numctr = 100

Преобразуем метод drawBars:

// 每一次的比例是多少
const dis = this.ctr / this.numctr
​
// 柱状图的高度 乘以对应的比例
const barH = (count / max) * diff * dis
​
// 文字这里取整下,因为有可能除不尽 
this.ctx.fillText(
  parseInt(count * dis),
  x + this.rectWidth / 2,
  y - 5
)
​
// 最后执行动画
if (this.ctr < this.numctr) {
  this.ctr++
  requestAnimationFrame(() => {
    this.ctx.clearRect(0, 0, this.width, this.height)
    this.drawLineLabelMarkers()
  })
}
​

Добавляйте по одному каждый раз, пока оно не станет больше, чем общее количество, а затем продолжайте перерисовывать. Эффект анимации может быть сформирован. Давайте посмотрим на гифку:

奥运gif图

Суммировать

Эта статья написана здесь, и она закончена, я резюмирую ее следующим образом:

  1. Как холст рисует прямую линию в 1px, в ней есть ямки
  2. Еще есть как проектировать анимацию, и суть в том, чтобы найти эти изменения, а потом с ними разобраться.
  3. Как сделать линейный градиент на холсте.
  4. Краулер провалил, резюмировать нечего, но один момент: кукольная библиотека, играть может каждый.

Эта статья является второй статьей на холсте для реализации визуальных диаграмм.Позже я продолжу делиться различными визуальными диаграммами, круговыми диаграммами, древовидными диаграммами, диаграммами K-линий и т. д. Я также постоянно думаю, когда пишу статью.Как выразить это лучше. Если вас интересует визуализация, ставьте лайк и подписывайтесь на 👍! , вы можете подписаться на меня нижеСтолбец визуализации данных, делиться статьей в неделю либо в 2d, либо в three.js. Каждую статью я буду создавать с душой, а не по гидрологии.

Давайте болеть за Китай 🇨🇳 Олимпийские игры вместе! Оля, дай! ! !

исходный код

Подпишитесь на официальный аккаунт [внешняя графика] и ответьте на слово [Олимпиада], чтобы получить весь исходный код.