O objetivo aqui é, de alguma forma, e com uso de alguma biblioteca Web, isto é, do lado do cliente, com uma lista de pontos tridimensionais, poder fazer uma plotagem de forma simples.
Fase 1: Escolha
Analisando as bibliotecas que o Google me informou (as primeiras que aparecem ao pesquisar javascript 3d plotting library), temos uma lista:
- D3.js
O site de demonstrações não parece incluir plotagem 3D. - Plotly
Diz-se que não é mais mantido / foi abandonado. Todavia, a página de demonstrações inclui gráficos em três dimensões. - three.js
É muito mais complexo do que eu necessito. (ENCE‑N) - chart.js
Não possui plotagem 3D.
Resolução: Dadas as opções, escolhi a biblioteca Plotly.
Como usar o Plotly
Opções de Plotagens 3D¹
¹ Apenas as que pareciam úteis para o projeto.
- 3D Cluster Graph / Gráfico de Agrupamentos em 3D
- 3D Surface Plots / Plotagens de Superfícies em 3D
- 3D Mesh Plots / Plotagem de Malhas em 3D
- 3D Scatter Plots / Plotagem de Dispersão em 3D
Resolução
Agrupamentos / Superfícies / Malhas: Não parecem ser o quê necessito.
Dispersão: Parece ser exatamente o que preciso.
Código
Original
O código para a imagem original (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 primeiro ato do código é requisitar um arquivo CSV, com o método #Plotly.d3.csv# (que parece ser da forma)
Plotly.d3.csv(url, (erros, linhas)>null)
onde os dados estão distribuídos na forma (x_1, y_1, z_1, x_2, y_2, z_2)
, onde os três primeiros valores são as coordenadas dos pontos em azul, e os outros três, dos pontos em cinza.
Gostei dessa função. Vamos escrever 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, posso requisitar dados em CSV e esperar os dados com um simples #await#.
Mas, pelo visto com a função #desempacota#, que dada uma #chave# (ou o nome da coluna no cabeçalho do arquivo CSV), retorna a respectiva coluna, as #linhas# estão num formato de uma lista de objetos do tipo #“chave” / “nome da coluna”: valor#, assim:
[{ chave1: valor1, chave2: valor2, ... }]
Isso não parece certo. Vamos converter para isso:
{ chave1: valor1[], chave2: valor2[], ... }
O processo é 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, agora, escrever uma função que, dados os pontos e certas configurações opicionais, 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 juntando 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 }
]);
})();
Agora, é uma boa hora para se fazer um teste 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 resultados.
Agora, testando a distribuição de fibonacci, na qual para se distribuir n
pontos igualmente na região [0; 1[^2
. Seguindo essa distribuição, o ponto i
ficará na posição \left(\left\{\dfrac{i}{\phi}\right\}; \dfrac{i}{n}\right)
, onde \{x\}
é a parte fracionária de x
. Como estamos plotando em 3D, vamos adicionar um valor aleatório k
de modo a termos o ponto \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' });