查找坐标系中最不填充​​的点

时间:2019-02-04 12:41:46

标签: javascript coordinates point coordinate-systems

我有一个基本上代表屏幕的坐标系。
而且我有任意数量的职位。例如:

population = [
    {x: 100.44, 200.54},
    {x: 123.45, 678.9},
    {x: 1300.23, 435.81},
    {x: 462.23, 468.37},
    {x: 956.58, 385.38},
];

我正在寻找一种算法,该算法可以找到人口最多的点。

白色的小圆圈代表人口,红色的Xs标记点对我来说似乎很稀疏:

screenshot

我的目标是运行一个动画,将所有这些白色迷你圆圈随机移动到随机方向,并且一旦圆圈离开屏幕,它就会被传送到人口最稠密的位置,从而减少大的空白空间。

我试图通过计算每个整数坐标到每个圆的距离之和,然后选择距离总和最高的坐标来实现。仅此一项似乎已经占用大量CPU,但是我注意到该算法使圆移到我的坐标系的边界。因此,我还将每个整数坐标到每个边界整数坐标的距离总和相加。到那时,脚本基本上冻结了。因此,这绝对不是正确的方法。

我的想法不多了。我想我不需要一种完美的算法,而是需要一种在精度和性能之间保持良好平衡的算法。 最后,我希望能够在1920x1080画布上每秒运行多次该算法,并包含大约80个这些小圆。理想情况下,该算法将具有一个参数来调整精度,从而调整其使用的CPU时间。

这是我上面提到的方法。我注释掉了导致脚本冻结的行:

let circles = [
    {x: 60.44, y: 190.54},
    {x: 103.45, y: 18.9},
    {x: 390.23, y: 135.81},
    {x: 302.23, y: 28.37},
    {x: 56.58, y: 85.38},
]

function getDistance(p1, p2) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
function drawCircle(ctx,x,y,r,c) {
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, false)
    ctx.fillStyle = c
    ctx.fill()
}


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

let highestDistanceSum = 0
let coordWithHighestDistanceSum
for (let x=0; x<canvas.width; x++) {
    for (let y=0; y<canvas.height; y++) {
        let canvasCoord = {x: x, y: y}
        let distanceSum = 0
        for (let circle of circles) {
            distanceSum += getDistance(canvasCoord, circle)
        }
        /*
        // Pretend as if every pixel on the border is a circle
        // Causes massive CPU usage
        for (let x2=0; x<canvas.width; x2++) {
            distanceSum += getDistance(canvasCoord, {x: x2, y: 0})
            distanceSum += getDistance(canvasCoord, {x: x2, y: canvas.height})
        }
        for (let y2=0; y<canvas.height; y2++) {
            distanceSum += getDistance(canvasCoord, {x: 0, y: y2})
            distanceSum += getDistance(canvasCoord, {x: canvas.width, y: y2})
        }
        */
            
        if (distanceSum > highestDistanceSum) {
            coordWithHighestDistanceSum = canvasCoord
            highestDistanceSum = distanceSum
        }
    }
}


for (let p of circles) {
    drawCircle(ctx, p.x, p.y, 3, 'black')
}

drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>

3 个答案:

答案 0 :(得分:5)

(由于它是黑色画布上的白点,因此我将白点称为星号,以便于区分)

首先,您的解决方案似乎不符合您的标准。您不需要与所有恒星的距离总和最大的点。您需要距其最近的恒星最远的点。。

举例来说,例如,这样的情况:中心有一颗恒星,而距中心有一段距离有大量恒星:

enter image description here

“最大距离总和”方法可能会在红色圆圈中给出一个点(该点太靠近中心星,甚至与中心星重叠),而您想要的则更像是绿色圆圈中的某点:

enter image description here

请牢记:

  1. 首先,我们应该将屏幕划分为适当大小的正方形,我们将根据这些正方形构建一个距离图,以找到与任何恒星距离最大的正方形。

“合理大小”部分在性能方面非常重要。您使用的1920x1080分辨率在我看来太细粒度了。为了获得视觉上令人满意的结果,分辨率为48x30甚至32x20绰绰有余。

  1. 要实际构建距离图,我们可以简单地使用Breadth-first Search。我们将所有星星的位置转换为网格坐标,并将其用作BFS的起始位置。

