I have a requirement, where I need to assign a different color to each path segments. I approached the problem by generating a dynamic data-driven linear Gradientas following
//-----******PROOF OF CONCEPT*******---
const color = ["red", "green", "blue", "magenta"];
const svgns = 'http://www.w3.org/2000/svg';
const svgVan = document.querySelector('svg');
const lg = document.createElementNS(svgns, 'linearGradient');
lg.setAttribute('id', 'linear1');
lg.setAttribute('x1', '0%');
lg.setAttribute('y1', '0%');
lg.setAttribute('x2', '100%');
lg.setAttribute('y2', '0%');
svgVan.appendChild(lg);
color.forEach(
(a, i) => {
const stop1 = document.createElementNS(svgns, 'stop');
stop1.setAttribute('offset', i / 4);
stop1.setAttribute('stop-color', color[i]);
lg.appendChild(stop1)
const stop2 = document.createElementNS(svgns, 'stop');
stop2.setAttribute('offset', (i + 1) / 4);
stop2.setAttribute('stop-color', color[i]);
lg.appendChild(stop2)
}
)
const rect = document.createElementNS(svgns, 'rect')
rect.setAttribute('x', '100');
rect.setAttribute('y', '100');
rect.setAttribute('width', '600');
rect.setAttribute('height', '200');
rect.setAttribute('fill', 'url(#linear1)')
rect.setAttribute('stroke', 'black');
svgVan.appendChild(rect);
//-----******Application*******---
const array = [
{ x: 0, y: 80 },
{ x: 50, y: 20 },
{ x: 100, y: 50 },
{ x: 150, y: 30 },
{ x: 200, y: 40 },
{ x: 250, y: 90 },
{ x: 300, y: null },
{ x: 350, y: null },
{ x: 400, y: 20 },
{ x: 450, y: 70 },
{ x: 500, y: 60 },
];
var result = array.reduce((acc, curr, index) => acc + curr.y, 0);
for (let i = 0; i < array.length; i++) {
if (i == 0) {
array[i].z = array[i].y
} else {
array[i].z = array[i].y + array[i - 1].z
}
}
array.forEach(
(a) => {
a.pct = a.z / result
}
)
const lnr = document.createElementNS(svgns, 'linearGradient')
lnr.setAttribute('id', 'linearTest')
lnr.setAttribute('x1', '0%')
lnr.setAttribute('y1', '0%')
lnr.setAttribute('x2', '100%')
lnr.setAttribute('y2', '0%')
svgVan.appendChild(lnr);
const colorName = ["Blue", "Brown", "Crimson", "DarkCyan", "DarkMagenta", "DarkOliveGreen", "DarkOrchid", "DarkOrange", "DarkSalmon", "DarkSeaGreen", "DarkSlateBlue", "DarkSlateGrey"]
array.forEach(
(a, i, r) => {
const stop1 = document.createElementNS(svgns, 'stop');
stop1.setAttribute('offset', (i == 0) ? 0 : r[i - 1].pct);
stop1.setAttribute('stop-color', colorName[i]);
lnr.appendChild(stop1)
const stop2 = document.createElementNS(svgns, 'stop');
stop2.setAttribute('offset', a.pct);
stop2.setAttribute('stop-color', colorName[i]);
lnr.appendChild(stop2);
}
)
const pathVal = 'M' + array.filter((a) => a.y !== null).map((a, i) => a.x + ',' + a.y).join(' L')
const path = document.createElementNS(svgns, 'path');
path.setAttribute('d', pathVal);
path.setAttribute('fill', 'none')
path.setAttribute('stroke', 'url(#linearTest)')
path.setAttribute('stroke-width', '3px')
path.style.setProperty('transform', 'translate(100px, 400px)')
svgVan.appendChild(path);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<div id="container" class="svg-container"></div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 720">
</svg>
<!--d3 script-->
<script src="index.js"></script>
</body>
</html>
However, the code is not assigning colors as desired to the path segments and I am having a hard time detecting where it is going wrong.
Path Segments are as following
Like Robert says; It is easier to draw the lines.
For a smoother effect you need to calc gradients, and calc the direction of the gradients depending on line slope.
<svg-rainbow-line
points="0,80-50,20-100,50-150,30-200,40-250,90-300,0-350,0-400,20-450,70-500,60"
colors="red,orange,yellow,green,blue,purple"
width="20"></svg-rainbow-line>
<script>
customElements.define("svg-rainbow-line", class extends HTMLElement {
connectedCallback() {
let xaxis = [], yaxis = []; // find viewPort boundaries
let w = Number(this.getAttribute("width")||5); // stroke-width and padding
let colors = (this.getAttribute("colors")||"red,yellow,blue").split(",");
this.innerHTML = `<svg style="background:grey">${
this
.getAttribute("points")
.split("-")
.map(xy => xy.split(","))
.map(([x2, y2,stopcolor="not implemented"], idx, arr) => {
xaxis.push(~~x2); yaxis.push(~~y2);// force integers
if (idx) {
let [x1, y1,startcolor=colors.shift()] = arr[idx - 1]; // previous point
// calc gradient here
colors.push(startcolor);// cycle colors
return `<line stroke="${startcolor}" stroke-width="${w}"
stroke-linecap="round"
x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"/>`;
}
return "";
}).join("")}</svg>`;
xaxis.sort((a, b) => a - b); yaxis.sort((a, b) => a - b); // sort by number!
let vb = `${xaxis.at(0)-w} ${yaxis.at(0)-w} ${xaxis.at(-1)+w*2} ${yaxis.at(-1)+w*2}`;
this.querySelector("svg").setAttribute("viewBox", vb);
}
})
</script>
Related
I want to create a webpage that displays html news widgets on a globe, I have the globe and I have the html news widget but I don't know how to implement the widget into the code so that it displays on the globe.
the globe:
the widget:
the embed:
my code
<head>
<style> body { margin: 0; } </style>
<script src="//unpkg.com/globe.gl"> </script>
<!-- <script src="../../dist/globe.gl.js"></script>-->
</head>
<body>
<div id="globeViz"></div>
<script>
//this is where I have chosen to put the html embed
const markerSvg = <rssapp-wall id="tszZCksFzEB4w9UN"> </rssapp-wall> <script src="https://widget.rss.app/v1/wall.js" type="text/javascript" async></script>;
// Gen random data
const N = 30;
const gData = [...Array(N).keys()].map(() => ({
lat: (Math.random() - 0.5) * 180,
lng: (Math.random() - 0.5) * 360,
size: 7 + Math.random() * 30,
color: ['red', 'white', 'blue', 'green'][Math.round(Math.random() * 3)]
}));
Globe()
.globeImageUrl('//unpkg.com/three- globe/example/img/earth-blue-marble.jpg')
.bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
.backgroundImageUrl('//unpkg.com/three- globe/example/img/night-sky.png')
.htmlElementsData(gData)
.htmlElement(d => {
const el = document.createElement('div');
el.innerHTML = markerSvg;
el.style.color = d.color;
el.style.width = `${d.size}px`;
el.style['pointer-events'] = 'auto';
el.style.cursor = 'pointer';
el.onclick = () => console.info(d);
return el;
})
(document.getElementById('globeViz'));
</script>
</body>
currently with the code like this I am getting the error : "JSX expressions must have one parent element"
code from example file (https://github.com/vasturiano/globe.gl/blob/master/example/html-markers/index.html):
<head>
<style> body { margin: 0; } </style>
<script src="//unpkg.com/globe.gl"> </script>
<!-- <script src="../../dist/globe.gl.js"></script>-->
</head>
<body>
<div id="globeViz"></div>
<script>
const markerSvg = `<svg viewBox="-4 0 36 36">
<path fill="currentColor" d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z"></path>
<circle fill="black" cx="14" cy="14" r="7"></circle>
</svg>`;
// Gen random data
const N = 30;
const gData = [...Array(N).keys()].map(() => ({
lat: (Math.random() - 0.5) * 180,
lng: (Math.random() - 0.5) * 360,
size: 7 + Math.random() * 30,
color: ['red', 'white', 'blue', 'green'][Math.round(Math.random() * 3)]
}));
Globe()
.globeImageUrl('//unpkg.com/three- globe/example/img/earth-blue-marble.jpg')
.bumpImageUrl('//unpkg.com/three-globe/example/img/earth-topology.png')
.backgroundImageUrl('//unpkg.com/three- globe/example/img/night-sky.png')
.htmlElementsData(gData)
.htmlElement(d => {
const el = document.createElement('div');
el.innerHTML = markerSvg;
el.style.color = d.color;
el.style.width = `${d.size}px`;
el.style['pointer-events'] = 'auto';
el.style.cursor = 'pointer';
el.onclick = () => console.info(d);
return el;
})
(document.getElementById('globeViz'));
</script>
</body>
live demo
https://shange-fagan.github.io/globe.news/
edit: I am also getting the error "Unterminated string literal." at the end of the html embed
I have code that implements histograms using Vue.js and d3js library. However, now I need to flip the chart to be horizontal using the same output formats. I tried to invert x to y but to no avail.
There were also attempts to rotate the entire diagram, but the lines go beyond the block with the diagram. The data comes from the server in the form of a matrix. Could you help me?
This is my code
<template>
<div class="graph-wrapper horizontal-bar">
<GraphTooltip
v-if="showTooltip"
:graphId="graphId"
:activeIndex="activePoints.index"
:placeOnLeft="activePoints.x"
:graphWidth="width"
:bandWidth="bandWidthOneGroup"
:graphPaddingLeft="padding.left"
/>
<svg
:id="graphKey"
width="100%"
:height="svgHeight"
#mousemove="mouseMove"
#mouseout="mouseOut"
>
<rect class="background" width="100%" :height="svgHeight" />
<g :style="{transform: `translate(${padding.left}px, ${padding.top}px)`}">
<g class="axis axis-x" />
<g class="axis axis-y" />
<g class="bars">
<g
v-for="(bar, barIndex) in barsData"
:key="`rect_${barIndex}`"
:style="{transform: `translate(${bar.offset}px, 0px)`}"
class="bar-group"
:class="{active: showTooltip && activePoints.index === barIndex}"
>
<rect
v-for="(rect, rectIndex) in bar.values"
:key="`rect_${rectIndex}`"
class="rect"
:x="rect.y"
:y="rect.x"
:width="rect.width"
:height="rect.height"
:style="{fill: rect.color}"
></rect>
</g>
</g>
<g class="axis axis-y-numbers" />
</g>
</svg>
</div>
</template>
<script>
import GraphTooltip from './GraphTooltip'
import * as d3 from 'd3'
export default {
props: {
graphId: Number,
graphKey: String
},
components: {
GraphTooltip
},
mixins: [colorsMixin],
data() {
return {
padding: {top: 6, right: 14, bottom: 35, left: 14},
width: 0,
height: 0,
graphBoxWidth: 0,
maxTitleWidth: 70,
paddingOuter: 0,
activePoints: {},
showTooltip: false,
xsScreenWidth: 576
}
},
computed: {
graphData() {
return this.$store.getters.graphData(this.$props.graphId)
},
measureCount() {
return this.graphData.data.measures.length
},
svgHeight() {
return this.height + this.padding.bottom + this.padding.top
},
dimensionTextPresent() {
return !!this.graphData.dimension
},
maxCountOnX() {
return parseInt(this.width / this.maxTitleWidth)
},
axisXValues() {
return this.graphData.data.matrix.map(data => data.x.value)
},
axisYValues() {
// get one array of all data
const allDataArray = this.graphData.data.matrix.flatMap(d => d.y.map(y => y.value))
let dataMin = parseFloat(d3.min(allDataArray))
const dataMax = parseFloat(d3.max(allDataArray))
if (allDataArray.every(value => value > 0)) {
dataMin = dataMin - 1
}
const extendedInterval = getExtendedInterval(dataMin, dataMax)
return this.graphBoxWidth > this.xsScreenWidth
? getIntervalValues(extendedInterval, 4)
: getIntervalValues(extendedInterval, 2)
},
scaleX() {
return d3
.scaleBand()
.range([0, this.width])
.domain(this.axisXValues)
.paddingInner(0.375)
.paddingOuter(this.paddingOuter)
},
scaleXOneGroup() {
return d3
.scaleBand()
.range([0, this.scaleX.bandwidth()])
.domain(this.graphData.data.measures)
},
scaleY() {
return d3
.scaleLinear()
.range([this.height, 0])
.domain(d3.extent(this.axisYValues))
},
bandWidthOneGroup() {
return this.scaleX.bandwidth()
},
barsData() {
return this.axisXValues.map((_, groupIndex) => {
const allRects = this.graphData.data.measures.map((rect, rectIndex) => {
return {
x: this.scaleXOneGroup(rect),
y: this.scaleY(this.graphData.data.matrix[groupIndex].y[rectIndex].value),
width: this.scaleXOneGroup.bandwidth(),
height:
this.height - this.scaleY(this.graphData.data.matrix[groupIndex].y[rectIndex].value),
color: this.getColor(rectIndex)
}
})
const xArray = allRects.map(element => element.x)
const filteredRects = allRects.filter(rect => rect.height !== 0)
filteredRects.forEach((element, index) => {
element.x = xArray[index]
})
const offsetAmount = allRects.length - filteredRects.length
return {
offset:
this.scaleX(this.axisXValues[groupIndex]) +
(offsetAmount / 2) * this.scaleXOneGroup.bandwidth(),
values: filteredRects
}
})
},
allPointsInPX() {
// Array of all x points with coordinates in px
return this.graphData.data.matrix.map((data, index) => {
return {
x: this.scaleX(this.axisXValues[index]),
index: index
}
})
}
},
mounted() {
window.addEventListener('resize', this.onResize)
this.onResize()
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
},
watch: {
width() {
this.drawAxes()
}
},
methods: {
onResize() {
// Width of area with all paddings
this.graphBoxWidth = this.$el.parentNode.offsetWidth
// Graph width is calculated without paddings
this.width = this.$el.offsetWidth - this.padding.left - this.padding.right
// On small screen graph height is smaller
this.height = this.$el.offsetWidth > this.xsScreenWidth ? 220 : 163
// Define padding from axes to bars
this.paddingOuter = this.$el.offsetWidth > this.xsScreenWidth ? 0.1875 : 0
},
drawAxes() {
const xValues =
this.graphBoxWidth > this.xsScreenWidth
? this.axisXValues.length > this.maxCountOnX
? shortenValues(this.axisXValues, this.maxCountOnX)
: this.axisXValues
: [this.axisXValues[0], this.axisXValues[this.axisXValues.length - 1]]
const xFormat = this.dimensionTextPresent ? formatText : formatDate
// Draw vertical lines and numbers
const xAxis = d3
.axisBottom()
.scale(this.scaleX)
.tickSize(-this.height)
.tickFormat(xFormat)
.tickValues(xValues)
d3.select(`#${this.graphKey} .axis.axis-x`)
.attr('transform', `translate(0,${this.height})`)
.call(xAxis)
d3.selectAll(`#${this.graphKey} .axis.axis-x .tick text`).call(
wrapXValues,
this.maxTitleWidth
)
if (this.graphBoxWidth < this.xsScreenWidth) {
// On small screens move first tick and last to make text fits
d3.select(`#${this.graphKey} .axis.axis-x .tick:first-of-type text`).attr(
'transform',
`translate(-${this.bandWidthOneGroup / 2},0)`
)
d3.select(`#${this.graphKey} .axis.axis-x .tick:last-child text`).attr(
'transform',
`translate(${this.bandWidthOneGroup / 2},0)`
)
}
// Draw horizontal lines
const yAxisGrid = d3
.axisLeft()
.scale(this.scaleY)
.tickSize(this.width)
.tickValues(this.axisYValues)
.tickFormat('')
d3.select(`#${this.graphKey} .axis.axis-y`)
.attr('transform', `translate(${this.width}, 0)`)
.call(yAxisGrid)
// Draw numbers for axis Y
const yAxisNumbers = d3
.axisRight()
.scale(this.scaleY)
.tickSize(0)
.tickFormat(formatNumber)
.tickValues(this.axisYValues)
// Delete previous rects, if window width is changed
d3.select(`#${this.graphKey} .axis.axis-y-numbers`)
.call(yAxisNumbers)
.selectAll('.rect-numbers')
.remove()
// Add background rects for texts
d3.select(`#${this.graphKey} .axis.axis-y-numbers`)
.selectAll('.tick')
.insert('rect', 'text')
.attr('class', 'rect-numbers')
// Get width of text elements on axis Y
let widthTextsY = []
d3.selectAll(`#${this.graphKey} .axis.axis-y-numbers text`)
.each(function(d, i) {
widthTextsY[i] = this.getBBox().width
})
.attr('x', 1)
// Set the width of text elements to rect, 2 - is additional padding
d3.selectAll(`#${this.graphKey} .axis.axis-y-numbers rect`).attr(
'width',
(d, i) => widthTextsY[i] + 2
)
// Hide bottom 0 on axis Y
const yBottomValue = d3.select(`#${this.graphKey} .axis.axis-y-numbers .tick text`).datum()
if (yBottomValue === 0) {
d3.select(`#${this.graphKey} .axis.axis-y-numbers .tick:first-of-type`).attr(
'class',
'tick hide-axis'
)
}
},
mouseOut() {
this.showTooltip = false
},
mouseMove({offsetX, offsetY}) {
const mouseX = offsetX - this.padding.left
// Find the closest point on axis X
const closestPoint = this.getClosestPoint(mouseX)
if (closestPoint.index !== this.activePoints.index) {
this.activePoints = this.allPointsInPX[closestPoint.index]
}
if (!this.showTooltip && offsetY < this.height) {
this.showTooltip = true
}
},
getClosestPoint(x) {
return this.allPointsInPX
.map((point, index) => ({
x: point.x,
diff:
Math.abs(point.x - x) < Math.abs(point.x + this.bandWidthOneGroup - x)
? Math.abs(point.x - x)
: Math.abs(point.x + this.bandWidthOneGroup - x),
index
}))
.reduce((memo, val) => (memo.diff < val.diff ? memo : val))
}
}
}
</script>
I have created some charts which are showing in front of another object (globe) as shown below:
how objects look like in the A-Frame inspector
but when I am using a camera and move in the scene the order of the objects change and the bar charts are not visible anymore (overlap). Not sure what causing this and how to resolve it.
Here are some screenshots from this issue:
overlay screenshot 1
overlay screenshot 2
overlay screenshot 3
and the code
const globeEntity = document.getElementById('globe');
const getAlt = d => d.MMS / 2500
//const catColor = d3.scaleOrdinal(d3.schemeCategory10.map(col => polished.transparentize(0.2, col)));
// const catColor = d3.scaleOrdinal(d3.schemeCategory10.map(col => polished.transparentize(0.2, col)));
const colorArray = ['#fb0511', '#fa4400', '#f56500', '#eb8100', '#dd9a00', '#cbb100', '#b4c700', '#97db00', '#6fed00', '#05ff34']
//https://colordesigner.io/gradient-generator
var assd = "sjdhdh"
globeEntity.setAttribute('globe', {
pointLat: 'lat',
pointLng: 'long',
pointAltitude: getAlt,
pointRadius: function(d) {
return d.Population / 3000000
},
pointColor: function(d) {
return colorArray[d.Dep]
},
labelLat: 'lat',
labelLng: 'long',
labelAltitude: d => getAlt(d),
labelDotRadius: function(d) {
return d.Population / 3000000
},
labelDotOrientation: () => 'bottom',
labelColor: function(d) {
return colorArray[d.Dep]
},
labelText: 'NAME',
labelSize: 0.05,
labelResolution: 0.15,
onHover: hoverObj => {
let label = '',
desc = '';
if (hoverObj) {
const d = hoverObj.data;
label = `${d.NAME}; Pop.:${new Intl.NumberFormat().format(d.Population)}; Dep:${d.Dep}`;
desc = `Market Share: ${d.MMS}% `
}
document.querySelector('#globe-tooltip').setAttribute('value', label);
document.querySelector('#globe-tooltip-details').setAttribute('value', desc);
// document.querySelector('#textentity').setAttribute('text', desc)
//document.querySelector('#globe-tooltip-details').setAttribute('visible', 'true');
},
onClick: clickObj => {
let label = '',
desc = '';
if (clickObj) {
const d = clickObj.data;
label = `${d.NAME}; Pop.:${new Intl.NumberFormat().format(d.Population)}; Dep:${d.Dep} clicked`;
desc = `Market Share: ${d.MMS}% Clk`
}
document.querySelector('#globe-tooltip').setAttribute('value', label);
document.querySelector('#globe-tooltip-details').setAttribute('value', desc);
}
});
fetch('https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/TAJSON.json?v=1658396524027').then(res => res.json()).then(pt => {
globeEntity.setAttribute('globe', {
pointsData: pt,
labelsData: pt
});
});
// default alpha for bars
var alpha = 0.9
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
function colorPicker(v) {
console.log(v)
if (v <= 30) {
return "#F96B4D";
} else if (v < 50 & v > 30) {
return "yellow"
} else if (v > 50) {
return "#73FA28 ";
}
}
// d3.csv('https://drive.google.com/file/d/1PT6mwPM42mZvhrLYdC8WmwEjwusr0AfZ', function(data){
var X_x = -0.23865 - 10.379
var Y_y = 2.2
var Z_z = -2.21058
d3.csv('https://docs.google.com/spreadsheets/d/e/2PACX-1vS8opHDQ6I1QQbwtS3oxSk4ZNZr5MGsqJnmRMX9xGKcClEhbkCYWP_tsQKF2Y8JWaO6FXkTyqDVNIJt/pub?gid=0&single=true&output=csv', function(data) {
let dataVariable = 'MMS'
// let dataVariable = 'Diff 2008-2012'
data.forEach(function(d) {
d[dataVariable] = +d[dataVariable];
console.log("I am in ")
console.log(d)
});
console.log(data)
let yScale = d3.scale.linear()
.range([3, 5]) //[0, 3]
.domain([0, 70]);
let xScale = d3.scale.linear()
.range([-1, 2]) //[0, 3]
.domain([0, 30]);
console.log(yScale.domain())
let scene = d3.select('a-scene')
console.log(scene)
let bars = scene.selectAll('a-box.bar')
.data(data)
.enter().append('a-box')
.classed('bar', true);
bars.attr({
// src:"https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/Person.jpg",
// normalmap:"#PersonAsset_NRM",
// normalTextureRepeat:"2 2",
position: function(d, i) {
var x = xScale((i * 2) + X_x);
var y = (yScale(d[dataVariable]) / 2) + Y_y - 1.5;
var z = Z_z
return x + " " + y + " " + z
},
width: function(d) {
return 0.07
},
depth: function(d) {
return 0.03
},
height: function(d) {
return d3.max([yScale(d[dataVariable]) - 3, 0.01])
},
opacity: alpha,
material: "shader: standard",
roughness: 0.1,
//repeat: function(d) {return (1,getRandomArbitrary(1,8))},
// scale:"1 1 1",
color: function(d) {
return colorPicker(d['MMS']); // call the color picker to get the fill.
}
});
let text = scene.selectAll('a-text .desc')
.data(data)
.enter().append('a-text')
.classed('desc', true);
text.attr({
position: function(d, i) {
var x = xScale((i * 2) + X_x)
var y = 2.2
var z = Z_z
return x + " " + y + " " + z
},
value: function(d) {
return d['AREA_NAME'].replace(' ', '\n');
},
color: '#faf443',
align: 'center',
baseline: 'top',
width: 1,
})
let numFormat = d3.format("s")
let numText = scene.selectAll('a-text .num')
.data(data)
.enter().append('a-text')
.classed('num', true);
numText.attr({
position: function(d, i) {
var x = xScale((i * 2) + X_x)
var y = yScale(d[dataVariable]) + Y_y - 2.8
var z = Z_z
return x + " " + y + " " + z
},
value: function(d) {
return numFormat(d[dataVariable]);
},
color: 'red',
align: 'center',
width: 1.5,
})
scene.append('a-text')
.attr({
position: '5 5 0',
value: 'MMS (each person is equal to 50k cutomers!)',
color: 'green',
align: 'center',
width: 5,
})
});
<meta charset="utf-8">
<title>A-Frame 3D Globe Component Example</title>
<meta name="description" content="Example for 3D Globe component."></meta>
<!-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions-->
<!-- from https://r105.threejsfundamentals.org/threejs/lessons/threejs-load-gltf.html-->
<script src="https://r105.threejsfundamentals.org/threejs/resources/threejs/r105/three.min.js"></script>
<script src="https://r105.threejsfundamentals.org/threejs/resources/threejs/r105/js/controls/OrbitControls.js"></script>
<script src="https://r105.threejsfundamentals.org/threejs/resources/threejs/r105/js/loaders/GLTFLoader.js"></script>
<script src="https://r105.threejsfundamentals.org/threejs/../3rdparty/dat.gui.min.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="//unpkg.com/polished#3.5.2/dist/polished.js"></script>
<script src="//unpkg.com/aframe"></script>
<script src="//unpkg.com/aframe-extras/dist/aframe-extras.min.js"></script>
<script src="https://cdn.rawgit.com/tizzle/aframe-orbit-controls-component/v0.1.14/dist/aframe-orbit-controls-component.min.js"></script>
<script src="https://cdn.statically.io/gh/vasturiano/aframe-globe-component/c23c2a7e/dist/aframe-globe-component.min.js"></script>
<script src="https://unpkg.com/aframe-look-at-component#0.8.0/dist/aframe-look-at-component.min.js"></script>
</head>
<body>
<a-scene>
<a-assets>
<img id="skyTexture" src="https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/clear-sunny-sky.jpg?v=1657244930844">
<a-asset-item id="Person1" src="https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/scenePerson1.gltf"></a-asset-item>
<img id="skyNight" src="https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/Solarsystemscope_texture_2k_stars_milky_way.jpg?v=1658390858400">
</a-assets>
<a-entity position="0 0 0" movement-controls="fly: true; speed: 0.5">
<a-entity cursor="rayOrigin: mouse; mouseCursorStylesEnabled: true;" raycaster="objects: [globe]; interval: 100"></a-entity>
<a-entity laser-controls="hand: left" raycaster="objects: [globe]; interval: 100; lineColor: yellow; lineOpacity: 1;showLine:true "></a-entity>
<a-entity laser-controls="hand: right" raycaster="objects: [globe]; interval: 100; lineColor: red; lineOpacity: 1;showLine:true "></a-entity>
<a-entity id="globe" scale="0.1 0.1 0.1" globe="
globe-image-url: https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/8k_earth_daymap.jpg;
bump-image-url: https://upload.wikimedia.org/wikipedia/commons/f/fb/Solarsystemscope_texture_8k_earth_normal_map.tif" rotation="48.202939304356164 179.81249076149652 0.6153566719705041" position="0.43949 -0.25848 -12.17506"></a-entity>
<!-- 0 -175 0 -->
<!-- https://www.h-schmidt.net/map/download/world_shaded_43k.jpg -->
<a-camera id="cam" look-controls wasd-controls="acceleration:10; ">
<!--wsAxis:y;wsInverted:true -->
<a-cursor></a-cursor>
<a-text id="globe-tooltip" position="0 -0.4 -1" width="2" align="center" color="lavender"></a-text>
<a-text id="globe-tooltip-details" position="0 -0.5 -1" width="1" align="center" color="lavender"></a-text>
</a-camera>
</a-entity>
<a-sky src="#skyNight"></a-sky>
<a-entity id="moon" class="collidable" position="13.202 14.175 2.96" scale="0.05 0.05 0.05" globe="globe-image-url: https://cdn.glitch.global/c153e3cf-7430-444d-9897-4e97f1ef8d35/2k_moon.jpg?v=1658444439177;">
</a-entity>
<a-text id="moontooltip" position="10.5605 3.75316 -1.37718" rotation="0 -59.99999999999999 -0.5" width="22" align="center" color="lavender" text="value: 3D vis of MMS (hight), \n
Dep (Color),\n Pop (radious). \n
and TAs, \nPOC" look-at="#cam" opacity="0.8" scale="1 1 1"></a-text>
</a-scene>
and the order of objects in the scene,
You have two movement systems:
the movement-controls at the rig which has the entire globe as a child object
the wasd-controls attached to the camera which is also a child of the rig.
So when You try moving the camera, You also independantly move the globe with the movement controls, positioning the bar chart inside the globe.
Here's a glitch with the wasd-controls removed
In this jsFiddle I have an SVG rect that is resizable using interact.js. There's also a 10px by 10px grid and the .resizable function has included a 10px by 10 px snap. The objective is to resize the rect and have the edges snap exactly on the grid.
In most cases it works fine, but many times it is not, as you can see in the picture below. Maybe an adjustment needs to be done manually on resizeend ? How to fix this problem?
As Erik said:
With this target.setAttribute(attr/a, Math.round(v/10)*10) it seems to work:
.on('resizemove', function(event) {
// Resize the rect, not the group, it will resize automatically
const target = event.target.querySelector('rect');
for (const attr of ['width', 'height']) {
let v = Number(target.getAttribute(attr));
v += event.deltaRect[attr];
target.setAttribute(attr, Math.round(v/10)*10);
}
for (const attr of ['top', 'left']) {
const a = attr == 'left' ? 'x' : 'y';
let v = Number(target.getAttribute(a));
v += event.deltaRect[attr];
target.setAttribute(a, Math.round(v/10)*10);
}
findLocations(rect, handles);
});
Full Demo here - https://jsfiddle.net/alexander_L/1mzs36qL/3/ and below:
const svg = document.getElementById('mysvg');
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
// draw vertical lines
var gridSize = 10;
for (var i=0;i < 100;i++){
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
svg.appendChild(line);
line.setAttribute("x1", (i + 1) * gridSize)
line.setAttribute("y1", 0)
line.setAttribute("x2", (i + 1) * gridSize)
line.setAttribute("y2", 500)
line.setAttribute("stroke-width", 1)
line.setAttribute("stroke", 'gray');
}
// draw horizontal lines
for (var i=0;i < 100;i++){
var line = document.createElementNS("http://www.w3.org/2000/svg", "line");
svg.appendChild(line);
line.setAttribute("x1", 0)
line.setAttribute("y1", (i + 1) * gridSize)
line.setAttribute("x2", 2000)
line.setAttribute("y2", (i + 1) * gridSize)
line.setAttribute("stroke-width", 1)
line.setAttribute("stroke", 'gray');
}
svg.appendChild(group);
group.appendChild(rect);
group.setAttribute('class', 'resize-me');
rect.setAttribute('x', 100);
rect.setAttribute('y', 100);
rect.setAttribute('width', 100);
rect.setAttribute('height', 100);
rect.setAttribute('stroke-width', 1);
rect.setAttribute('stroke', 'white');
rect.setAttribute('fill', 'grey');
// Create the handles
const handles = [];
for (let i = 0; i < 8; i++) {
const handle = document.createElementNS("http://www.w3.org/2000/svg", "rect");
handle.setAttribute('width', 8);
handle.setAttribute('height', 8);
handle.setAttribute('stroke-width', 1);
handle.setAttribute('stroke', 'white');
handle.setAttribute('fill', 'black');
handles.push(handle);
group.appendChild(handle);
}
// Manually assign them their resize duties (R->L, T->B)
handles[0].classList.add('resize-top', 'resize-left');
handles[1].classList.add('resize-top');
handles[2].classList.add('resize-top', 'resize-right');
handles[3].classList.add('resize-left');
handles[4].classList.add('resize-right');
handles[5].classList.add('resize-bottom', 'resize-left');
handles[6].classList.add('resize-bottom');
handles[7].classList.add('resize-bottom', 'resize-right');
// This function takes the rect and the list of handles and positions
// the handles accordingly
const findLocations = (r, h) => {
const x = Number(r.getAttribute('x'));
const y = Number(r.getAttribute('y'));
const width = Number(r.getAttribute('width'));
const height = Number(r.getAttribute('height'));
// Important these are in the same order as the classes above
let locations = [
[0, 0],
[width / 2, 0],
[width, 0],
[0, height / 2],
[width, height / 2],
[0, height],
[width / 2, height],
[width, height]
];
// Move each location such that it's relative to the (x,y) of the rect,
// and also subtract half the width of the handles to make up for their
// own size.
locations = locations.map(subarr => [
subarr[0] + x - 4,
subarr[1] + y - 4
]);
for (let i = 0; i < locations.length; i++) {
h[i].setAttribute('x', locations[i][0]);
h[i].setAttribute('y', locations[i][1]);
}
}
interact('.resize-me')
.resizable({
edges: {
left: '.resize-left',
right: '.resize-right',
bottom: '.resize-bottom',
top: '.resize-top'
},
modifiers: [
interact.modifiers.snap({
targets: [
interact.snappers.grid({
x: 10,
y: 10,
})
]
})
]
})
.on('resizemove', function(event) {
// Resize the rect, not the group, it will resize automatically
const target = event.target.querySelector('rect');
for (const attr of ['width', 'height']) {
let v = Number(target.getAttribute(attr));
v += event.deltaRect[attr];
target.setAttribute(attr, Math.round(v/10)*10);
}
for (const attr of ['top', 'left']) {
const a = attr == 'left' ? 'x' : 'y';
let v = Number(target.getAttribute(a));
v += event.deltaRect[attr];
target.setAttribute(a, Math.round(v/10)*10);
}
findLocations(rect, handles);
});
findLocations(rect, handles);
svg {
width: 100%;
height: 240px;
background-color: #2e9;
-ms-touch-action: none;
touch-action: none;
}
body { margin: 0; }
<script src="https://cdn.jsdelivr.net/npm/interactjs#latest/dist/interact.min.js"></script>
<svg id="mysvg"></svg>
I have quite a few used-defined svg markers (glyphs) (big thanks to the SO user with the name rioV8 for his help on this - and not only this -...) and ideally I would like these glyphs to get their shape from the feature properties structure.
//create feature properties
var p = {
"id": i,
"popup": "Dot_" + i,
"year": parseInt(data[i].year),
"glyphName": "square",
"size": 500 // Fixed size circle radius=~13
};
These user-defined glyphs extend L.circleMarker and for simplicity let's say that their shapes can be square or diamond. Currently, I am extending L.Class and am passing glyphName in the constructor: (feel free to criticise that, If it doesnt look nice to you)
var glyph = L.Class.extend({
initialize: function(glyphName) {
glyphName === "square"? this.type = MarkerSquare:
glyphName === "diamond"? this.type = MarkerDiamond:
this.type = L.circleMarker;
},
});
and when I need to plot the glyphs I have something like:
L.geoJson(myDots[i], {
pointToLayer: function(feature, latlng) {
var p = latlng;
var myGlyph = new glyph('diamond')
return new myGlyph.type(p, style(feature));
},
onEachFeature: onEachDot
}).addTo(map);
Can I have the shape determined by the feature properties please? Eventually, what i am trying to achieve is to merge these two lines
var myGlyph = new glyph('diamond')
return new myGlyph.type(p, style(feature));
to something like
return new myGlyph.type(p, style(feature));
That will enable me to plot different shapes, and those shapes will be determined by the input data used to populate features properties. In a similar manner that these properties are used for color or size they could now be used to set the shape.
Thanks! (Full code below)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<style>
html,
body {
height: 100%;
margin: 0;
}
#map {
width: 600px;
height: 600px;
}
</style>
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.3.3/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet#1.3.3/dist/leaflet.js"></script>
</head>
<body>
<div id="map"></div>
<script>
L.Canvas.include({
_updateMarkerDiamond: function(layer) {
if (!this._drawing || layer._empty()) {
return;
}
var p = layer._point,
ctx = this._ctx,
r = Math.max(Math.round(layer._radius), 6);
this._drawnLayers[layer._leaflet_id] = layer;
ctx.beginPath();
ctx.moveTo(p.x - r, p.y);
ctx.lineTo(p.x, p.y - r);
ctx.lineTo(p.x + r, p.y);
ctx.lineTo(p.x, p.y + r);
ctx.lineTo(p.x - r, p.y);
ctx.closePath();
this._fillStroke(ctx, layer);
}
});
var MarkerDiamond = L.CircleMarker.extend({
_updatePath: function() {
this._renderer._updateMarkerDiamond(this);
}
});
L.Canvas.include({
_updateMarkerSquare: function(layer) {
if (!this._drawing || layer._empty()) {
return;
}
var p = layer._point,
ctx = this._ctx,
r = Math.max(Math.round(layer._radius), 5);
this._drawnLayers[layer._leaflet_id] = layer;
ctx.beginPath();
ctx.moveTo(p.x - r, p.y - r);
ctx.lineTo(p.x + r, p.y - r);
ctx.lineTo(p.x + r, p.y + r);
ctx.lineTo(p.x - r, p.y + r);
ctx.lineTo(p.x - r, p.y - r);
ctx.closePath();
this._fillStroke(ctx, layer);
}
});
var MarkerSquare = L.CircleMarker.extend({
_updatePath: function() {
this._renderer._updateMarkerSquare(this);
}
});
var glyph = L.Class.extend({
initialize: function(glyphName) {
glyphName === "square"? this.type = MarkerSquare:
glyphName === "diamond"? this.type = MarkerDiamond:
this.type = L.circleMarker;
},
});
var data = [];
var NumOfPoints = 100;
for (let i = 0; i < NumOfPoints; i++) {
data.push({
num: i,
x: Math.random() * 60,
y: Math.random() * 60,
year: Math.floor(100 * Math.random())
})
}
renderChart(data);
function make_dots(data) {
var arr = [];
var nest = d3.nest()
.key(function(d) {
return Math.floor(d.year / 10);
})
.entries(data);
for (var k = 0; k < nest.length; ++k) {
arr[k] = helper(nest[k].values);
}
return arr;
}
function helper(data) {
dots = {
type: "FeatureCollection",
features: []
};
for (var i = 0; i < data.length; ++i) {
x = data[i].x;
y = data[i].y;
var g = {
"type": "Point",
"coordinates": [x, y]
};
//create feature properties
var p = {
"id": i,
"popup": "Dot_" + i,
"year": parseInt(data[i].year),
//"glyphName": "square",
"size": 500 // Fixed size circle radius=~13
};
//create features with proper geojson structure
dots.features.push({
"geometry": g,
"type": "Feature",
"properties": p
});
}
return dots;
}
//create color ramp
function getColor(y) {
return y > 90 ? '#6068F0' :
y > 80 ? '#6B64DC' :
y > 70 ? '#7660C9' :
y > 60 ? '#815CB6' :
y > 50 ? '#8C58A3' :
y > 40 ? '#985490' :
y > 30 ? '#A3507C' :
y > 20 ? '#AE4C69' :
y > 10 ? '#B94856' :
y > 0 ? '#C44443' :
'#D04030';
}
//calculate radius so that resulting circles will be proportional by area
function getRadius(y) {
r = Math.sqrt(y / Math.PI)
return r;
}
var myRenderer;
//create style, with fillColor picked from color ramp
function style(feature) {
return {
radius: getRadius(feature.properties.size),
fillColor: getColor(feature.properties.year),
color: "#000",
weight: 0,
opacity: 1,
fillOpacity: 0.9,
renderer: myRenderer
};
}
//create highlight style, with darker color and larger radius
function highlightStyle(feature) {
return {
radius: getRadius(feature.properties.size) + 1.5,
fillColor: "#FFCE00",
color: "#FFCE00",
weight: 1,
opacity: 1,
fillOpacity: 0.9,
};
}
//attach styles and popups to the marker layer
function highlightDot(e) {
var layer = e.target;
dotStyleHighlight = highlightStyle(layer.feature);
layer.setStyle(dotStyleHighlight);
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
}
function resetDotHighlight(e) {
var layer = e.target;
dotStyleDefault = style(layer.feature);
layer.setStyle(dotStyleDefault);
}
function onEachDot(feature, layer) {
layer.on({
mouseover: highlightDot,
mouseout: resetDotHighlight
});
var popup = '<table style="width:110px"><tbody><tr><td><div><b>Marker:</b></div></td><td><div>' + feature.properties.popup +
'</div></td></tr><tr class><td><div><b>Group:</b></div></td><td><div>' + feature.properties.glyphName +
'</div></td></tr><tr><td><div><b>X:</b></div></td><td><div>' + feature.geometry.coordinates[0] +
'</div></td></tr><tr><td><div><b>Y:</b></div></td><td><div>' + feature.geometry.coordinates[1] +
'</div></td></tr></tbody></table>'
layer.bindPopup(popup);
}
function renderChart(data) {
var myDots = make_dots(data);
var minZoom = 0,
maxZoom = 15;
var map = L.map('map', {
minZoom: minZoom,
maxZoom: maxZoom
}).setView([30, 30], 3);
L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
continuousWorld: false,
minZoom: 0,
noWrap: true
}).addTo(map);
myRenderer = L.canvas({
padding: 0.5
});
// Define an array to keep layerGroups
var dotlayer = [];
//create marker layer and display it on the map
for (var i = 0; i < myDots.length; i += 1) {
dotlayer[i] = L.geoJson(myDots[i], {
pointToLayer: function(feature, latlng) {
var p = latlng;
var myGlyph = new glyph('diamond')
return new myGlyph.type(p, style(feature));
},
onEachFeature: onEachDot
}).addTo(map);
}
var cl = L.control.layers(null, {}).addTo(map);
for (j = 0; j < dotlayer.length; j += 1) {
var name = "Group " + j + "0-" + j + "9";
cl.addOverlay(dotlayer[j], name);
}
}
</script>
</body>
</html>
You need to make the shape of the marker a property of the marker and merge the render parts of the MarkerDiamond and MarkerSquare into a different marker and decide which render part to draw with an if inside the _updateMarkerXX method based on the property shape.
layer.options.shape contains the shape inside the render routine.
Or do it in the Marker routine
var Marker = L.CircleMarker.extend({
_updatePath: function() {
if (this.options.shape === "square")
this._renderer._updateMarkerSquare(this);
if (this.options.shape === "diamond")
this._renderer._updateMarkerDiamond(this);
}
});
function style(feature) {
return {
radius: getRadius(feature.properties.size),
shape: feature.properties.shape,
fillColor: getColor(feature.properties.year),
color: "#000",
weight: 0,
opacity: 1,
fillOpacity: 0.9,
renderer: myRenderer
};
}
Edit
It might be useful to time the use of Magic Numbers (Enums) instead of strings, because the compare of a number is cheaper than string compare. And Aenaon has about 300K markers, but it might be negligible.