I am trying to draw multi color circles in a canvas image. When I draw one circle previous circle was deleted.So I make a function to replicate the previous circle but there I was unable to get the exact coordinate of the center of the circle and a straight line is created. I am sharing my code.
In the draw circle function I need the proper centerx and centery.
class Circle extends Component {
constructor(props) {
super(props);
//added state
this.state = {
isDown: false,
previousPointX: '',
previousPointY: '',
base_image: {},
circleConfig: {
maxCircle: 4,
color: ["red", "blue", "#ffa500", "green"]
},
circles: [],
canvasId: this.props.canvasid,
rotate:this.props.rotate
}
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
render() {
return (
<div>
<canvas ref="canvas" className="CursorCanvas"
width={400}
height={390}
onMouseDown={
e => {
let nativeEvent = e.nativeEvent;
this.handleMouseDown(nativeEvent);
}}
onMouseMove={
e => {
let nativeEvent = e.nativeEvent;
this.handleMouseMove(nativeEvent);
}}
onMouseUp={
e => {
let nativeEvent = e.nativeEvent;
this.handleMouseUp(nativeEvent);
}}
/>
<pre hidden>{JSON.stringify(this.state.lines, null, 2)}</pre>
</div>
);
}
drawCircle(circles, ctx) {
console.log('draw circle',circles)
circles.forEach((item) => {
var r=(item.endx-item.startx)/2;
var centerx=(item.endx-item.startx)/2;
var centery=(item.endy-item.starty)/2;
ctx.arc(centerx, centery, r, 0, 2 * Math.PI);
ctx.strokeStyle = item.color ;
})
}
handleMouseDown(event) {
if (this.state.circles.length >= this.state.circleConfig.maxCircle) return;
this.setState({
isDown: true,
previousPointX: event.offsetX,
previousPointY: event.offsetY
},()=>{
console.log('mousedown',this.state)
})
}
handleMouseMove(event){
if (this.state.isDown) {
event.preventDefault();
event.stopPropagation();
const canvas = ReactDOM.findDOMNode(this.refs.canvas);
var x = event.offsetX;
var y = event.offsetY;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this.state.base_image, 0, 0);
//Save
ctx.save();
ctx.beginPath();
this.drawCircle(this.state.circles,ctx);
var circleLength = this.state.circles.length || 0;
//Dynamic scaling
var scalex = (x-this.state.previousPointX)/2;
var scaley = (y-this.state.previousPointY)/2;
ctx.scale(scalex,scaley);
//Create ellipse
var centerx = (this.state.previousPointX/scalex)+1;
var centery = (this.state.previousPointY/scaley)+1;
ctx.arc(centerx, centery, 1, 0, 2*Math.PI);
ctx.restore();
ctx.stroke();
ctx.strokeStyle = this.state.circleConfig.color[circleLength];;
}
}
handleMouseUp(event) {
if (this.state.circles.length >= this.state.circleConfig.maxCircle) return;
this.setState({
isDown: false
});
console.log('mouseup',this.state)
const canvas = ReactDOM.findDOMNode(this.refs.canvas);
var x = event.offsetX;
var y = event.offsetY;
var ctx = canvas.getContext("2d");
var circleLength = this.state.circles.length || 0;
if (this.state.previousPointX !== x && this.state.previousPointY !== y) {
this.setState({
circles: this.state.circles.concat({
startx: this.state.previousPointX,
starty: this.state.previousPointY,
endx: x,
endy: y,
color: this.state.circleConfig.color[circleLength]
})
},
() => {
ctx.stroke();
ctx.closePath();
this.props.drawCircleToStore(this.state.circles, this.state.canvasId, this.props.imgSrc,this.state.rotate)
}
);
}
}
componentDidMount(){
const canvas = ReactDOM.findDOMNode(this.refs.canvas);
const ctx = canvas.getContext("2d");
const base_image = new Image();
base_image.src = this.props.imgSrc
base_image.onload = function (){
ctx.drawImage(base_image, 0, 0);
}
this.setState({
base_image: base_image
});
}
}
In the drawCircle metho the center coordinate wil be
var r = (item.endx - item.startx) / 2;
centerx = (r + item.startx);
centery = (r + item.starty);
ctx.arc(centerx, centery, r, 0, 2 * Math.PI);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
class CircleMultiple extends Component {
constructor(props) {
super(props);
//added state
this.state = {
isDown: false,
previousPointX: '',
previousPointY: '',
base_image: {},
circleConfig: {
maxCircle: 4,
},
circles: [],
canvasId: this.props.canvasid,
rotate: this.props.rotate
}
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
render() {
return (
<div>
<canvas ref="canvas" className="CursorCanvas" width="300" height="300"
onMouseDown={
e => {
let nativeEvent = e.nativeEvent;
this.handleMouseDown(nativeEvent);
}}
onMouseMove={
e => {
let nativeEvent = e.nativeEvent;
this.handleMouseMove(nativeEvent);
}}
onMouseUp={
e => {
let nativeEvent = e.nativeEvent;
this.handleMouseUp(nativeEvent);
}}
/>
<pre hidden>{JSON.stringify(this.state.circles, null, 2)}</pre>
</div>
);
}
drawCircle(circles, ctx) {
circles.forEach((item) => {
ctx.beginPath();
var r = (item.endx - item.startx) / 2;
var centerx = (r + item.startx);
var centery = (r + item.starty);
ctx.arc(centerx, centery, r, 0, 2 * Math.PI);
ctx.stroke();
ctx.closePath();
})
}
handleMouseDown(event) {
if (this.state.circles.length >= this.state.circleConfig.maxCircle) return;
this.setState({
isDown: true,
previousPointX: event.offsetX,
previousPointY: event.offsetY
}, () => {
// console.log('mousedown',this.state)
})
}
handleMouseMove(event) {
if (this.state.isDown) {
event.preventDefault();
event.stopPropagation();
const canvas = ReactDOM.findDOMNode(this.refs.canvas);
var x = event.offsetX;
var y = event.offsetY;
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ctx.drawImage(this.state.base_image, 0, 0);
ctx.drawImage(this.state.base_image,
canvas.width / 2 - this.state.base_image.width / 2,
canvas.height / 2 - this.state.base_image.height / 2
);
//Save
ctx.save();
ctx.beginPath();
this.drawCircle(this.state.circles, ctx);
// var circleLength = this.state.circles.length || 0;
//Dynamic scaling
var scalex = (x - this.state.previousPointX) / 2;
var scaley = (y - this.state.previousPointY) / 2;
ctx.scale(scalex, scaley);
//Create ellipse
var centerx = (this.state.previousPointX / scalex) + 1;
var centery = (this.state.previousPointY / scaley) + 1;
ctx.beginPath();
ctx.arc(centerx, centery, 1, 0, 2 * Math.PI);
ctx.restore();
ctx.stroke();
// ctx.strokeStyle = this.state.circleConfig.color[circleLength];
// console.log('centerx',centerx,'centery',centery)
}
}
handleMouseUp(event) {
if (this.state.circles.length >= this.state.circleConfig.maxCircle) return;
this.setState({
isDown: false
});
// console.log('mouseup',this.state)
var x = event.offsetX;
var y = event.offsetY;
if (this.state.previousPointX !== x && this.state.previousPointY !== y) {
this.setState({
circles: this.state.circles.concat({
startx: this.state.previousPointX,
starty: this.state.previousPointY,
endx: x,
endy: y,
r: (x - this.state.previousPointX) / 2,
centerx: (((x - this.state.previousPointX) / 2) + this.state.previousPointX),
centery: (((x - this.state.previousPointX) / 2) + this.state.previousPointY)
// color: this.state.circleConfig.color[circleLength]
})
},
() => {
//console.log('mouseup', this.state);
}
);
}
}
componentDidMount() {
const canvas = ReactDOM.findDOMNode(this.refs.canvas);
const ctx = canvas.getContext("2d");
const base_image = new Image();
base_image.src = this.props.imgSrc
base_image.onload = function () {
// ctx.drawImage(base_image, 0, 0);
ctx.drawImage(base_image,
canvas.width / 2 - base_image.width / 2,
canvas.height / 2 - base_image.height / 2
);
}
this.setState({
base_image: base_image
});
}
}
export default CircleMultiple;
To remove the connecting line between circles need to add
ctx.beginPath();
before calling drawCircle() method.
Related
I am using THREE.js in react.
I want to fill 500 number of 1X1X1 box geometry in a 200x200x200 area randomly.
The following code shows how to get the position.xyz with the range from -100 to 100.
However, how to get the position that all 1X1X1 box geometry could not touch each other (no overlapping)?
I have no idea.
function Geometry() {
const geo_num = 500;
const box_length = 200;
const geo_position = useMemo(() => {
const array = []
for (let i = 0; i < geo_num; i++) {
const xdirection = Math.round(Math.random()) * 2 - 1;
const ydirection = Math.round(Math.random()) * 2 - 1;
const zdirection = Math.round(Math.random()) * 2 - 1;
const xpos = Math.random() * box_length * xdirection * 0.5;
const ypos = Math.random() * box_length * ydirection * 0.5;
const zpos = Math.random() * box_length * zdirection * 0.5;
array.push(new THREE.Vector3(xpos, ypos, zpos))
}
return array
})
return (
<>
{
geo_position.map((element, index) => (
<mesh position={element} key={index} >
<boxGeometry args={[1, 1, 1]} />
</mesh>
))
}
</>
)
}
A very rough concept of how you can do it, using an array and its .includes() method:
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.136.0";
import {OrbitControls} from "https://cdn.skypack.dev/three#0.136.0/examples/jsm/controls/OrbitControls";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(50, 50, 250);
let renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let contols = new OrbitControls(camera, renderer.domElement);
let boxCount = 500;
let range = [-100, 100];
let v3Array = [];
let counter = 0;
let v3 = new THREE.Vector3();
while(counter < boxCount){
let v3 = [
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
].join("|");
if (!v3Array.includes(v3)){
v3Array.push(v3);
counter++
}
}
v3Array.map( p => {
let o = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({color: Math.random() * 0xffffff, wireframe: true}));
let pos = p.split("|").map(c => {return parseInt(c)});
o.position.set(pos[0], pos[1], pos[2]);
scene.add(o);
});
let boxHelper = new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3().setScalar(range[0] - 0.5), new THREE.Vector3().setScalar(range[1] + 0.5)), "yellow");
scene.add(boxHelper);
renderer.setAnimationLoop( () => {
renderer.render(scene, camera);
})
</script>
I have a component that draws a sequence that it gets from my redux store:
const sequence = useSelector(state => state.sequence)
My useEffect call that does the canvas draw is being triggered whenever ‘sequence’ updates:
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(sequence)
});
}, [sequence])
But for some reason the ‘sequence’ populated by useSelector is behind my store state. If I draw sequence instead of the store state it draws the correct state:
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(store.getState().sequence)
});
}, [sequence])
I don't get why I need to use the store state here instead of the selector. What am I not understanding?
Here is the full module:
import React, { useEffect, useRef } from 'react';
import store from '../app/store'
import { useSelector } from "react-redux";
import * as PIXI from 'pixi.js'
import SongPlayer from "../player/song-player";
Math.minmax = (value, min, max) => Math.min(Math.max(value, min), max);
var pixiapp;
var container;
var parentContainer;
var renderer;
var height;
var width;
var mouseIsDown = false;
var isDraggingVertical = false
var isDraggingHorizontal = false
const dragThresholdVertical = 10
const dragThresholdHorizontal = 10
const grey = 0x404040
const darkgrey = 0x707070
const green = 0x10a010
const brightRed = 0xdb5660
const brightGreen = 0x56db60
const blue = 0x0000ff
const borderColor = 0x1e1e1e
const envelopeLineColor = 0xff0000
const backgroundColor = 0x28282b
const darkgreen = 0x107010
const red = 0xff0000
const white = 0xffffff;
const colorVelocityBar = 0x5e5f68;
const middleNote = 60;
const minNote = middleNote - 24;
const maxNote = middleNote + 24;
const borderWidth = 10;
const bottomBorderHeight = 20;
const topBorderHeight = 20;
const gapWidth = 4;
const barWidth = 50;
const numSteps = 16;
const lineThickness = 0.005;
const DragTargets = {
None: 0,
NoteBox: 1,
OctaveBox: 2,
VelocityBox: 3,
}
const DragDirections = {
None: 0,
Vertical: 1,
Horizontal: 2,
}
var dragTarget
var dragDirection
var dragStepNum = -1
var dragStartPos = { x: -1, y: -1, }
const noteBarTextStyle = new PIXI.TextStyle({
align: "center",
fill: "#000000",
fontFamily: "Helvetica",
fontSize: 12
});
const turnAKnobTextStyle = new PIXI.TextStyle({
align: "center",
fill: "#000000",
fontFamily: "Helvetica",
fontSize: 36,
});
const noteName = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
]
function getControllerValueTop(value, minValue, maxValue) {
// return bottomBorderHeight + (value - minvalue) / (maxValue - minvalue) * (height - bottomBorderHeight - topBorderHeight) / 127
return height - bottomBorderHeight - (value - minValue) / (maxValue - minValue) * (height - bottomBorderHeight - topBorderHeight);
}
function getTimeX(sequence, time) {
const maxTime = sequence.steps.length * sequence.tempo / 60 / sequence.division
// console.log(`maxTime ${maxTime} sequence.steps.length ${sequence.steps.length} sequence.tempo ${sequence.tempo} sequence.division ${sequence.division}`)
const timex = borderWidth + time / maxTime * (width - borderWidth - borderWidth)
// console.log(`time ${time} timex ${timex} borderWidth ${borderWidth} maxTime ${maxTime} width ${width}`)
return timex
}
// function getXTime(sequence, x) {
// const maxTime = sequence.steps * sequence.tempo / 60 / sequence.division
// return borderWidth + x / (width - borderWidth - borderWidth) * maxTime
// }
function getNoteTop(note) {
return height - bottomBorderHeight - (note + 12 - minNote) / (maxNote - minNote + 12) * (height - bottomBorderHeight - topBorderHeight);
}
function getNoteBottom(note) {
return getNoteTop(note - 12) + lineThickness;
// return bottomBorderHeight - lineThickness + (note - minNote) / (maxNote - minNote + 12) * (1 - bottomBorderHeight - topBorderHeight);
}
function getNoteHeight() {
return (height - bottomBorderHeight - topBorderHeight) * 12 / (maxNote - minNote + 12);
}
function getStepLeft(sequence, stepNum) {
var stride = getStepStride(sequence)
// console.log(`getStepLeft: borderWidth ${borderWidth} num steps ${sequence.steps.length} stride ${stride}`)
return borderWidth + stepNum * stride;
}
function getStepRight(sequence, stepNum) {
var stride = (width - (borderWidth * 2)) / sequence.steps.length;
var step = sequence.steps[stepNum];
return getStepLeft(sequence, stepNum) + stride * step.gateLength - lineThickness;
}
function getStepStride(sequence) {
var stride = (width - (borderWidth * 2)) / sequence.steps.length;
// console.log(`getStepStride: ${sequence.numSteps} -> ${stride}`)
return stride
}
function getStepWidth(sequence, stepNum) {
var stride = getStepStride(sequence);
var step = sequence.steps[stepNum];
return stride * step.gateLength;
}
function drawSequenceCanvas(sequence) {
const sequencePlayer = SongPlayer.getSequencePlayer(sequence._id)
// console.log(`draw: sequence player is ${JSON.stringify(sequencePlayer)}`)
const stepnum = sequencePlayer ? sequencePlayer.stepNum : -1;
// console.log(`stepnum ${stepnum}`)
if (container) {
container.destroy({ children: true })
}
container = new PIXI.Container();
parentContainer.addChild(container);
// Background
var bar = new PIXI.Graphics();
// bar.lineStyle(2, 0x00FFFF, 1);
bar.beginFill(backgroundColor, 1);
bar.drawRect(0, 0, width, height);
bar.endFill();
container.addChild(bar);
// Top border
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, 0, width, topBorderHeight, 16);
bar.endFill();
container.addChild(bar);
// Bottom border
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, height-bottomBorderHeight, width, bottomBorderHeight);
bar.endFill();
container.addChild(bar);
// Sides
bar = new PIXI.Graphics();
bar.beginFill(borderColor, 1);
bar.drawRect(0, topBorderHeight, borderWidth, height - bottomBorderHeight - topBorderHeight);
bar.drawRect(width - borderWidth, topBorderHeight, borderWidth, height - bottomBorderHeight - topBorderHeight);
bar.endFill();
container.addChild(bar);
// Velocity bars
var stepNum = 0;
var barColor = colorVelocityBar;
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
bar.beginFill(barColor, 1);
var barHeight = height - bottomBorderHeight - getControllerValueTop(step.velocity, 0, 127);
bar.drawRect(getStepLeft(sequence, stepNum), height - bottomBorderHeight - barHeight, getStepWidth(sequence, stepNum), barHeight);
bar.endFill();
container.addChild(bar);
++stepNum;
})
// Horizontal grid lines
const lineColor = borderColor;
var lines = new PIXI.Graphics();
// console.log(`minnote ${minNote} ${getNoteTop(minNote)} ${getNoteTop(minNote - 12)}`)
lines.position.set(borderWidth, getNoteTop(minNote - 12));
lines.lineStyle(2, lineColor, 1)
.moveTo(borderWidth, getNoteTop(minNote - 12))
.lineTo(width - borderWidth, getNoteTop(minNote - 12))
.moveTo(borderWidth, getNoteTop(minNote))
.lineTo(width - borderWidth, getNoteTop(minNote))
.moveTo(borderWidth, getNoteTop(minNote + 12))
.lineTo(width - borderWidth, getNoteTop(minNote + 12))
.moveTo(borderWidth, getNoteTop(minNote + 24))
.lineTo(width - borderWidth, getNoteTop(minNote + 24))
.moveTo(borderWidth, getNoteTop(minNote + 36))
.lineTo(width - borderWidth, getNoteTop(minNote + 36))
.moveTo(borderWidth, getNoteTop(minNote + 48))
.lineTo(width - borderWidth, getNoteTop(minNote + 48))
container.addChild(lines);
// Note bars
stepNum = 0;
console.log(`SequenceCanvas.draw - note bars - ${JSON.stringify(sequence.steps)}`);
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
barColor = sequencePlayer && stepNum === sequencePlayer.stepNum ? brightRed : brightGreen;
bar.beginFill(barColor, 1);
var barHeight = height - bottomBorderHeight - getControllerValueTop(step.velocity, 0, 127);
bar.drawRoundedRect(getStepLeft(sequence, stepNum), getNoteTop(step.note), getStepWidth(sequence, stepNum), getNoteHeight(), 8);
bar.endFill();
container.addChild(bar);
const noteText = new PIXI.Text(noteName[step.note % 12], noteBarTextStyle)
noteText.anchor.set(0.5);
// noteText.width = getStepStride(sequence)
noteText.x = getStepLeft(sequence, stepNum) + getStepWidth(sequence, stepNum) / 2
noteText.y = getNoteTop(step.note) + getNoteHeight() / 2
container.addChild(noteText)
++stepNum;
})
// Bottom bars
stepNum = 0;
barColor = green;
// console.log(`SequenceCanvas.Init ${JSON.stringify(sequence.steps)}`);
sequence.steps.forEach(step => {
const bar = new PIXI.Graphics();
// bar.lineStyle(2, 0xFF00FF, 1);
bar.beginFill(0x707481, 1);
bar.drawRoundedRect(getStepLeft(sequence, stepNum), height - bottomBorderHeight, getStepWidth(sequence, stepNum), bottomBorderHeight, 4);
bar.endFill();
container.addChild(bar);
const noteText = new PIXI.Text((step.note % 12), noteBarTextStyle)
noteText.anchor.set(0.5);
// noteText.width = getStepStride(sequence)
noteText.x = getStepLeft(sequence, stepNum) + getStepStride(sequence) / 2
noteText.y = height - bottomBorderHeight + 10
container.addChild(noteText)
++stepNum;
})
if (sequence.currentEnvelopeId != 'notes') {
DrawEnvelopes(container, sequence)
}
// // Random box for testing
// bar = new PIXI.Graphics();
// bar.lineStyle(2, 0x00FFFF, 1);
// bar.beginFill(0x650A5A, 0.25);
// bar.drawRoundedRect(height, getNoteTop(minNote), 50, getNoteHeight(minNote), 16);
// bar.endFill();
// container.addChild(bar);
}
function DrawEnvelopes(container, sequence) {
if (sequence.currentEnvelopeId) {
console.log(`DrawEnvelopes: ${sequence.currentEnvelopeId}`)
if (sequence.currentEnvelopeId == "notes") {
// don't draw envelopes
} else if (sequence.currentEnvelopeId == "new" || sequence.currentEnvelopeId == "learn") {
console.log(`draw warning`)
const bar = new PIXI.Graphics();
bar.beginFill(white, 0.5);
bar.drawRect(0, 0, width, height);
bar.endFill();
container.addChild(bar);
const turnAKnobText = new PIXI.Text("Turn a knob to map", turnAKnobTextStyle)
turnAKnobText.anchor.set(0.5);
turnAKnobText.width = width / 2
turnAKnobText.x = width / 2
turnAKnobText.y = height / 2
container.addChild(turnAKnobText)
} else {
DrawEnvelope(container, sequence, sequence.currentEnvelopeId)
}
}
}
function DrawEnvelope(container, sequence, envelopeId) {
console.log(`DrawEnvelope: ${envelopeId} ${JSON.stringify(sequence)}`)
const envelope = sequence.envelopes[envelopeId]
if (!envelope) {
return
}
console.log(`DrawEnvelope: ${envelopeId} - envelope.controller ${JSON.stringify(envelope.controller)}`)
if (envelope.controller !== "notes") {
console.log(`DrawEnvelope: ${envelope.controller}`)
if (envelope.controller == null) {
} else {
const points = envelope.points
// console.log(`DrawEnvelope: We have ${envelope.controllers.length} controllers with points ${JSON.stringify(points)}`)
// console.log(`DrawEnvelope: point 0 ${JSON.stringify(points[0])}`)
// Lines
const lineColor = envelopeLineColor;
var lines = new PIXI.Graphics();
lines.position.set(0, 0);
lines.lineStyle(1, lineColor, 1)
var y = getControllerValueTop(points[0].value, 0, 127)
lines.moveTo(0, y)
for (const [pointTime, point] of Object.entries(points)) {
var x = getTimeX(sequence, point.time)
y = getControllerValueTop(point.value, 0, 127)
// console.log(`point ${JSON.stringify(point)} draw line to time ${point.time} value ${point.value} ${x},${y}`)
lines.lineTo(x, y)
}
lines.lineTo(width, y)
// Dots
lines.fillStyle = { color: lineColor, alpha: 0.5, visible: true }
lines.beginFill(lineColor)
for (const [pointTime, point] of Object.entries(points)) {
var x = getTimeX(sequence, point.time)
y = getControllerValueTop(point.value, 0, 127)
// console.log(`point ${JSON.stringify(point)} draw line to time ${point.time} value ${point.value} ${x},${y}`)
lines.drawCircle(x, y, 4)
}
container.addChild(lines);
}
}
}
const handlePointerDown = e => {
const sequence = store.getState().sequence
console.log(`handlePointerDown ${mouseIsDown} ${JSON.stringify(e.data)}`)
mouseIsDown = true;
var x = e.data.global.x
var y = e.data.global.y
dragStartPos = { x: x, y: y, }
dragStepNum = -1
for (var stepNum = 0; stepNum < sequence.steps.length; stepNum++) {
var stepleft = getStepLeft(sequence, stepNum);
if (x > stepleft && x < getStepRight(sequence, stepNum)) {
dragStepNum = stepNum
console.log(`tapped step ${dragStepNum}`)
break
}
}
console.log(`handlePointerDown: ${x},${y} dragStepNum ${dragStepNum} target ${dragTarget}`)
console.log(`handlePointerDown: step ${JSON.stringify(sequence.steps[stepNum])}`)
if (dragStartPos.y > height - bottomBorderHeight) {
dragTarget = DragTargets.NoteBox
} else if (dragStartPos.y < getNoteBottom(sequence.steps[stepNum].note) && dragStartPos.y > getNoteTop(sequence.steps[stepNum].note)) {
dragTarget = DragTargets.OctaveBox
} else if (dragStartPos.y > bottomBorderHeight && dragStartPos.y < height - topBorderHeight) {
console.log(`drag velocity box y = ${dragStartPos.y}`)
dragTarget = DragTargets.VelocityBox
}
console.log(`handlePointerDown: step ${dragStepNum} target ${dragTarget}`)
}
const handlePointerUp = e => {
const sequence = store.getState().sequence
console.log(`handlePointerUp ${mouseIsDown}`)
mouseIsDown = false;
isDraggingVertical = false;
isDraggingHorizontal = false;
}
const handlePointerMove = e => {
const sequence = store.getState().sequence
if (mouseIsDown) {
var x = e.data.global.x
var y = e.data.global.y
if (x !== null && y !== null && x !== Infinity && y !== Infinity) {
// console.log(`drag ${JSON.stringify(e.data)}`)
if (dragTarget === DragTargets.OctaveBox) {
// Detect direction before we start editing
if (!isDraggingVertical && !isDraggingHorizontal) {
if (Math.abs(y - dragStartPos.y) > dragThresholdVertical) {
isDraggingVertical = true;
}
else if (Math.abs(x - dragStartPos.x) > dragThresholdHorizontal) {
isDraggingHorizontal = true;
}
else {
return
}
}
if (isDraggingVertical) {
console.log(`drag note: ${sequence.steps[dragStepNum].note} ${x},${y} `)
const notenum = sequence.steps[dragStepNum].note
if (y < getNoteTop(notenum)) {
console.log(`drag y ${y} < ${getNoteTop(notenum)} getNoteTop note for note ${notenum}, dragStepNum ${dragStepNum}`)
store.dispatch({type: "sequence/stepNote", payload: {stepNum: dragStepNum, note: notenum + 12}})
} else if (y > getNoteBottom(notenum)) {
console.log(`drag y ${y} > ${getNoteBottom(notenum)} getNoteBottom for note ${notenum}, dragStepNum ${dragStepNum}`)
store.dispatch({type: "sequence/stepNote", payload: {stepNum: dragStepNum, note: notenum - 12}})
}
}
else {
var stride = getStepStride(sequence);
// note.gateLength
// x = Math.minmix(x, getStepLeft(sequence, dragStepNum), getStepRight(sequence, dragStepNum))
var newGateLength = (x - getStepLeft(sequence, dragStepNum)) / getStepStride(sequence)
if (x < getStepLeft(sequence, dragStepNum)) {
newGateLength = 0;
} else if (x > getStepLeft(sequence, dragStepNum) + getStepStride(sequence)) {
newGateLength = 1.0
}
store.dispatch({
type: "sequence/stepGateLength",
payload: {stepNum: dragStepNum, gateLength: newGateLength}
})
}
}
else if (dragTarget === DragTargets.NoteBox) {
const notenum = sequence.steps[dragStepNum].note
if (y - dragStartPos.y > bottomBorderHeight) {
store.dispatch({ type: "sequence/stepNote", payload: { stepNum: dragStepNum, note: notenum - 1 } })
dragStartPos.y = y
} else if (y - dragStartPos.y < -bottomBorderHeight) {
store.dispatch({ type: "sequence/stepNote", payload: { stepNum: dragStepNum, note: notenum + 1 } })
dragStartPos.y = y
}
}
else if (dragTarget === DragTargets.VelocityBox) {
const velocity = sequence.steps[dragStepNum].velocity
y = Math.minmax(y, topBorderHeight, height - bottomBorderHeight)
const newVelocity = 127 - (127 * (y - topBorderHeight) / (height - topBorderHeight - bottomBorderHeight))
console.log(`drag velocitybox ${y} => ${newVelocity}`)
store.dispatch({ type: "sequence/stepVelocity", payload: { stepNum: dragStepNum, velocity: newVelocity } })
}
}
}
}
function SequenceCanvas(props) {
console.log(`hi from SequenceCanvas ${JSON.stringify(props)}`)
// The React way to get a dom element is by giving it a ref prop
const canvasRef = useRef(null)
const sequence = useSelector(state => state.sequence)
useEffect( () => {
// console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
width = props.width;
height = props.height;
console.log("new PIXI.Application")
pixiapp = new PIXI.Application({
view: canvasRef.current,
powerPreference: 'high-performance',
backgroundAlpha: false,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
width: width,
height: height
});
renderer = PIXI.autoDetectRenderer();
parentContainer = new PIXI.Container();
parentContainer.interactive = true
parentContainer.on('pointerdown', (e) => { handlePointerDown(e) })
parentContainer.on('pointerup', (e) => { handlePointerUp(e) })
parentContainer.on('mouseupoutside', (e) => { handlePointerUp(e) })
parentContainer.on('pointermove', (e) => { handlePointerMove(e) })
pixiapp.stage.addChild(parentContainer);
drawSequenceCanvas(sequence)
return () => {
parentContainer.removeAllListeners()
}
}, [])
useEffect(() => {
store.subscribe(() => {
console.log(`sequence-canvas: store state is now ${JSON.stringify(store.getState())}`)
drawSequenceCanvas(store.getState().sequence)
});
}, [sequence])
const timerIdRef = useRef(0)
function handleInterval() {
console.log(`hi from sequenceCanvas.IntervalTimer`)
drawSequenceCanvas(sequence)
}
const startIntervalTimer = () => {
console.log(`SequenceCanvas.startIntervalTimer - timerIdRef.current === ${timerIdRef.current}`)
if (timerIdRef.current == 0) {
timerIdRef.current = setInterval(() => {
handleInterval()
})
}
}
const stopIntervalTimer = () => {
console.log(`SequenceCanvas.stopIntervalTimer`)
clearInterval(timerIdRef.current)
timerIdRef.current = 0
}
useEffect(() => {
console.log(`startIntervalTime from useEffect`)
// startIntervalTimer();
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<div>
<canvas {...props} ref={canvasRef}>
Your browser does not support the HTML canvas tag.
</canvas>
</div>
</div>
);
}
// export {SequenceCanvas, drawSequenceCanvas};
export {SequenceCanvas};
Every time that sequence is changed - you create new store subscribe on useEffect hook - more and more. You can make it work like this:
const sequence = useSelector(state => state.sequence);
const sequenceRef = useRef(sequence);
sequenceRef.current = sequence;
useEffect(() => {
return store.subscribe(() => { // <--- return will unsubscribe from store
drawSequenceCanvas(sequenceRef.current); // <---actual link
});
}, []);
or you can do it like this
const sequence = useSelector(state => state.sequence);
useEffect(() => {
return store.subscribe(() => { // <--- return will unsubscribe from store
drawSequenceCanvas(sequence);
});
}, [sequence]);
BUT - useSelector hook is already uses store subscribe - so you can do just like this:
const sequence = useSelector(state => state.sequence);
useEffect(() => {
drawSequenceCanvas(sequence);
}, [sequence]);
// OR with usePrevious hook - https://usehooks.com/usePrevious/
const sequence = useSelector(state => state.sequence);
const prevSequence = usePrevious(sequence);
if (sequence !== prevSequence) {
drawSequenceCanvas(sequence);
}
I have a react app that allows you to draw and select different line widths.
Code:
const [sliderVal, setSliderVal] = useState(1)
const canvasRef = useRef(null)
useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
/* Mouse Capturing Work */
var mouse = {x: 0, y: 0};
var last_mouse = {x: 0, y: 0};
canvas.addEventListener('mousemove', function(e) {
last_mouse.x = mouse.x;
last_mouse.y = mouse.y;
mouse.x = e.pageX - this.offsetLeft;
mouse.y = e.pageY - this.offsetTop;
canvas.style.cursor = "crosshair";
}, false);
/* Drawing on Paint App */
ctx.lineWidth = sliderVal;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'blue';
canvas.addEventListener('mousedown', function() {
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', function() {
canvas.removeEventListener('mousemove', onPaint, false);
}, false);
var onPaint = function() {
ctx.beginPath();
ctx.moveTo(last_mouse.x, last_mouse.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.closePath();
ctx.stroke();
};
}, [sliderVal])
I'm trying to make it so if sliderVal changes the whole canvas wont re-render only the line width will. Right now it works but if I select a new lineWidth the whole canvas gets cleared.
I recommend you not to use the eventListener in that way, canvas in ReactJS already has some methods to which you can access and pass a function. This new way of handling the canvas solves your problem:
const [sliderVal, setSliderVal] = useState(1);
const [mouse, setMouse] = useState({
x: 0,
y: 0,
lastX: 0,
lastY: 0,
click: false
});
const canvasRef = useRef(null);
const paint = (e) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const bounds = canvas.getBoundingClientRect();
setMouse({
...mouse,
x: e.pageX - bounds.left - window.scrollX,
y: e.pageY - bounds.top - window.scrollY,
lastX: mouse.x,
lastY: mouse.y
});
ctx.lineWidth = sliderVal;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.strokeStyle = "blue";
if (mouse.click) {
ctx.beginPath();
ctx.moveTo(mouse.lastX, mouse.lastY);
ctx.lineTo(mouse.x, mouse.y);
ctx.closePath();
ctx.stroke();
}
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
canvas.style.cursor = "crosshair";
}, []);
return (
<canvas
onMouseDown={() => setMouse({ ...mouse, click: true })}
onMouseMove={(e) => paint(e)}
onMouseUp={() => setMouse({ ...mouse, click: false })}
ref={canvasRef}
/>
I share a codesandbox with the functional solution
I wanted to implement the code from this video:
https://www.youtube.com/watch?v=nPEYdw2Ssa8
(you can download it from here https://codepen.io/i2801/pen/waQPQj)
but using React. I had no luck and got error:
Attempted import error: 'CSS3DObject' is not exported from 'three' (imported as 'THREE').
Of course I have installed all the dependencies i tried to import
So here is My code
import React, { Component } from 'react';
import * as THREE from 'three';
import OrbitControls from 'three-orbitcontrols';
import Tween from 'three-tween';
import TrackballControls from 'three-trackballcontrols';
import CSS3DRenderer from 'three-css3drenderer';
export default class Scene extends Component {
componentWillMount() {
window.addEventListener('resize', this.handleWindowResize)
}
componentDidMount() {
this.setupScene();
this.THREE = THREE;
}
setupScene = () => {
this.THREE = THREE;
let objects = [];
let targets = { table: [], sphere: [], helix: [], grid: [] };
let table = [
"H", "Hydrogen", "1.00794", 1, 1,
//Deleted most of the stuff to save space
"Uuo", "Ununoctium", "(294)", 18, 7
];
this.width = this.container.clientWidth;
this.height = this.container.clientHeight;
const renderer = new THREE.CSS3DRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.domElement.style.position = 'absolute';
document.getElementById('container').appendChild(renderer.domElement);
let scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 3000;
scene.add(camera);
this.renderer = renderer;
this.scene = scene;
this.camera = camera;
this.objects = objects;
this.targets = targets;
for (var i = 0; i < table.length; i += 5) {
var element = document.createElement('div');
element.className = 'element';
element.style.backgroundColor = 'rgba(0,127,127,' + (Math.random() * 0.5 + 0.25) + ')';
var number = document.createElement('div');
number.className = 'number';
number.textContent = (i / 5) + 1;
element.appendChild(number);
var symbol = document.createElement('div');
symbol.className = 'symbol';
symbol.textContent = table[i];
element.appendChild(symbol);
var details = document.createElement('div');
details.className = 'details';
details.innerHTML = table[i + 1] + '<br>' + table[i + 2];
element.appendChild(details);
var object = new THREE.CSS3DObject(element);
object.position.x = Math.random() * 4000 - 2000;
object.position.y = Math.random() * 4000 - 2000;
object.position.z = Math.random() * 4000 - 2000;
scene.add(object);
objects.push(object);
var object = new THREE.Object3D();
object.position.x = (table[i + 3] * 140) - 1260;
object.position.y = - (table[i + 4] * 180) + 990;
targets.table.push(object);
}
for (var i = 0; i < objects.length; i++) {
var object = new THREE.Object3D();
object.position.x = ((i % 5) * 400) - 800;
object.position.y = (- (Math.floor(i / 5) % 5) * 400) + 800;
object.position.z = (Math.floor(i / 25)) * 1000 - 2000;
targets.grid.push(object);
}
//
this.renderer = new THREE.CSS3DRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.domElement.style.position = 'absolute';
document.getElementById('container').appendChild(renderer.domElement);
//
this.controls = new THREE.TrackballControls(camera, renderer.domElement);
this.controls.rotateSpeed = 0.5;
this.controls.minDistance = 500;
this.controls.maxDistance = 6000;
this.controls.addEventListener('change', this.renderScene);
var button = document.getElementById('table');
button.addEventListener('click', event => {
this.transform(targets.table, 2000);
}, false);
// var button = document.getElementById('sphere');
// button.addEventListener('click', event => {
// transform(targets.sphere, 2000);
// }, false);
// var button = document.getElementById('helix');
// button.addEventListener('click', event => {
// transform(targets.helix, 2000);
// }, false);
// var button = document.getElementById('grid');
// button.addEventListener('click', event => {
// transform(targets.grid, 2000);
// }, false);
this.transform(targets.table, 2000);
//
this.start();
}
transform = (targets, duration) => {
TWEEN.removeAll();
for (var i = 0; i < objects.length; i++) {
var object = this.objects[i];
var target = this.targets[i];
new TWEEN.Tween(object.position)
.to({ x: target.position.x, y: target.position.y, z: target.position.z }, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
new TWEEN.Tween(object.rotation)
.to({ x: target.rotation.x, y: target.rotation.y, z: target.rotation.z }, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 2)
.onUpdate(this.renderScene)
.start();
}
start = () => {
if (!this.frameId) {
this.frameId = requestAnimationFrame(this.animate)
}
}
renderScene = () => {
this.renderer.render(this.scene, this.camera)
}
animate = () => {
this.frameId = requestAnimationFrame(this.animate);
this.renderScene();
TWEEN.update();
this.controls.update();
}
stop = () => {
cancelAnimationFrame(this.frameId);
}
handleWindowResize = () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderScene();
}
componentWillUnmount = () => {
this.stop();
this.destroyContext();
}
destroyContext = () => {
this.container.removeChild(this.renderer.domElement);
this.renderer.forceContextLoss();
this.renderer.context = null;
this.renderer.domElement = null;
this.renderer = null;
}
render() {
const width = '100%';
const height = '100%';
return (
<div
ref={(container) => { this.container = container }}
style={{ width: width, height: height, position: 'absolute', overflow: 'hidden' }}
>
</div>
)
}
}
Thanks in advance,
Stack is complaining about my post being mostly code so here is some dummy text to bypass it: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales volutpat ligula, sed iaculis metus consectetur eget. Sed vulputate tellus vel velit consequat scelerisque. Vestibulum sodales massa risus, ac
THREE.CSS3DObject is not part of the three.js core. But you should be able to import it from the npm module three-css3drenderer. Try this:
import { CSS3DObject } from 'three-css3drenderer';
Also ensure to create you renderer like so (notice the missing THREE namespace):
this.renderer = new CSS3DRenderer();
With help of Mugen87 i have got every thing set up and working !
If you would like to try this on your own here is the code you need :
import React, { Component } from 'react';
import * as THREE from 'three';
import OrbitControls from 'three-orbitcontrols/OrbitControls';
import TWEEN from 'three-tween';
import TrackballControls from 'three-trackballcontrols';
import { CSS3DRenderer } from 'three-css3drenderer';
import { CSS3DObject } from 'three-css3drenderer';
export default class Scene extends Component {
componentWillMount() {
window.addEventListener('resize', this.handleWindowResize)
}
componentDidMount() {
this.setupScene();
this.THREE = THREE;
}
setupScene = () => {
this.THREE = THREE;
this.objects = [];
this.targets = { table: [], sphere: [], helix: [], grid: [] };
this.table = [
"H", "Hydrogen", "1.00794", 1, 1,
//Deleted most of the stuff to save up space
"Uuo", "Ununoctium", "(294)", 18, 7
];
this.width = this.container.clientWidth;
this.height = this.container.clientHeight;
this.renderer = new CSS3DRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.domElement.style.position = 'absolute';
document.getElementById('container').appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 10000);
this.camera.position.z = 3000;
this.scene.add(this.camera);
for (var i = 0; i < this.table.length; i += 5) {
var element = document.createElement('div');
element.className = 'element';
element.style.backgroundColor = 'rgba(0,127,127,' + (Math.random() * 0.5 + 0.25) + ')';
var number = document.createElement('div');
number.className = 'number';
number.textContent = (i / 5) + 1;
element.appendChild(number);
var symbol = document.createElement('div');
symbol.className = 'symbol';
symbol.textContent = this.table[i];
element.appendChild(symbol);
var details = document.createElement('div');
details.className = 'details';
details.innerHTML = this.table[i + 1] + '<br>' + this.table[i + 2];
element.appendChild(details);
var object = new CSS3DObject(element);
object.position.x = Math.random() * 4000 - 2000;
object.position.y = Math.random() * 4000 - 2000;
object.position.z = Math.random() * 4000 - 2000;
this.scene.add(object);
this.objects.push(object);
var object = new THREE.Object3D();
object.position.x = (this.table[i + 3] * 140) - 1260;
object.position.y = - (this.table[i + 4] * 180) + 990;
this.targets.table.push(object);
}
for (var i = 0; i < this.objects.length; i++) {
var object = new THREE.Object3D();
object.position.x = ((i % 5) * 400) - 800;
object.position.y = (- (Math.floor(i / 5) % 5) * 400) + 800;
object.position.z = (Math.floor(i / 25)) * 1000 - 2000;
this.targets.grid.push(object);
}
this.controls = new TrackballControls(this.camera, this.renderer.domElement);
this.controls.rotateSpeed = 0.5;
this.controls.minDistance = 500;
this.controls.maxDistance = 6000;
this.controls.addEventListener('change', this.renderScene);
// var button = document.getElementById('table');
// button.addEventListener('click', event => {
// this.transform(this.targets.table, 2000);
// }, false);
// var button = document.getElementById('sphere');
// button.addEventListener('click', event => {
// transform(targets.sphere, 2000);
// }, false);
// var button = document.getElementById('helix');
// button.addEventListener('click', event => {
// transform(targets.helix, 2000);
// }, false);
// var button = document.getElementById('grid');
// button.addEventListener('click', event => {
// transform(targets.grid, 2000);
// }, false);
this.transform(this.targets.table, 2000);
//
this.start();
}
transform = (targets, duration) => {
TWEEN.removeAll();
for (var i = 0; i < this.objects.length; i++) {
var object = this.objects[i];
var target = targets[i];
new TWEEN.Tween(object.position)
.to({ x: target.position.x, y: target.position.y, z: target.position.z }, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
new TWEEN.Tween(object.rotation)
.to({ x: target.rotation.x, y: target.rotation.y, z: target.rotation.z }, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
}
new TWEEN.Tween(this)
.to({}, duration * 2)
.onUpdate(this.renderScene)
.start();
}
start = () => {
if (!this.frameId) {
this.frameId = requestAnimationFrame(this.animate)
}
}
renderScene = () => {
this.renderer.render(this.scene, this.camera)
}
animate = () => {
this.frameId = requestAnimationFrame(this.animate);
this.renderScene();
TWEEN.update();
this.controls.update();
}
stop = () => {
cancelAnimationFrame(this.frameId);
}
handleWindowResize = () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderScene();
}
componentWillUnmount = () => {
this.stop();
this.destroyContext();
}
destroyContext = () => {
this.container.removeChild(this.renderer.domElement);
this.renderer.forceContextLoss();
this.renderer.context = null;
this.renderer.domElement = null;
this.renderer = null;
}
render() {
const width = '100%';
const height = '100%';
return (
<React.Fragment>
<div
ref={(container) => { this.container = container }}
style={{ width: width, height: height, position: 'absolute', overflow: 'hidden' }}
>
</div>
<div id="container"></div>
</React.Fragment>
)
}
}
I am not too good at data visualization.I want to create a Sunburst where the user can zoom. I have done the zoom with the help of my friend but I am unable to add text from data. Here is my code of zoomable Sunburst.
import React from "react";
import { Group } from "#vx/group";
import { Arc } from "#vx/shape";
import { Partition } from "#vx/hierarchy";
import { arc as d3arc } from "d3-shape";
import {
scaleLinear,
scaleSqrt,
scaleOrdinal,
schemeCategory20c
} from "d3-scale";
import { interpolate } from "d3-interpolate";
import Animate from "react-move/Animate";
import NodeGroup from "react-move/NodeGroup";
const color = scaleOrdinal(schemeCategory20c);
export default class extends React.Component {
state = {
xDomain: [0, 1],
xRange: [0, 2 * Math.PI],
yDomain: [0, 1],
yRange: [0, this.props.width / 2]
};
xScale = scaleLinear();
yScale = scaleSqrt();
arc = d3arc()
.startAngle(d => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x0))))
.endAngle(d => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x1))))
.innerRadius(d => Math.max(0, this.yScale(d.y0)))
.outerRadius(d => Math.max(0, this.yScale(d.y1)));
handleClick = d => {
this.setState({
xDomain: [d.x0, d.x1],
yDomain: [d.y0, 1],
yRange: [d.y0 ? 20 : 0, this.props.width / 2]
});
};
render() {
const {
root,
width,
height,
margin = {
top: 0,
left: 0,
right: 0,
bottom: 0
}
} = this.props;
const { xDomain, xRange, yDomain, yRange } = this.state;
if (width < 10) return null;
const radius = Math.min(width, height) / 2 - 10;
return (
<svg width={width} height={height}>
<Partition top={margin.top} left={margin.left} root={root}>
{({ data }) => {
const nodes = data.descendants();
return (
<Animate
start={() => {
this.xScale.domain(xDomain).range(xRange);
this.yScale.domain(yDomain).range(yRange);
}}
update={() => {
const xd = interpolate(this.xScale.domain(), xDomain);
const yd = interpolate(this.yScale.domain(), yDomain);
const yr = interpolate(this.yScale.range(), yRange);
return {
unused: t => {
this.xScale.domain(xd(t));
this.yScale.domain(yd(t)).range(yr(t));
},
timing: {
duration: 800
}
};
}}
>
{() => (
<Group top={height / 2} left={width / 2}>
{nodes.map((node, i) => (
<path
d={this.arc(node)}
stroke="#fff"
fill={color(
(node.children ? node.data : node.parent.data).name
)}
fillRule="evenodd"
onClick={() => this.handleClick(node)}
text="H"
key={`node-${i}`}
/>
))}
</Group>
)}
</Animate>
);
}}
</Partition>
</svg>
);
}
}
Currently this visualization does not display the name of data from data.js. I want to display that and add a tooltip. How can I achieve that?
class Sunburst extends React.Component {
componentDidMount() {
this.renderSunburst(this.props);
}
componentWillReceiveProps(nextProps) {
if (!isEqual(this.props, nextProps)) {
this.renderSunburst(nextProps);
}
}
arcTweenData(a, i, node, x, arc) {
const oi = d3.interpolate({ x0: (a.x0s ? a.x0s : 0), x1: (a.x1s ? a.x1s : 0) }, a);
function tween(t) {
const b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return arc(b);
}
if (i === 0) {
const xd = d3.interpolate(x.domain(), [node.x0, node.x1]);
return function (t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
formatNameTooltip(d) {
const name = d.data.name;
return `${name}`;
}
labelName(d) {
const name = d.data.name;
return `${name}`;
}
labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * 130;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
update(root, firstBuild, svg, partition, hueDXScale, x, y, radius, arc, node, self) {
if (firstBuild) {
firstBuild = false;
function click(d) {
node = d; // eslint-disable-line
self.props.onSelect && self.props.onSelect(d);
svg.selectAll('path').transition().duration(1000);
}
const tooltipContent = self.props.tooltipContent;
const tooltip = d3.select(`#${self.props.keyId}`)
.append(tooltipContent ? tooltipContent.type : 'div')
.style('position', 'absolute')
.style('z-index', '10')
.style('opacity', '0');
if (tooltipContent) {
Object.keys(tooltipContent.props).forEach((key) => {
tooltip.attr(key, tooltipContent.props[key]);
});
}
svg.selectAll('path')
.data(partition(root).descendants())
.enter()
.append('path')
.style('fill', (d) => {
const current = d;
if (current.depth === 0) {
return '#ffff';
}
if (current.depth === 1) {
return '#3f51b5';
}
if (current.depth > 1) {
return '#f44336';
}
})
.attr('stroke', '#fff') // lines color
.attr('stroke-width', '2') // line width
.on('click', d => click(d, node, svg, self, x, y, radius, arc))
.on('mouseover', function (d) {
if (self.props.tooltip) {
d3.select(this).style('cursor', 'pointer');
tooltip.html(() => { const name = self.formatNameTooltip(d); return name; });
return tooltip.transition().duration(50).style('opacity', 1);
}
return null;
})
.on('mousemove', () => {
if (self.props.tooltip) {
tooltip
.style('top', `${d3.event.pageY - 50}px`)
.style('left', `${self.props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
}
return null;
})
.on('mouseout', function () {
if (self.props.tooltip) {
d3.select(this).style('cursor', 'default');
tooltip.transition().duration(50).style('opacity', 0);
}
return null;
})
} else {
svg.selectAll('path').data(partition(root).descendants());
}
svg.selectAll('path').transition().duration(1000).attrTween('d', (d, i) => self.arcTweenData(d, i, node, x, arc));
}
renderSunburst(props) {
if (props.data) {
const self = this, // eslint-disable-line
gWidth = props.width,
gHeight = props.height,
radius = (Math.min(gWidth, gHeight) / 2) - 10,
svg = d3.select('svg').append('g').attr('transform', `translate(${gWidth / 2},${gHeight / 2})`),
x = d3.scaleLinear().range([0, 2 * Math.PI]),
y = props.scale === 'linear' ? d3.scaleLinear().range([0, radius]) : d3.scaleSqrt().range([0, radius]),
partition = d3.partition(),
arc = d3.arc()
.startAngle(d => Math.max(0, Math.min(2 * Math.PI, x(d.x0))))
.endAngle(d => Math.max(0, Math.min(2 * Math.PI, x(d.x1))))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1))),
hueDXScale = d3.scaleLinear()
.domain([0, 1])
.range([0, 360]),
rootData = d3.hierarchy(props.data);
const firstBuild = true;
const node = rootData;
rootData.sum(d => d.size);
self.update(rootData, firstBuild, svg, partition, hueDXScale, x, y, radius, arc, node, self); // GO!
}
}
render() {
return (
<div id={this.props.keyId} className="text-center">
<svg style={{ width: parseInt(this.props.width, 10) || 480, height: parseInt(this.props.height, 10) || 400 }} id={`${this.props.keyId}-svg`} />
</div>
);
}
}
export default Sunburst;