结果将是这样的:

enter image description here

这里仍然有一个大问题:最红的方块在底部边缘!

倒角边缘和角正方形比中心坐标具有“欺骗”优势,因为没有恒星朝向一侧(对于角正方形,甚至没有3个边)。因此,角落和边缘正方形与任何恒星之间的距离极有可能。

您的艺术作品在视觉上不是很令人愉悦吗?因此,我们可以通过过滤掉某个填充内的结果来作弊。幸运的是,BFS的结果默认情况下是排序的,因此我们可以对结果进行迭代,直到找到适合所需范围的结果为止。

下面是带注释的完整代码。即使可以看到距离图,整个过程也需要20毫秒,对于一个webgl片段(以@ 30fps〜33ms /帧的速度运行)应该足够了

此解决方案还将处理少数情况,即几颗恒星在同一帧外移出。在那种情况下,只需从BFS的结果中获取几个不同的坐标即可。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body style="margin: 0; padding: 0;">
  <canvas id="canvas" style="display: block;"></canvas>
  <script>
    // higher GRID_WIDTH = better result, more calculation time
    // We will caculate gridHeight later based on window size
    const GRID_WIDTH = 48;
    const GRID_PADDING = 3;

    const heatMapColors = [
      '#ffffff',
      '#ffdddd',
      '#ffbbbb',
      '#ff9999',
      '#ff7777',
      '#ff5555',
      '#ff3333',
      '#ff0000'
    ]

    const init = () => {
      var circles = [];
      for (var i = 0; i < 90; i++) {
        circles.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight
        });
      }

      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const ctx = canvas.getContext("2d");

      const cellSize = window.innerWidth / GRID_WIDTH;
      const gridHeight = Math.ceil(canvas.height / cellSize);

      update(ctx, circles, GRID_WIDTH, gridHeight, cellSize);
    }

    const update = (ctx, circles, gridWidth, gridHeight, cellSize) => {
      const start = new Date();

      // Perform a BFS from all stars to find distance of each rect from closest star
      // After BFS visitedCoords will be an array of all grid rect, with distance-from-star (weight) sorted in ascending order
      var bfsFrontier = getGridCoordOfStars(circles, cellSize).map(coord => ({ ...coord, weight: 0 }));
      var visitedCoords = [...bfsFrontier];

      while (bfsFrontier.length > 0) {
        const current = bfsFrontier.shift();
        const neighbors = getNeighbors(current, gridWidth, gridHeight);

        for (let neighbor of neighbors) {
          if (visitedCoords.findIndex(weightedCoord => coordsEqual(weightedCoord, neighbor)) === -1) {
            visitedCoords.push(neighbor);
            bfsFrontier.push(neighbor);
          }
        }
      }

      // Visualize heatmap
      for (let coord of visitedCoords) {
        drawRect(ctx, coord.x * cellSize, coord.y * cellSize, cellSize, cellSize, heatMapColors[Math.min(coord.weight, heatMapColors.length - 1)]);
      }

      const emptiestCoord = getLastCoordWithinPadding(visitedCoords, gridWidth, gridHeight, GRID_PADDING);
      const emptiestPosition = {
        x: (emptiestCoord.x + 0.5) * cellSize,
        y: (emptiestCoord.y + 0.5) * cellSize
      }

      drawCircle(ctx, emptiestPosition.x, emptiestPosition.y, 5, 'yellow');
      for (let p of circles) {
        drawCircle(ctx, p.x, p.y, 3, 'black')
      }

      console.log(`Processing time: ${new Date().getTime() - start.getTime()} ms`);
    }

    const drawCircle = (ctx, x, y, r, c) => {
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI, false)
      ctx.fillStyle = c
      ctx.fill()
    }

    const drawRect = (ctx, x, y, width, height, c) => {
      ctx.beginPath();
      ctx.rect(x, y, width, height);
      ctx.fillStyle = c;
      ctx.fill();
    }

    // Convert star position to grid coordinate
    // Don't need to worry about duplication, BFS still work with duplicates
    const getGridCoordOfStars = (stars, cellSize) =>
      stars.map(star => ({
        x: Math.floor(star.x / cellSize),
        y: Math.floor(star.y / cellSize)
      }))

    const coordsEqual = (coord1, coord2) => coord1.x === coord2.x && coord1.y === coord2.y;

    const getNeighbors = (weightedCoord, gridWidth, gridHeight) => {
      var result = [];
      if (weightedCoord.x > 0) result.push({ x: weightedCoord.x - 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })
      if (weightedCoord.x < gridWidth - 1) result.push({ x: weightedCoord.x + 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })

      if (weightedCoord.y > 0) result.push({ x: weightedCoord.x, y: weightedCoord.y - 1, weight: weightedCoord.weight + 1 })
      if (weightedCoord.y < gridHeight - 1) result.push({ x: weightedCoord.x, y: weightedCoord.y + 1, weight: weightedCoord.weight + 1 })

      return result;
    }

    // loop through a BFS result from bottom to top and return first occurence inside padding
    const getLastCoordWithinPadding = (coords, gridWidth, gridHeight, padding) => {
      for (let i = coords.length - 1; i > 0; i--) {
        const coord = coords[i];
        if (
          coord.x >= padding
          && coord.x < gridWidth - padding - 1
          && coord.y >= padding
          && coord.y < gridHeight - padding - 1
        ) return coord;
      }

      // This does not happen with current logic, but I leave it here to catch future code changes
      return coords[coords.length - 1];
    }

    init();
  </script>
