Plotagem de pontos em 3D

#151

O obje­ti­vo aqui é, de algu­ma for­ma, e com uso de algu­ma bibli­o­te­ca Web, isto é, do lado do cli­en­te, com uma lis­ta de pon­tos tri­di­men­si­o­nais, poder fazer uma plo­ta­gem de for­ma simples.

Fase 1: Escolha

Ana­li­san­do as bibli­o­te­cas que o Goo­gle me infor­mou (as pri­mei­ras que apa­re­cem ao pes­qui­sar javas­cript 3d plot­ting library), temos uma lista:

  • D3.js
    O site de demons­tra­ções não pare­ce incluir plo­ta­gem 3D.
  • Plo­tly
    Diz-se que não é mais man­ti­do / foi aban­do­na­do. Toda­via, a pági­na de demons­tra­ções inclui grá­fi­cos em três dimensões.
  • three.js
    É mui­to mais com­ple­xo do que eu neces­si­to. (ENCE‑N)
  • chart.js
    Não pos­sui plo­ta­gem 3D.

Reso­lu­ção: Dadas as opções, esco­lhi a bibli­o­te­ca Plo­tly.

Como usar o Plotly

Opções de Plotagens 3D¹

¹ Ape­nas as que pare­ci­am úteis para o projeto.

  1. 3D Clus­ter Graph / Grá­fi­co de Agru­pa­men­tos em 3D
  2. 3D Sur­fa­ce Plots / Plo­ta­gens de Super­fí­ci­es em 3D


  3. 3D Mesh Plots / Plo­ta­gem de Malhas em 3D

  4. 3D Scat­ter Plots / Plo­ta­gem de Dis­per­são em 3D

Resolução

Agru­pa­men­tos / Super­fí­ci­es / Malhas: Não pare­cem ser o quê necessito.

Dis­per­são: Pare­ce ser exa­ta­men­te o que preciso.

Código

Original

O códi­go para a ima­gem ori­gi­nal (NTOA) é esse:

Plotly.d3.csv(
    'https://raw.githubusercontent.com/plotly/datasets/master/3d-scatter.csv',
    (err, linhas) => {
    if(err) console.error(err);
    const desempacota = (linhas, chave) => linhas.map(linha => linha[chave]);
    var traçado1 = {
        x: desempacota(linhas, 'x1'),
        y: desempacota(linhas, 'y1'),
        z: desempacota(linhas, 'z1'),
        mode: 'markers',
        marker: {
            size: 3,
            line: {
                color: 'rgba(217, 217, 217, 0.14)',
                width: 0.5
            },
            opacity: 0.8
        },
        type: 'scatter3d'
    };
    var traçado2 = {
        x: desempacota(linhas, 'x2'),
        y: desempacota(linhas, 'y2'),
        z: desempacota(linhas, 'z2'),
        mode: 'markers',
        marker: {
            color: 'rgb(127, 127, 127)',
            size: 3,
            symbol: 'circle',
            line: {
                color: 'rgb(204, 204, 204)',
                width: 1
            },
            opacity: 0.8
        },
        type: 'scatter3d'
    };
    var dados = [traçado1, traçado2];
    var leiaute = { margin: { l: 0, r: 0, b: 0, t: 0 } };
    Plotly.newPlot('div-da-plotagem', dados, leiaute);
});

Vamos por partes.

O pri­mei­ro ato do códi­go é requi­si­tar um arqui­vo CSV, com o méto­do #Plotly.d3.csv# (que pare­ce ser da forma)

Plotly.d3.csv(url, (erros, linhas)>null)

onde os dados estão dis­tri­buí­dos na for­ma (x_1, y_1, z_1, x_2, y_2, z_2), onde os três pri­mei­ros valo­res são as coor­de­na­das dos pon­tos em azul, e os outros três, dos pon­tos em cinza.

Gos­tei des­sa fun­ção. Vamos escre­ver uma Promessa.

async function pegarCSV(url){
    return new Promise((retornar, rejeitar) => {
        Ploty.d3.csv(url, (erros, linhas) => {
            if(erros) rejeitar(erros);
            else retornar(linhas);
        });
    });
}

// Uso
await pegarCSV(url);

Assim, pos­so requi­si­tar dados em CSV e espe­rar os dados com um sim­ples #await#.

