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>
Related
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
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>
I have recreated this index chart in Dash.
This is a gif of the chart in the link.
o
The data is dynamically updated by a callback that listens to the hoverData property of the dcc.Graph component when the user hovers the mouse over the graph.
I also added a callback that disables/enables the updates. Right now it is triggered by clicking on the graph area, that is, a change in the clickData property.
However, this doesn’t feel very intuitive to me.
I would like to enable the hover updates when the user holds the left mouse button down and drags the mouse, and disabling it when the mouse is released.
How can I implement this functionality?
Check out the following implementation, did not refer the api so there could be a better way to do it:
mousedown, mouseup, mousemove, SO answer for drag behavior
vis.add(pv.Panel)
.events("all")
.event("mousedown", function() {
isMousedown = true;
})
.event("mouseup", function() {
isMousedown = false;
})
.event("mousemove", function() {
if (isMousedown) {
idx = x.invert(vis.mouse().x) >> 0;
update();
}
});
var data = [],
fy = function(d) {
return d.price
}, // y-axis value
fx = function(d) {
return d.index
}, // x-axis value
ft = function() {
return data[this.parent.index].ticker
}, // label
w = 730,
h = 360,
S = pv.max(pv.values(stocks), function(s) {
return s.values.length
}),
idx = Math.floor(S / 2) - 1,
x = pv.Scale.linear(0, S - 1).range(0, w),
y = pv.Scale.linear(-1, 5).range(0, h),
rescale = true;
/* Normalize the data according to an index point. */
var indexify = function(data, cols, idx) {
return cols.map(function(c) {
var v = data[c].values[idx];
return {
ticker: c,
values: data[c].values.map(function(d, i) {
return {
index: i,
price: ((d - v) / v)
};
})
}
});
};
/* Compute new index values, rescale if needed, and render. */
var update = function() {
data = indexify(stocks, names, idx);
if (rescale) {
var min = pv.min(data.map(function(d) {
return pv.min(d.values, fy)
}));
var max = pv.max(data.map(function(d) {
return pv.max(d.values, fy)
}));
}
y.domain(min, max).nice();
vis.render();
}
/* The visualization panel. Stores the active index. */
var vis = new pv.Panel()
.def("i", -1)
.left(60)
.right(70)
.top(20.5)
.bottom(18)
.width(w)
.height(h);
/* Horizontal gridlines showing %-change. */
vis.add(pv.Rule)
.data(function() {
return y.ticks(8)
})
.bottom(y)
.strokeStyle(function(d) {
return d == 0 ? "black" : "#cccccc"
})
.anchor("left").add(pv.Label)
.text(function(d) {
return (d * 100).toFixed(0) + "%"
});
/* Y-axis label */
vis.add(pv.Label)
.data(["Gain / Loss Factor"])
.left(-45)
.bottom(h / 2)
.font("10pt Arial")
.textAlign("center")
.textAngle(-Math.PI / 2);
/* Stock lines. */
vis.add(pv.Panel)
.data(function() {
return data
})
.add(pv.Line)
.data(function(d) {
return d.values
})
.left(x.by(fx))
.bottom(y.by(fy))
.lineWidth(2)
.add(pv.Label)
.visible(function() {
return this.index == S - 1
})
.textBaseline("middle")
.textMargin(6)
.text(ft);
/* Current index line. */
vis.add(pv.Rule)
.visible(function() {
return idx >= 0 && idx != vis.i()
})
.left(function() {
return x(idx)
})
.top(-4)
.bottom(-4)
.strokeStyle("red")
.anchor("bottom").add(pv.Label)
.text(function() {
return stocks.Date.values[idx]
});
/* An invisible bar to capture events (without flickering). */
var isMousedown = false;
vis.add(pv.Panel)
.events("all")
.event("mousedown", function() {
isMousedown = true;
})
.event("mouseup", function() {
isMousedown = false;
})
.event("mousemove", function() {
if (isMousedown) {
idx = x.invert(vis.mouse().x) >> 0;
update();
}
});
update();
#fig {
width: 860px;
height: 400px;
}
<script src="https://mbostock.github.io/protovis/protovis-r3.2.js"></script>
<script src="https://mbostock.github.io/protovis/ex/stocks.js"></script>
<link href="https://mbostock.github.io/protovis/style.css" rel="stylesheet" />
<div id="fig">
I have a sunburst chart made in D3. Each 'petal' represents a subset of data. When a user clicks on one of the 'petals', I would like it to transition, fanning out to only show that subset (see image):
I'm having trouble getting the code to properly transition.
On click, all 'petals' (besides the selected one) should disappear and the remain paths should animate along the circle (using attrTween, arcTween, and interpolate?). The primary value that would be changing is the angleSize (var angleSize = (2 * Math.PI) / theData.length;).
I've tried using this, this, this, and this as reference without much success. What's the best way to handle the animation?
Thanks for your time!
--> See Plunker Here. <--
Code is below:
var colors = {
'Rank1' : '#3FA548',
'Rank2' : '#00B09E',
'Rank3' : '#8971B3',
'Rank4' : '#DFC423',
'Rank5' : '#E74341'
};
var $container = $('.chart'),
m = 40,
width = $container.width() - m,
height = $container.height() - m,
r = Math.min(width, height) / 2;
var study = null;
var arc = d3.svg.arc();
d3.csv('text.csv', ready);
function ready(err, data) {
if (err) console.warn('Error', err);
var svg = d3.select('.chart')
.append('svg')
.attr({
'width' : (r + m) * 2,
'height' : (r + m) * 2,
'class' : 'container'
})
.append('g')
.attr('transform', 'translate(' + (width / 4) + ', ' + (height / 2) + ' )');
var slice = svg.selectAll('.slice');
function updateChart(study) {
if (study) {
var theData = data.filter(function(d) {
return d.study_name === study;
});
} else {
var theData = data;
}
slice = slice.data(theData);
slice.enter()
.append('g')
.attr('class', 'slice');
var angleSize = (2 * Math.PI) / theData.length;
var startRadArr = [],
endRadArr = [];
for ( var i = 0; i < data.length; i++ ) {
var startRadius = (width / 20),
endRadius = startRadius;
for ( var x = 0; x < 4; x++ ) {
startRadArr.push(startRadius);
if ( x == 0 ) {
endRadius += Number(data[i].group1_score) * (width / 500);
} else if ( x == 1 ) {
endRadius += Number(data[i].group2_score) * (width / 500);
} else if ( x == 2 ) {
endRadius += Number(data[i].group3_score) * (width / 500);
} else {
endRadius += Number(data[i].group4_score) * (width / 500);
}
endRadArr.push(endRadius);
startRadius = endRadius + 0.3;
}
}
var startRadGroup = [],
endRadGroup = [];
for (i = 0; i < startRadArr.length; i += 4) {
startRadGroup.push(startRadArr.slice(i, i + 4));
}
for (i = 0; i < endRadArr.length; i += 4) {
endRadGroup.push(endRadArr.slice(i, i + 4));
}
slice.selectAll('path')
.remove();
for ( var x = 0; x < 4; x++ ) {
slice.append('path')
.attr({
'class' : function(d, i) {
if ( x == 0 ) {
return d.group1_class;
} else if ( x == 1 ) {
return d.group2_class;
} else if ( x == 2 ) {
return d.group3_class;
} else {
return d.group4_class;
}
},
'company' : function(d, i) {
return d.brand_name;
},
'cat' : function(d, i) {
if ( x == 0 ) {
return 'Group1';
} else if ( x == 1 ) {
return 'Group2';
} else if ( x == 2 ) {
return 'Group3';
} else {
return 'Group4';
}
},
'study' : function(d, i) {
return d.study_name;
},
'companyid' : function(d, i) {
return d.brand_id;
},
'startradius' : function(d, i) {
return startRadGroup[i][x];
},
'endradius' : function(d, i) {
return endRadGroup[i][x];
},
'startangle' : function(d, i) {
return angleSize * i;
},
'endangle' : function(d, i) {
return angleSize * (i + 1);
}
})
.on('click', selectStudy);
}
slice.exit()
.remove();
slice.selectAll('path')
.attr({
'd' : function(d) {
return arc({
innerRadius : +d3.select(this)[0][0].attributes.startradius.nodeValue,
outerRadius : +d3.select(this)[0][0].attributes.endradius.nodeValue,
startAngle : +d3.select(this)[0][0].attributes.startangle.nodeValue,
endAngle : +d3.select(this)[0][0].attributes.endangle.nodeValue
})
}
});
}
function selectStudy(d) {
study = $(this).attr('study');
updateChart(study);
}
updateChart();
}
EDIT
Updated the code (based on this) to include a properly working enter, update, and exit pattern. Still unsure about the transition however. Most of the examples I've linked to use something similar to d3.interpolate(this._current, a);, tweening between differing data.
In this chart, this._current and a are the same, angleSize (var angleSize = (2 * Math.PI) / theData.length;), startAngle, and endAngle are the only thing that changes.
Your problem is that you are not really binding data to the elements, and therefore the transition is not possible. I mangled your code a little bit so the data contains all the nested information about the starting and ending angles, so that it can be bound to the paths inside each slice.
Take a look at this Plunker: https://plnkr.co/edit/a7cxRplzy66Pc1arM2a9?p=preview
Here's the listing of the modified version:
var colors = {
Rank1: '#3FA548',
Rank2: '#00B09E',
Rank3: '#8971B3',
Rank4: '#DFC423',
Rank5: '#E74341'
};
// Configuration
var $container = $('.chart'),
m = 40,
width = $container.width() - m,
height = $container.height() - m,
r = Math.min(width, height) / 2;
var study = null;
var arc = d3.svg.arc();
// Load data
d3.csv('text.csv', ready);
// Data loaded callback
function ready(err, data) {
if (err) console.warn('Error', err);
var svg = d3.select('.chart')
.append('svg')
.attr({
'width': (r + m) * 2,
'height': (r + m) * 2,
'class': 'container'
})
.append('g')
.attr('transform', 'translate(' + (width / 4) + ', ' + (height / 2) + ' )');
var slices = svg.selectAll('.slice');
function updateChart(study) {
var theData = data;
if (study) {
theData = data.filter(function (d) {
return d.study_name === study;
});
}
var angleSize = (2 * Math.PI) / theData.length;
theData.forEach(function (item, i) {
var startRadius = (width / 20),
endRadius = startRadius,
groupName;
item.paths = [];
for (var g = 0; g < 4; g++) {
item.paths[g] = {};
item.paths[g].startRadius = startRadius;
groupName = 'group' + (g + 1) + '_score';
endRadius += Number(item[groupName]) * (width / 500);
item.paths[g].endRadius = endRadius;
startRadius = endRadius + 0.3;
}
});
// Set the data
slices = slices.data(theData);
// Enter
slices.enter()
.append('g')
.attr('class', 'slice');
// Exit
slices.exit()
.remove();
// Update
slices
.transition()
.duration(750)
.each(function (dSlice, iSlice) {
var slice = d3.select(this);
var paths = slice.selectAll('path');
// Set data
paths = paths.data(dSlice.paths);
// Exit
paths.exit()
.remove();
// Enter
paths.enter()
.append('path')
.attr('class', 'path');
// Update
paths
.transition()
.attr({
'class': function (d, i) {
return dSlice['group' + (i + 1) + '_class'];
},
'company': dSlice.brand_name,
'cat': function (d, i) {
return 'Group' + (i + 1);
},
'study': function (d, i) {
return dSlice.study_name;
},
'companyid': function (d, i) {
return dSlice.brand_id;
},
'startradius': function (d, i) {
return d.startRadius;
},
'endradius': function (d, i) {
return d.endRadius;
},
'startangle': function (d, i) {
return angleSize * iSlice;
},
'endangle': function (d, i) {
return angleSize * (iSlice + 1);
},
'd': function (d) {
return arc({
innerRadius: +d.startRadius,
outerRadius: +d.endRadius,
startAngle: +angleSize * iSlice,
endAngle: +angleSize * (iSlice + 1)
})
}
})
.duration(750);
paths.on('click', selectStudy);
});
function selectStudy(d, i) {
study = $(this).attr('study');
updateChart(study);
}
}
updateChart();
}
As you can see, the key is correctly preparing the data (let's say the format in your example .tsv file is not the best choice, but sometimes we can't choose our data sources...)
Then afterwards, by putting the code for the paths generation inside the .each call on the slices, the data can be accessed from the function (d, i) { ... } callbacks and every element happens to receive the corresponding data.
Another trick is using the slices data (accessed inside the .each function via the dSlice and iSlice vars) on the paths' callbacks. This way the paths can consume this data for their own purposes. In this case, the company and study_name properties.
Now in order to tweak the transition and make it more accurate, the starting attributes can change. You can try by setting up some attributes for the paths in the .enter() phase.
I have set up this jsfiddle : http://jsfiddle.net/386er/dhzq6q6f/14/
var moveCell = function(direction) {
var cellToBeMoved = pickRandomCell();
var currentX = cellToBeMoved.x.baseVal.value;
var currentY = cellToBeMoved.y.baseVal.value;
var change = getPlusOrMinus() * (cellSize + 1 );
var newX = currentX + change;
var newY = currentY + change;
var selectedCell = d3.select(cellToBeMoved);
if (direction === 'x') {
selectedCell.transition().duration(1500)
.attr('x', newX );
} else {
selectedCell.transition().duration(1500)
.attr('y', newY );
}
}
In the moveCell function, I pick a random cell, request its current x and y coordinates and then add or subtract its width or height, to move it to an adjacent cell.
What I am wondering about: If you watch the cells move, some will only move partially to the next cell. Can anoyne tell me, why this is so ?
The first thing to do in this situation is put .each("interrupt", function() { console.log("interrupted!") }); on your transitions. Then you will see the problem.
Its supposed to fix it if you name the transitions like selection.transition("name"), but that doesn't fix it.
That means you have to do as suggested by #jcuenod and exclude the ones that are moving. One way to do that which is idiomatic is like this...
if (direction === 'x') {
selectedCell.transition("x").duration(1500)
.attr('x', newX)
.each("start", function () { lock.call(this, "lockedX") })
.each("end", function () { unlock.call(this, "lockedX") });
} else {
selectedCell.transition("y").duration(1500)
.attr('y', newY)
.each("start", function () { lock.call(this, "lockedX") })
.each("end", function () { unlock.call(this, "lockedX") });
}
function lock(lockClass) {
var c = { cell: false }; c[lockClass] = true;
d3.select(this).classed(c)
};
function unlock(lockClass) {
var c = { cell: this.classList.length == 1 }; c[lockClass] = false;
d3.select(this).classed(c);
};
Here is a fiddle to prove the concept.
Pure and idiomatic d3 version
Just for completeness here is the d3 way to do it.
I've tried to make it as idiomatic as possible. The main points being...
Purely data-driven
The data is updated and the viz manipulation left entirely to d3 declarations.
Use d3 to detect and act on changes to svg element attributes
This is done by using a composite key function in the selection.data() method and by exploiting the fact that changed nodes (squares where the x, y or fillattributes don't match the updated data) are captured by the exit selection.
Splice changed elements into the data array so d3 can detect changes
Since a reference to the data array elements is bound to the DOM elements, any change to the data will also be reflected in the selection.datum(). d3 uses a key function to compare the data values to the datum, in order to classify nodes as update, enter or exit. If a key is made, that is a function of the data/datum values, changes to data will not be detected. By splice-ing changes into the data array, the value referenced by selection.datum() will be different from the data array, so data changes will flag exit nodes.
By simply manipulating attributes and putting transitions on the exit selection and not removing it, it essentially becomes a 'changed' selection.
this only works if the data values are objects.
Concurrent transitions
Named transitions are used to ensure x and y transitions don't interrupt each other, but it was also necessary to use tag class attributes to lock out transitioning elements. This is done using transition start and end events.
Animation frames
d3.timer is used to smooth animation and marshal resources. d3Timer calls back to update the data before the transitions are updated, before each animation frame.
Use d3.scale.ordinal() to manage positioning
This is great because you it works every time and you don't even have to thin about it.
$(function () {
var container,
svg,
gridHeight = 800,
gridWidth = 1600,
cellSize, cellPitch,
cellsColumns = 100,
cellsRows = 50,
squares,
container = d3.select('.svg-container'),
svg = container.append('svg')
.attr('width', gridWidth)
.attr('height', gridHeight)
.style({ 'background-color': 'black', opacity: 1 }),
createRandomRGB = function () {
var red = Math.floor((Math.random() * 256)).toString(),
green = Math.floor((Math.random() * 256)).toString(),
blue = Math.floor((Math.random() * 256)).toString(),
rgb = 'rgb(' + red + ',' + green + ',' + blue + ')';
return rgb;
},
createGrid = function (width, height) {
var scaleHorizontal = d3.scale.ordinal()
.domain(d3.range(cellsColumns))
.rangeBands([0, width], 1 / 15),
rangeHorizontal = scaleHorizontal.range(),
scaleVertical = d3.scale.ordinal()
.domain(d3.range(cellsRows))
.rangeBands([0, height]),
rangeVertical = scaleVertical.range(),
squares = [];
rangeHorizontal.forEach(function (dh, i) {
rangeVertical.forEach(function (dv, j) {
var indx;
squares[indx = i + j * cellsColumns] = { x: dh, y: dv, c: createRandomRGB(), indx: indx }
})
});
cellSize = scaleHorizontal.rangeBand();
cellPitch = {
x: rangeHorizontal[1] - rangeHorizontal[0],
y: rangeVertical[1] - rangeVertical[0]
}
svg.selectAll("rect").data(squares, function (d, i) { return d.indx })
.enter().append('rect')
.attr('class', 'cell')
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function (d) { return d.x })
.attr('y', function (d) { return d.y })
.style('fill', function (d) { return d.c });
return squares;
},
choseRandom = function (options) {
options = options || [true, false];
var max = options.length;
return options[Math.floor(Math.random() * (max))];
},
pickRandomCell = function (cells) {
var l = cells.size(),
r = Math.floor(Math.random() * l);
return l ? d3.select(cells[0][r]).datum().indx : -1;
};
function lock(lockClass) {
var c = { cell: false }; c[lockClass] = true;
d3.select(this).classed(c)
};
function unlock(lockClass) {
var c = { cell: this.classList.length == 1 }; c[lockClass] = false;
d3.select(this).classed(c);
};
function permutateColours() {
var samples = Math.min(50, Math.max(~~(squares.length / 50),1)), s, ii = [], i, k = 0,
cells = d3.selectAll('.cell');
while (samples--) {
do i = pickRandomCell(cells); while (ii.indexOf(i) > -1 && k++ < 5 && i > -1);
if (k < 10 && i > -1) {
ii.push(i);
s = squares[i];
squares.splice(i, 1, { x: s.x, y: s.y, c: createRandomRGB(), indx: s.indx });
}
}
}
function permutatePositions() {
var samples = Math.min(20, Math.max(~~(squares.length / 100),1)), s, ss = [], d, m, p, k = 0,
cells = d3.selectAll('.cell');
while (samples--) {
do s = pickRandomCell(cells); while (ss.indexOf(s) > -1 && k++ < 5 && s > -1);
if (k < 10 && s > -1) {
ss.push(s);
d = squares[s];
m = { x: d.x, y: d.y, c: d.c, indx: d.indx };
m[p = choseRandom(["x", "y"])] = m[p] + choseRandom([-1, 1]) * cellPitch[p];
squares.splice(s, 1, m);
}
}
}
function updateSquares() {
//use a composite key function to transform the exit selection into
// an attribute update selection
//because it's the exit selection, d3 doesn't bind the new data
// that's done manually with the .each
var changes = svg.selectAll("rect")
.data(squares, function (d, i) { return d.indx + "_" + d.x + "_" + d.y + "_" + d.c; })
.exit().each(function (d, i, j) { d3.select(this).datum(squares[i]) })
changes.transition("x").duration(1500)
.attr('x', function (d) { return d.x })
.each("start", function () { lock.call(this, "lockedX") })
.each("end", function () { unlock.call(this, "lockedX") })
changes.transition("y").duration(1500)
.attr('y', function (d) { return d.y })
.each("start", function () { lock.call(this, "lockedY") })
.each("end", function () { unlock.call(this, "lockedY") });
changes.attr("stroke", "white")
.style("stroke-opacity", 0.6)
.transition("fill").duration(800)
.style('fill', function (d, i) { return d.c })
.style("stroke-opacity", 0)
.each("start", function () { lock.call(this, "lockedFill") })
.each("end", function () { unlock.call(this, "lockedFill") });
}
squares = createGrid(gridWidth, gridHeight);
d3.timer(function tick() {
permutateColours();
permutatePositions();
updateSquares();
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<div class="svg-container"></div>
NOTE: requires d3 version 3.5.5 for the position transitions to run.
EDIT: fixed a problem with lock and un-lock. Would probably better to tag the data rather than write classes to the DOM but, anyway... this way is fun.