</body>

</html>


修改

我刚刚阅读了@ArneHugo的答案,我看到将边框和星星一起添加,因为BFS的起始位置也将起作用。速度稍慢一些,但效果更好。

这是实现他们想法的另一个版本:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body style="margin: 0; padding: 0;">
  <canvas id="canvas" style="display: block;"></canvas>
  <script>
    const GRID_WIDTH = 48; // We will caculate gridHeight based on window size

    const heatMapColors = [
      '#ffffff',
      '#ffdddd',
      '#ffbbbb',
      '#ff9999',
      '#ff7777',
      '#ff5555',
      '#ff3333',
      '#ff0000'
    ]

    const init = () => {
      var circles = [];
      for (var i = 0; i < 90; i++) {
        circles.push({
          x: Math.random() * window.innerWidth,
          y: Math.random() * window.innerHeight
        });
      }

      const canvas = document.getElementById('canvas')
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const ctx = canvas.getContext("2d");

      const cellSize = window.innerWidth / GRID_WIDTH;
      const gridHeight = Math.ceil(canvas.height / cellSize); // calculate gridHeight

      // cache border coords array since it's never changed
      const borderCoords = getBorderCoords(GRID_WIDTH, gridHeight);

      update(ctx, circles, GRID_WIDTH, gridHeight, cellSize, borderCoords);
    }

    const update = (ctx, circles, gridWidth, gridHeight, cellSize, borderCoords) => {
      const start = new Date();

      // Perform a BFS from all stars to find distance of each rect from closest star
      // After BFS visitedCoords will be an array of all grid rect, with distance-from-star (weight) sorted in ascending order

      var bfsFrontier = borderCoords.concat(
        getGridCoordOfStars(circles, cellSize).map(coord => ({ ...coord, weight: 0 }))
      );

      var visitedCoords = [...bfsFrontier];

      while (bfsFrontier.length > 0) {
        const current = bfsFrontier.shift();
        const neighbors = getNeighbors(current, gridWidth, gridHeight);

        for (let neighbor of neighbors) {
          if (visitedCoords.findIndex(weightedCoord => coordsEqual(weightedCoord, neighbor)) === -1) {
            visitedCoords.push(neighbor);
            bfsFrontier.push(neighbor);
          }
        }
      }

      // Visualize heatmap
      for (let coord of visitedCoords) {
        drawRect(ctx, coord.x * cellSize, coord.y * cellSize, cellSize, cellSize, heatMapColors[Math.min(coord.weight, heatMapColors.length - 1)]);
      }

      const emptiestCoord = visitedCoords[visitedCoords.length - 1];
      const emptiestPosition = {
        x: (emptiestCoord.x + 0.5) * cellSize,
        y: (emptiestCoord.y + 0.5) * cellSize
      }

      drawCircle(ctx, emptiestPosition.x, emptiestPosition.y, 5, 'yellow');
      for (let p of circles) {
        drawCircle(ctx, p.x, p.y, 3, 'black')
      }

      console.log(`Processing time: ${new Date().getTime() - start.getTime()} ms`);
    }

    const drawCircle = (ctx, x, y, r, c) => {
      ctx.beginPath()
      ctx.arc(x, y, r, 0, 2 * Math.PI, false)
      ctx.fillStyle = c
      ctx.fill()
    }

    const drawRect = (ctx, x, y, width, height, c) => {
      ctx.beginPath();
      ctx.rect(x, y, width, height);
      ctx.fillStyle = c;
      ctx.fill();
    }

    const getBorderCoords = (gridWidth, gridHeight) => {
      var borderCoords = [];
      for (var x = 0; x < gridWidth; x++) {
        for (var y = 0; y < gridHeight; y++) {
          if (x === 0 || y === 0 || x === gridWidth - 1 || y === gridHeight - 1) borderCoords.push({ x, y, weight: 0 })
        }
      }

      return borderCoords;
    }

    // Convert star position to grid coordinate and filter out duplicates
    const getGridCoordOfStars = (stars, cellSize) => stars.map(star => ({
      x: Math.floor(star.x / cellSize),
      y: Math.floor(star.y / cellSize)
    }))

    const uniqueCoord = (arr) => arr.filter((candidate, index) => arr.findIndex(item => coordsEqual(item, candidate)) === index);

    const coordsEqual = (coord1, coord2) => coord1.x === coord2.x && coord1.y === coord2.y;

    const getNeighbors = (weightedCoord, gridWidth, gridHeight) => {
      var result = [];
      if (weightedCoord.x > 0) result.push({ x: weightedCoord.x - 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })
      if (weightedCoord.x < gridWidth - 1) result.push({ x: weightedCoord.x + 1, y: weightedCoord.y, weight: weightedCoord.weight + 1 })

      if (weightedCoord.y > 0) result.push({ x: weightedCoord.x, y: weightedCoord.y - 1, weight: weightedCoord.weight + 1 })
      if (weightedCoord.y < gridHeight - 1) result.push({ x: weightedCoord.x, y: weightedCoord.y + 1, weight: weightedCoord.weight + 1 })

      return result;
    }

    init();
  </script>