Mas, pelo vis­to com a fun­ção #desem­pa­co­ta#, que dada uma #cha­ve# (ou o nome da colu­na no cabe­ça­lho do arqui­vo CSV), retor­na a res­pec­ti­va colu­na, as #linhas# estão num for­ma­to de uma lis­ta de obje­tos do tipo #“cha­ve” / “nome da colu­na”: valor#, assim:

[{ chave1: valor1, chave2: valor2, ... }]

Isso não pare­ce cer­to. Vamos con­ver­ter para isso:

{ chave1: valor1[], chave2: valor2[], ... }

O pro­ces­so é o seguinte:

function converter(linhas){
    var retorno = {}, i, linha, chave;
    for(i in linhas){
        linha = linhas[i];
        for(chave in linha){
            retorno[chave] = retorno[chave] || [];
            retorno[chave][i] = linha[chave];
        }
    }
    return retorno;
}

// E alterando pegarCSV...
            else retornar(converter(linhas));

Vamos, ago­ra, escre­ver uma fun­ção que, dados os pon­tos e cer­tas con­fi­gu­ra­ções opi­ci­o­nais, faça a plotagem.

function plotar3d(dados, cfg = {}){
    var { elemento, marcadores, leiaute } = cfg;

        elemento = elemento || 'grafico';
    leiaute = leiaute || {};
    leiaute.margin = leiaute.margin || {
        margin: { b: 0, l: 0, r: 0, t: 0 }
    };
    marcadores = marcadores || {
        color: 'white', opacity: 1,
        size: 3, line: {
            color: 'whitesmoke', width: 1
        }
    };
    if(!Array.isArray(marcadores))
        marcadores = dados.map(_ => marcadores);

    dados = dados.map( (d, i) => ({
        ...d,
        mode: 'markers', marker: marcadores[i],
        type: 'scatter3d'
    }) );
    Plotly.newPlot(elemento, dados, leiaute);
}

E jun­tan­do tudo:

class Plotar{
    static converter(linhas){...}
    static async pegarCSV(url){...}
    static plotar3d(dados, cfg = {}){...}
}

E para usar (com os dados do exemplo):

(async function(){

var dados = await Plotar.pegarCSV(url);
Plotar.plotar3d([
    { x: dados.x1, y: dados.y1, z: dados.z1 },
    { x: dados.x2, y: dados.y2, z: dados.z2 }
]);

})();

Ago­ra, é uma boa hora para se fazer um tes­te da biblioteca.


function aleatório(){
    let x = 2*Math.random() - 1;
    return (Math.tanh(x) - Math.tanh(0))/aleatório.k;
}
aleatório.k = Math.tanh(1) - Math.tanh(0);
var dados = Plotar.converter(
    Array.from({ length: 200 }).map(
        () => ({
            x: aleatório(),
            y: aleatório(),
            z: aleatório()
        })
    )),
    marcadores = {
        color: '#0ff', size: 4, opacity: 0.618
    }, leiaute = {
        width: 500, height: 500
    },
    cfg = { marcadores, leiaute, elemento: 'plotagem-1' };

Plotar.plotar3d([dados], cfg);

Bons resul­ta­dos.

Ago­ra, tes­tan­do a dis­tri­bui­ção de fibo­nac­ci, na qual para se dis­tri­buir n pon­tos igual­men­te na região [0; 1[^2. Seguin­do essa dis­tri­bui­ção, o pon­to i fica­rá na posi­ção \left(\left\{\dfrac{i}{\phi}\right\}; \dfrac{i}{n}\right), onde \{x\} é a par­te fra­ci­o­ná­ria de x. Como esta­mos plo­tan­do em 3D, vamos adi­ci­o­nar um valor ale­a­tó­rio k de modo a ter­mos o pon­to \left(\left\{\dfrac{i}{\phi}\right\}; \dfrac{i}{n}; k\right).


Math.FI = (1 + Math.sqrt(5)) / 2;
const n = 2e2;
var dados = Array.from({ length: n }).map((_, i) => ({
    x: i / Math.FI % 1,
    y: i / n,
    z: Math.random()
}));

Plotar.plotar3d([Plotar.converter(dados)], {
    marcadores: {
        color: '#fc0b', size: 3
    },
    leiaute: { width: 4e2, height: 5e2 },
    elemento: 'plotagem-2'
});