</body>

</html>

答案 1 :(得分:3)

原始解决方案(带有修改)

这里是一个例子,尽管我认为如果您有更多的圈子,可能会更有趣。

  • 我只计算到几个最近的圆圈的距离
  • 根据您的要求,我已使画布的边框排斥新的圆圈,因此您在边框上获得新圆圈的可能性要小得多。 通过计算到边缘([canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0])的距离以及到每个现有圆的距离来完成。

(我也已更改为更具功能性的样式,因为我发现这样做比较容易,尽管这不是必需的。)

const numberOfCirclesToGetDistanceTo = 2

let circles = [
    {x: 60.44, y: 190.54},
    {x: 103.45, y: 18.9},
    {x: 390.23, y: 135.81},
    {x: 302.23, y: 28.37},
    {x: 56.58, y: 85.38},
]

function getDistance(p1, p2) {
    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
function drawCircle(ctx,x,y,r,c) {
    ctx.beginPath()
    ctx.arc(x, y, r, 0, 2 * Math.PI, false)
    ctx.fillStyle = c
    ctx.fill()
}


const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

function getCoordWithHighestDistanceSum() {
    const xList = Array(canvas.width).fill().map((_, index) => index)
    const yList = Array(canvas.height).fill().map((_, index) => index)
    const coords = xList.flatMap(x => yList.reduce((coords, y) => coords.concat({ x, y }), []))

    const ascending = (a, b) => a - b
    const sumTotal = (sum, next) => sum + next

    const coordsWithDistance = coords.map(coord => {
        const distances = [
            ...circles.map(circle => getDistance(coord, circle)),
            ...[canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0],
        ]

        return {
            coord,
            dist: distances
                .sort(ascending)
                .slice(0, numberOfCirclesToGetDistanceTo)
                .reduce(sumTotal, 0)
        }
    })

    return coordsWithDistance
        .sort((a, b) => b.dist - a.dist)
        [0].coord
}

const coordWithHighestDistanceSum = getCoordWithHighestDistanceSum()

for (let p of circles) {
    drawCircle(ctx, p.x, p.y, 3, 'black')
}

drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>

交互式版本

此处是更具交互性的版本,因此您可以测试其工作方式。如您所见,大多数情况下,随机生成其他圆时,沿边缘的人口最少。此外,检查距离时您所用的圆圈越多,在选择人口密度最低的区域时,您越趋向于边缘和角落。

查找人口最少的区域的逻辑与原始解决方案中的逻辑相同。

let circles = []
let coordWithHighestDistanceSum = void 0

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext("2d")

const xList = Array(canvas.width).fill().map((_, index) => index)
const yList = Array(canvas.height).fill().map((_, index) => index)
const coords = xList.flatMap(x => yList.reduce((coords, y) => coords.concat({ x, y }), []))

function render() {
    ctx.clearRect(0, 0, canvas.width, canvas.height)

    function drawCircle(ctx,x,y,r,c) {
        ctx.beginPath()
        ctx.arc(x, y, r, 0, 2 * Math.PI, false)
        ctx.fillStyle = c
        ctx.fill()
    }

    circles.forEach(circle => drawCircle(ctx, circle.x, circle.y, 3, 'black'))

    if (coordWithHighestDistanceSum) {
        drawCircle(ctx, coordWithHighestDistanceSum.x, coordWithHighestDistanceSum.y, 5, 'red')
    }
}

function generateCircles() {
    const nofCircles = Number(document.getElementById('nofCircles').value)

    const randomCoord = () => coords[Math.floor(Math.random() * coords.length)]

    circles = Array(nofCircles).fill().map(randomCoord)
    findLeastPopulatedCoordinate()

    render()
}

function findLeastPopulatedCoordinate() {
    const nofCirclesToSumDistanceTo = Number(document.getElementById('nofCirclesToSumDistanceTo').value)

    const ascending = (a, b) => a - b
    const sumTotal = (sum, next) => sum + next

    function getDistance(p1, p2) {
        return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
    }

    coordWithHighestDistanceSum = coords
        .map(coord => ({
            coord,
            dist: []
                .concat(circles.map(circle => getDistance(coord, circle)))
                .concat([canvas.width - coord.x, coord.x - 0, canvas.height - coord.y, coord.y -0])
                .sort(ascending)
                .slice(0, nofCirclesToSumDistanceTo)
                .reduce(sumTotal, 0)
        }))
        .sort((a, b) => b.dist - a.dist)
        [0].coord

    render()
}

generateCircles()
findLeastPopulatedCoordinate()
<div>
    <label>Number of circles</label>
    <input type="number" id="nofCircles" value="30" />
</div>
<div>
    <label>Number of circles to sum distance to</label>
    <input type="number" id="nofCirclesToSumDistanceTo" value="1" />
</div>
<button onclick="generateCircles()">Generate circles</button>
<button onclick="findLeastPopulatedCoordinate()">Find least populated coordinate</button>
<canvas id="canvas" width="400" height="200" style="border:1px solid #d3d3d3;"></canvas>

答案 2 :(得分:1)

您可以将画布视为具有1080×1920列和行的矩阵,以x表示空白区域的1s初始化,x th 列的y th 行的初始化为0。代表该坐标点。现在,您需要在二进制矩阵中找到最大的空白矩形。

Dr. Dobb's article包含解决该问题的最快算法之一。您可以在互联网上找到JavaScript实现,也可以自己实现。

您还可以考虑找到最大的空平方。

var canvas = document.querySelector("#canvas-1");
var rctx = canvas.getContext("2d");
var ncols = canvas.width;
var nrows = canvas.height;
var npoints = +canvas.dataset.points;
var matrix = Array(nrows).fill(0).map(function() {
  return Array(ncols).fill(1);
});
var i, x, y, t0, t1, maxrect, maxsquare;

/*
 * For consistency with algorithms, the matrix is initialized with 1s 
 * representing the blank area and the points are represented with 0s
 */
for (i = 0; i < npoints; i++) {
  x = Math.floor(Math.random() * ncols);
  y = Math.floor(Math.random() * nrows);
  matrix[y][x] = 0;
}

t0 = new Date();
maxrect = maximalRectangle(matrix);
t1 = new Date();
console.log("Rectangle found in %dms", t1 - t0);

t0 = new Date();
maxsquare = maximalSquare(matrix);
t1 = new Date();
console.log("Square found in %dms", t1 - t0);

/*
 * Render the results
 */
rctx.fillStyle = "rgba(255,0,0,.5)";
rctx.fillRect(maxrect.left, maxrect.top, maxrect.right - maxrect.left + 1, maxrect.bottom - maxrect.top + 1);

rctx.fillStyle = "rgba(0,0,255,.5)";
rctx.fillRect(maxsquare.left, maxsquare.top, maxsquare.right - maxsquare.left + 1, maxsquare.bottom - maxsquare.top + 1);

rctx.fillStyle = "rgb(255,255,255)";
for (y = 0; y < nrows; y++) {
  for (x = 0; x < ncols; x++) {
    if (matrix[y][x] === 0) {
      rctx.fillRect(x, y, 1, 1);
    }
  }
}

/*
 * implementation of this answer:
 * https://stackoverflow.com/a/20039017/87015
 */
function maximalRectangle(matrix) {
  var best_area = 0;
  var best_rect = {};
  var M = matrix[0].length;
  var N = matrix.length;
  var c = Array(M + 1).fill(0);
  var s = [];
  var m, n, open_width, area, prev;
  for (n = 0; n < N; n++) {
    for (m = 0; m < M; m++) {
      if (matrix[n][m] === 0) {
        c[m] = 0;
      } else {
        c[m]++;
      }
    }
    open_width = 0;
    for (m = 0; m < M + 1; m++) {
      if (c[m] > open_width) {
        s.push({
          m: m,
          w: open_width
        });
        open_width = c[m];
      } else if (c[m] < open_width) {
        do {
          prev = s.pop();
          area = open_width * (m - prev.m);
          if (area > best_area) {
            best_area = area;
            best_rect.left = prev.m;
            best_rect.right = m - 1;
            best_rect.top = n - open_width + 1;
            best_rect.bottom = n;
          }
          open_width = prev.w;
        } while (c[m] < open_width);
        open_width = c[m];
        if (open_width != 0) {
          s.push(prev);
        }
      }
    }
  }
  return {
    area: best_area,
    left: best_rect.left,
    top: best_rect.top,
    right: best_rect.right,
    bottom: best_rect.bottom
  };
}

/*
 * (possibly buggy) implementation of this answer:
 * https://stackoverflow.com/a/1726667/87015
 */
function maximalSquare(matrix) {
  var best_length = 0;
  var best_square = {};
  var M = matrix[0].length;
  var N = matrix.length;
  var c = Array(M + 1).fill(0);
  var n, m, temp, prev = 0;
  for (n = 1; n <= N; n++) {
    for (m = 1; m <= M; m++) {
      temp = c[m];
      if (matrix[n - 1][m - 1] === 1) {
        c[m] = Math.min(Math.min(c[m - 1], prev), c[m]) + 1;
        if (best_length < c[m]) {
          best_length = c[m];
          best_square.left = m - best_length;
          best_square.right = m - 1;
          best_square.top = n - best_length;
          best_square.bottom = n - 1;
        }
      } else {
        c[m] = 0;
      }
      prev = temp;
    }
  }
  return {
    area: best_length * best_length,
    left: best_square.left,
    top: best_square.top,
    right: best_square.right,
    bottom: best_square.bottom
  };
}
<canvas id="canvas-1" width="1920" height="1080" data-points="80" style="background-color: #000;"></canvas>