I need this type of gauge chart
How do I create the above guage chart
I have the speedometer gauge working, but it doesn't meet the needs.
Is there a way within the highchart api to use a triangle for the gauge and not the speedometer?
Unfortunately, it is not supported by default. However, you can achieve it wrapping Highcharts.seriesTypes.gauge.prototype.translate method and changing gauge dial element path. Check demo and code posted below.
Wrapper code:
(function(H) {
H.seriesTypes.gauge.prototype.translate = function() {
var series = this,
yAxis = series.yAxis,
options = series.options,
center = yAxis.center,
pInt = H.pInt,
merge = H.merge,
pick = H.pick,
isNumber = H.isNumber;
series.generatePoints();
series.points.forEach(function(point) {
var dialOptions = merge(options.dial, point.dial),
radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) /
200,
baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) /
100,
rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) /
100,
baseWidth = dialOptions.baseWidth || 3,
arrowHeight = dialOptions.arrowHeight || 10,
arrowWidth = dialOptions.arrowWidth || 5,
topWidth = dialOptions.topWidth || 1,
overshoot = options.overshoot,
rotation = yAxis.startAngleRad +
yAxis.translate(point.y, null, null, null, true);
// Handle the wrap and overshoot options
if (isNumber(overshoot)) {
overshoot = overshoot / 180 * Math.PI;
rotation = Math.max(
yAxis.startAngleRad - overshoot,
Math.min(yAxis.endAngleRad + overshoot, rotation)
);
} else if (options.wrap === false) {
rotation = Math.max(
yAxis.startAngleRad,
Math.min(yAxis.endAngleRad, rotation)
);
}
rotation = rotation * 180 / Math.PI;
point.shapeType = 'path';
point.shapeArgs = {
d: dialOptions.path || [
'M', -rearLength, -baseWidth / 2,
'L',
baseLength, -baseWidth / 2,
baseLength, -arrowWidth,
baseLength + arrowHeight, topWidth / 2,
baseLength, arrowWidth,
baseLength, baseWidth / 2, -rearLength, baseWidth / 2,
'z'
],
translateX: center[0],
translateY: center[1],
rotation: rotation
};
// Positions for data label
point.plotX = center[0];
point.plotY = center[1];
});
}
})(Highcharts);
Demo:
https://jsfiddle.net/BlackLabel/07c3dkn4/1/
Related
When some values are small in QML pie chart, slice labels are messed up:
How can I arrange slice labels like this?
Note that this is available in telerik and /or dev components for c#.
I used of #luffy 's comment and with some modification, I reached to following code and result:
import QtQuick 2.4
Rectangle {
id: root
// public
property string fontFamily: "sans-serif"
property int fontPointSize: 9
property double donutHoleSize: 0.4 //0~1
property string title: 'title'
property variant points: []//[['Zero', 60, 'red'], ['One', 40, 'blue']] // y values don't need to add to 100
width: 500
height: 700
// private
onPointsChanged: myCanvas.requestPaint()
Canvas {
id: myCanvas
anchors.fill: parent
property double factor: Math.min(width, height)
Text { // title
text: title
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: 0.03 * myCanvas.factor
}
onPaint: {
var context = getContext("2d")
var total = 0 // automatically calculated from points.y
var start = -Math.PI / 2 // Start from vertical. 0 is 3 o'clock and positive is clockwise
var radius = 0.25 * myCanvas.factor
var pixelSize = 0.03 * myCanvas.factor // text
context.font = root.fontPointSize + 'pt ' + root.fontFamily
var i = 0;
for(i = 0; i < points.length; i++) total += points[i][1] // total
context.clearRect(0, 0, width, height) // new points data
//--------------------------------------------------------
var end = 0;
var center = 0;
var angle = 0;
var midSlice = 0;
var point = 0;
var topRightCnt = 0
var bottomRightCnt = 0
var topLeftCnt = 0
var bottomLeftCnt = 0
var itemsPos = []
center = Qt.vector2d(width / 2, height / 2) // center
for(i = 0; i < points.length; i++) {
end = start + 2 * Math.PI * points[i][1] / total // radians
angle = (start + end) / 2 // of line
midSlice = Qt.vector2d(Math.cos((end + start) / 2), Math.sin((end + start) / 2)).times(radius) // point on edge/middle of slice
point = midSlice.times(1 + 1.4 * (1 - Math.abs(Math.cos(angle)))).plus(center) // elbow of line
if(point.x<center.x && point.y<=center.y) {
topLeftCnt++;
itemsPos[i] = "tl"
}
else if(point.x<center.x && point.y>center.y) {
bottomLeftCnt++;
itemsPos[i] = "bl"
}
else if(point.x>=center.x && point.y<=center.y) {
topRightCnt++;
itemsPos[i] = "tr"
}
else {
bottomRightCnt++;
itemsPos[i] = "br"
}
start = end // radians
}
//--------------------------------------------------------
end = 0;
angle = 0;
midSlice = 0;
point = 0;
var itemPosCounterTR = 0;
var itemPosCounterTL = 0;
var itemPosCounterBR = 0;
var itemPosCounterBL = 0;
for(i = 0; i < points.length; i++) {
end = start + 2 * Math.PI * points[i][1] / total // radians
// pie
context.fillStyle = points[i][2]
context.beginPath()
midSlice = Qt.vector2d(Math.cos((end + start) / 2), Math.sin((end + start) / 2)).times(radius) // point on edge/middle of slice
context.arc(center.x, center.y, radius, start, end) // x, y, radius, startingAngle (radians), endingAngle (radians)
context.lineTo(center.x, center.y) // center
context.fill()
// text
context.fillStyle = points[i][2]
angle = (start + end) / 2 // of line
point = midSlice.times(1 + 1.4 * (1 - Math.abs(Math.cos(angle)))).plus(center) // elbow of line
//---------------------------------------------
var textX = 0;
var textY = 0;
var dis = 0;
var percent = points[i][1] / total * 100
var text = points[i][0] + ': ' + (percent < 1? '< 1': Math.round(percent)) + '%' // display '< 1%' if < 1
var textWidth = context.measureText(text).width
var textHeight = 15
var diameter = radius * 2
var topCircle = center.y - radius
var leftCircle = center.x - radius
if(itemsPos[i] === "tr") {
textX = leftCircle + 1.15 * diameter
dis = Math.floor((1.15*radius) / topRightCnt) //Math.floor((height/2) / topRightCnt)
dis = (dis < 20 ? 20 : dis)
textY = topCircle -(0.15*diameter) + (itemPosCounterTR*dis) + (textHeight/2)
itemPosCounterTR++
}
else if(itemsPos[i] === "br") {
textX = leftCircle + 1.15 * diameter
dis = Math.floor((1.15*radius) / bottomRightCnt)
dis = (dis < 20 ? 20 : dis)
textY = topCircle+(1.15*diameter) - ((bottomRightCnt-itemPosCounterBR-1)*dis) - (textHeight/2)
itemPosCounterBR++
}
else if(itemsPos[i] === "tl") {
textX = leftCircle - (0.15 * diameter) - textWidth
dis = Math.floor((1.15*radius) / topLeftCnt)
dis = (dis < 20 ? 20 : dis)
textY = topCircle-(0.15*diameter) + ((topLeftCnt-itemPosCounterTL-1)*dis) + (textHeight/2)
itemPosCounterTL++;
}
else {
textX = leftCircle - (0.15 * diameter) - textWidth //-0.2 * width - textWidth
dis = Math.floor((1.15*radius) / bottomLeftCnt)
dis = (dis < 20 ? 20 : dis)
textY = topCircle+(1.15*diameter) - (itemPosCounterBL*dis) - (textHeight/2)
itemPosCounterBL++
}
//---------------------------------------------
context.fillText(text, textX, textY)
// line
context.lineWidth = 1
context.strokeStyle = points[i][2]
context.beginPath()
context.moveTo(center.x + midSlice.x, center.y + midSlice.y) // center
var endLineX = (point.x < center.x ? (textWidth + 0.5 * pixelSize) : (-0.5 * pixelSize)) + textX;
context.lineTo(endLineX, textY+3)
context.lineTo(endLineX + (point.x < center.x? -1: 1) * ((0.5 * pixelSize)+textWidth), textY+3) // horizontal
context.stroke()
start = end // radians
}
if(root.donutHoleSize > 0) {
root.donutHoleSize = Math.min(0.99, root.donutHoleSize);
var holeRadius = root.donutHoleSize * radius;
context.fillStyle = root.color
context.beginPath()
context.arc(center.x, center.y, holeRadius, 0, 2*Math.PI) // x, y, radius, startingAngle (radians), endingAngle (radians)
//context.lineTo(center.x, center.y) // center
context.fill()
}
}
}
}
And it's result:
Thanks #luffy.
I'm currently using the matrix_gesture_detector package to scale, transform and rotate a Transform widget.
Everything works fine but to improve UX I would like to snap the widget at 90, 180, 270 or 360 degrees once the user rotates the widget close enough to said angles.
Edit: To clarify I would like the user to be able to freely rotate the widget, but snap into the nearest 90 degree rotation within whichever quadrant it is, once it gets close enough.
Hence, the solution should detect that "closeness" and then act accordingly. Please visit this link to see a GIF which shows the desired effect
How can I achieve this?
Below is the code snippet
Widget transformContainer() {
Matrix4 matrix;
GlobalKey matrixDetectorKey = GlobalKey();
return MatrixGestureDetector(
key: matrixDetectorKey,
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
matrix = MatrixGestureDetector.compose(matrix, tm, sm, rm);
});
},
child: Transform(
transform: matrix,
child: Container(
padding: EdgeInsets.all(24.0),
width: 100.0,
height: 200.0,
color: Colors.teal,
),
),
);}
For this to work, you have to check if the rotation of the matrix is closing in on a snapping point and rotate the Z-Axis to that point. I was able to write this code which works for this scenario. You may have to tune the threshold to adjust the "snappiness" to your taste.
import vector_math and use the following code:
import 'package:vector_math/vector_math_64.dart' as vec;
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
Matrix4 ogRm = rm.clone();
double radian = MatrixGestureDetector.decomposeToValues(m).rotation;
double degrees = vec.degrees(radian);
double delta_0 = vec.absoluteError(degrees, 0);
double delta_90 = vec.absoluteError(degrees, 90);
double delta_180 = vec.absoluteError(degrees, 180);
double delta_270 = vec.absoluteError(degrees, -90);
double threshold = 4;
if (delta_0 <= threshold) {
rm.rotateZ(vec.radians(0) - radian);
} else if (delta_90 <= threshold) {
rm.rotateZ(vec.radians(90) - radian);
} else if (delta_180 <= threshold) {
rm.rotateZ(vec.radians(180) - radian);
} else if (delta_270 <= threshold) {
rm.rotateZ(vec.radians(270) - radian);
}
// update gesture matrix
if (ogRm != rm) m = m * rm;
setState(() {
//transform your widget using this matrix
matrix = m;
});
}
Note that 360 is the same as 0, so there's no need to check for it.
MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
//270 is the angle
m.rotate(m.getTranslation(), 270);
matrix4 = m;
});
},
child: Transform(
transform: matrix4,
child: Container(
padding: EdgeInsets.all(24.0),
width: 100,
height: 200,
color: Colors.teal,
),
),
),
You will have the rotate function inside the library. this code will work for snapping to 0 degrees. Basically what I am doing is if the difference in rotation and x-axis is less than 0.2 radians, I snap to 0 degrees while keeping the rotation in a separate variable. When the user moves beyond this, I add this value again and keep rotating normally.
Matrix4 _rotate(double angle, Offset focalPoint) {
double toBeRotated = 0;
var array = matrix.applyToVector3Array([0, 0, 0, 1, 0, 0]);
Offset delta = Offset(array[3] - array[0], array[4] - array[1]);
double rotation = delta.direction;
deltaAngle = deltaAngle + angle;
if ((rotation + deltaAngle).abs() > 0.2) {
toBeRotated = deltaAngle;
deltaAngle = 0;
} else if (rotation != 0 && (rotation + deltaAngle).abs() <= 0.2) {
toBeRotated = -rotation;
deltaAngle = deltaAngle + rotation;
} else {
toBeRotated = 0;
}
var c = cos(toBeRotated);
var s = sin(toBeRotated);
var dx = (1 - c) * focalPoint.dx + s * focalPoint.dy;
var dy = (1 - c) * focalPoint.dy - s * focalPoint.dx;
// ..[0] = c # x scale
// ..[1] = s # y skew
// ..[4] = -s # x skew
// ..[5] = c # y scale
// ..[10] = 1 # diagonal "one"
// ..[12] = dx # x translation
// ..[13] = dy # y translation
// ..[15] = 1 # diagonal "one"
return Matrix4(c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}
Having made some progress in the geometry side of things I'm moving on to putting together an entire scene. That scene has a couple dozen objects, each defined by a bounding cube whose corners are specified by two SCNVector3s (originally two sets of x,y,z).
Here's an example of what I have so far - it's an 11-element log-periodic antenna, like the old school TV antennas from the 70s. Each of the grey lines is an "element", typically made of aluminum rod. I used SCNCylinders from +ve to -ve Y and the entire thing is less than 100 lines (SK is pretty amazing).
The problem is what happens if the elements are not symmetrical across X and thus the SCNCylinder has to be rotated. I found this example, but I can't understand the specifics... it appears to take advantage of the fact that a sphere is symmetric so angles kind of "go away".
Does anyone have a general function that will take two 3D points and return the SCNVector3 suitable for setting the node's eulerAngle, or a similar solution?
Both solutions mentioned above work very well and I can contribute third solution to this question.
//extension code starts
func normalizeVector(_ iv: SCNVector3) -> SCNVector3 {
let length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z)
if length == 0 {
return SCNVector3(0.0, 0.0, 0.0)
}
return SCNVector3( iv.x / length, iv.y / length, iv.z / length)
}
extension SCNNode {
func buildLineInTwoPointsWithRotation(from startPoint: SCNVector3,
to endPoint: SCNVector3,
radius: CGFloat,
color: UIColor) -> SCNNode {
let w = SCNVector3(x: endPoint.x-startPoint.x,
y: endPoint.y-startPoint.y,
z: endPoint.z-startPoint.z)
let l = CGFloat(sqrt(w.x * w.x + w.y * w.y + w.z * w.z))
if l == 0.0 {
// two points together.
let sphere = SCNSphere(radius: radius)
sphere.firstMaterial?.diffuse.contents = color
self.geometry = sphere
self.position = startPoint
return self
}
let cyl = SCNCylinder(radius: radius, height: l)
cyl.firstMaterial?.diffuse.contents = color
self.geometry = cyl
//original vector of cylinder above 0,0,0
let ov = SCNVector3(0, l/2.0,0)
//target vector, in new coordination
let nv = SCNVector3((endPoint.x - startPoint.x)/2.0, (endPoint.y - startPoint.y)/2.0,
(endPoint.z-startPoint.z)/2.0)
// axis between two vector
let av = SCNVector3( (ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0)
//normalized axis vector
let av_normalized = normalizeVector(av)
let q0 = Float(0.0) //cos(angel/2), angle is always 180 or M_PI
let q1 = Float(av_normalized.x) // x' * sin(angle/2)
let q2 = Float(av_normalized.y) // y' * sin(angle/2)
let q3 = Float(av_normalized.z) // z' * sin(angle/2)
let r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3
let r_m12 = 2 * q1 * q2 + 2 * q0 * q3
let r_m13 = 2 * q1 * q3 - 2 * q0 * q2
let r_m21 = 2 * q1 * q2 - 2 * q0 * q3
let r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3
let r_m23 = 2 * q2 * q3 + 2 * q0 * q1
let r_m31 = 2 * q1 * q3 + 2 * q0 * q2
let r_m32 = 2 * q2 * q3 - 2 * q0 * q1
let r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3
self.transform.m11 = r_m11
self.transform.m12 = r_m12
self.transform.m13 = r_m13
self.transform.m14 = 0.0
self.transform.m21 = r_m21
self.transform.m22 = r_m22
self.transform.m23 = r_m23
self.transform.m24 = 0.0
self.transform.m31 = r_m31
self.transform.m32 = r_m32
self.transform.m33 = r_m33
self.transform.m34 = 0.0
self.transform.m41 = (startPoint.x + endPoint.x) / 2.0
self.transform.m42 = (startPoint.y + endPoint.y) / 2.0
self.transform.m43 = (startPoint.z + endPoint.z) / 2.0
self.transform.m44 = 1.0
return self
}
}
//extension ended.
//in your code, you can like this.
let twoPointsNode1 = SCNNode()
scene.rootNode.addChildNode(twoPointsNode1.buildLineInTwoPointsWithRotation(
from: SCNVector3(1,-1,3), to: SCNVector3( 7,11,7), radius: 0.2, color: .cyan))
//end
you can reference http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
BTW, you will get same result when you use a cylinder to make a line between two points from above 3 methods. But indeed, they will have different normal lines. In another words, if you use box between two points, sides of box, except top and bottom, will face different direction from above 3 methods.
let me know pls if you need further explanation.
EDIT: For under or equal to IOS 11
I've good news for you ! You can link two points and put a SCNNode on this Vector !
Take this and enjoy drawing line between two point !
class CylinderLine: SCNNode
{
init( parent: SCNNode,//Needed to add destination point of your line
v1: SCNVector3,//source
v2: SCNVector3,//destination
radius: CGFloat,//somes option for the cylinder
radSegmentCount: Int, //other option
color: UIColor )// color of your node object
{
super.init()
//Calcul the height of our line
let height = v1.distance(v2)
//set position to v1 coordonate
position = v1
//Create the second node to draw direction vector
let nodeV2 = SCNNode()
//define his position
nodeV2.position = v2
//add it to parent
parent.addChildNode(nodeV2)
//Align Z axis
let zAlign = SCNNode()
zAlign.eulerAngles.x = Float(M_PI_2)
//create our cylinder
let cyl = SCNCylinder(radius: radius, height: CGFloat(height))
cyl.radialSegmentCount = radSegmentCount
cyl.firstMaterial?.diffuse.contents = color
//Create node with cylinder
let nodeCyl = SCNNode(geometry: cyl )
nodeCyl.position.y = -height/2
zAlign.addChildNode(nodeCyl)
//Add it to child
addChildNode(zAlign)
//set contrainte direction to our vector
constraints = [SCNLookAtConstraint(target: nodeV2)]
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
private extension SCNVector3{
func distance(receiver:SCNVector3) -> Float{
let xd = receiver.x - self.x
let yd = receiver.y - self.y
let zd = receiver.z - self.z
let distance = Float(sqrt(xd * xd + yd * yd + zd * zd))
if (distance < 0){
return (distance * -1)
} else {
return (distance)
}
}
}
#maury-markowitz's answer worked for me, here is the latest (Swift4) version of it.
To anyone working with SCNVector3 in Swift I can only recommend to add the +-*/ operator overloads somewhere in your code (e.g. from here).
extension SCNNode {
static func lineNode(from: SCNVector3, to: SCNVector3, radius: CGFloat = 0.25) -> SCNNode {
let vector = to - from
let height = vector.length()
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.radialSegmentCount = 4
let node = SCNNode(geometry: cylinder)
node.position = (to + from) / 2
node.eulerAngles = SCNVector3.lineEulerAngles(vector: vector)
return node
}
}
extension SCNVector3 {
static func lineEulerAngles(vector: SCNVector3) -> SCNVector3 {
let height = vector.length()
let lxz = sqrtf(vector.x * vector.x + vector.z * vector.z)
let pitchB = vector.y < 0 ? Float.pi - asinf(lxz/height) : asinf(lxz/height)
let pitch = vector.z == 0 ? pitchB : sign(vector.z) * pitchB
var yaw: Float = 0
if vector.x != 0 || vector.z != 0 {
let inner = vector.x / (height * sinf(pitch))
if inner > 1 || inner < -1 {
yaw = Float.pi / 2
} else {
yaw = asinf(inner)
}
}
return SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
}
}
For the sake of another method, I achieved this through trigonometry. This made the code very minimal. Here is the end result:
In my case the nodes are always placed on a fixed plane that slices the Y-Axis.
// Create Cylinder Geometry
let line = SCNCylinder(radius: 0.002, height: node1.distance(to: node2))
// Create Material
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
material.lightingModel = .phong
line.materials = [material]
// Create Cylinder(line) Node
let newLine = SCNNode()
newLine.geometry = line
newLine.position = posBetween(first: node1, second: node2)
// This is the change in x,y and z between node1 and node2
let dirVector = SCNVector3Make(node2.x - node1.x, node2.y - node1.y, node2.z - node1.z)
// Get Y rotation in radians
let yAngle = atan(dirVector.x / dirVector.z)
// Rotate cylinder node about X axis so cylinder is laying down
currentLine.eulerAngles.x = .pi / 2
// Rotate cylinder node about Y axis so cylinder is pointing to each node
currentLine.eulerAngles.y = yAngle
This is the function to get the position between two nodes, place it within your class:
func posBetween(first: SCNVector3, second: SCNVector3) -> SCNVector3 {
return SCNVector3Make((first.x + second.x) / 2, (first.y + second.y) / 2, (first.z + second.z) / 2)
}
This is the extension to get the distance between nodes for the cylinder height, place it somewhere outside of your class:
extension SCNVector3 {
func distance(to destination: SCNVector3) -> CGFloat {
let dx = destination.x - x
let dy = destination.y - y
let dz = destination.z - z
return CGFloat(sqrt(dx*dx + dy*dy + dz*dz))
}
}
If you don't have one fixed axis like myself then you could do the extra trig to use this method.
Here's a solution using simd and quaternions for the rotation. I based the extension off of the answer by #Bersaelor.
I used this derivation (https://stackoverflow.com/a/1171995/6693924) to create the quaternion from two vectors. Hope this helps.
extension SCNNode {
static func lineNode(from: simd_float3, to: simd_float3, radius : CGFloat = 0.25) -> SCNNode
{
let vector = to - from
let height = simd_length(vector)
//cylinder
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.firstMaterial?.diffuse.contents = UIColor.white
//line node
let lineNode = SCNNode(geometry: cylinder)
//adjust line position
let line_axis = simd_float3(0, height/2, 0)
lineNode.simdPosition = from + line_axis
let vector_cross = simd_cross(line_axis, vector)
let qw = simd_length(line_axis) * simd_length(vector) + simd_dot(line_axis, vector)
let q = simd_quatf(ix: vector_cross.x, iy: vector_cross.y, iz: vector_cross.z, r: qw).normalized
lineNode.simdRotate(by: q, aroundTarget: from)
return lineNode
}
}
Sprout's (wow, the autocorrect will not allow me to actually type in his name!) post is indeed a solution, but I have implemented a very different solution in my code.
What I do is calculate the length of the line and the two endpoints, based on the X, Y and Z locations from the two ends:
let w = SCNVector3(x: CGFloat(x2m-x1m), y: CGFloat(y2m-y1m), z: CGFloat(z2m-z1m))
let l = w.length()
The length is simply pythag. Now I make an SCNNode that will hold the SCNCylinder, and position it in the middle of the line:
let node = SCNNode(geometry: cyl)
node.position = SCNVector3(x: CGFloat((x1m+x2m)/2.0), y: CGFloat((y1m+y2m)/2.0), z: CGFloat((z1m+z2m)/2.0))
And now the nasty part, where we calculate the Euler angles and rotate the node:
let lxz = (Double(w.x)**2 + Double(w.z)**2)**0.5
var pitch, pitchB: Double
if w.y < 0 {
pitchB = M_PI - asin(Double(lxz)/Double(l))
} else {
pitchB = asin(Double(lxz)/Double(l))
}
if w.z == 0 {
pitch = pitchB
} else {
pitch = sign(Double(w.z)) * pitchB
}
var yaw: Double
if w.x == 0 && w.z == 0 {
yaw = 0
} else {
let inner = Double(w.x) / (Double(l) * sin (pitch))
if inner > 1 {
yaw = M_PI_2
} else if inner < -1 {
yaw = M_PI_2
} else {
yaw = asin(inner)
}
}
node.eulerAngles = SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
I suspect there is a much simpler way to do this using one of the other rotation inputs, but this works and working is a feature!
Draw the line between two nodes:
func generateLine( startPoint: SCNVector3, endPoint: SCNVector3) -> SCNGeometry {
let vertices: [SCNVector3] = [startPoint, endPoint]
let data = NSData(bytes: vertices, length: MemoryLayout<SCNVector3>.size * vertices.count) as Data
let vertexSource = SCNGeometrySource(data: data,
semantic: .vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.stride)
let indices: [Int32] = [ 0, 1]
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count) as Data
let element = SCNGeometryElement(data: indexData,
primitiveType: .line,
primitiveCount: indices.count/2,
bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [vertexSource], elements: [element])
}
How To Use
let line = generateLine(startPoint: SCNVector3Make(1, 1, 1), endPoint: SCNVector3Make(8, 8, 8))
let lineNode = SCNNode(geometry: line)
lineNode.position = SCNVector3Make(15, 15, 10)
scene.rootNode.addChildNode(lineNode)
The thickness of the line requires implementing the SCNSceneRendererDelegate, in particular:
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval){
glLineWidth(10)
}
Objective-C version of Winchill's answer:
-(void)lineNodeFrom:(SCNVector3)to to:(SCNVector3)from radius:(float)radius{
SCNVector3 w = SCNVector3Make(to.x - from.x, to.y - from.y, from.z - to.z);
float l = sqrtf(powf(w.x, 2) + powf(w.y, 2) + powf(w.z, 2.0f));
SCNCylinder * cylinder = [SCNCylinder cylinderWithRadius:radius height:l];
SCNMaterial * material = [SCNMaterial material];
material.diffuse.contents = [[UIColor darkGrayColor] colorWithAlphaComponent:0.75f];
cylinder.materials = #[material];
[self setGeometry:cylinder];
//original vector of cylinder above 0,0,0
SCNVector3 ov = SCNVector3Make(0, l/2.0,0);
//target vector, in new coordination
SCNVector3 nv = SCNVector3Make((from.x - to.x)/2.0, (from.y - to.y)/2.0, (from.z-to.z)/2.0);
// axis between two vector
SCNVector3 av = SCNVector3Make((ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0);
//normalized axis vector
SCNVector3 av_normalized = [self normaliseVector:av];
float q0 = 0.0f; //cos(angel/2), angle is always 180 or M_PI
float q1 = av_normalized.x; // x' * sin(angle/2)
float q2 = av_normalized.y; // y' * sin(angle/2)
float q3 = av_normalized.z; // z' * sin(angle/2)
float r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3;
float r_m12 = 2 * q1 * q2 + 2 * q0 * q3;
float r_m13 = 2 * q1 * q3 - 2 * q0 * q2;
float r_m21 = 2 * q1 * q2 - 2 * q0 * q3;
float r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3;
float r_m23 = 2 * q2 * q3 + 2 * q0 * q1;
float r_m31 = 2 * q1 * q3 + 2 * q0 * q2;
float r_m32 = 2 * q2 * q3 - 2 * q0 * q1;
float r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3;
SCNMatrix4 transform;
transform.m11 = r_m11;
transform.m12 = r_m12;
transform.m13 = r_m13;
transform.m14 = 0.0;
transform.m21 = r_m21;
transform.m22 = r_m22;
transform.m23 = r_m23;
transform.m24 = 0.0;
transform.m31 = r_m31;
transform.m32 = r_m32;
transform.m33 = r_m33;
transform.m34 = 0.0;
transform.m41 = (to.x + from.x) / 2.0;
transform.m42 = (to.y + from.y) / 2.0;
transform.m43 = (to.z + from.z) / 2.0;
transform.m44 = 1.0;
self.transform = transform;
}
-(SCNVector3)normaliseVector:(SCNVector3)iv{
float length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z);
if (length == 0){
return SCNVector3Make(0.0, 0.0, 0.0);
}
return SCNVector3Make(iv.x / length, iv.y / length, iv.z / length);
}
I would like to move the y-axis labels to the left side of heatmap.2. (This is similar, but not the same, as the question regarding moving the axis on heatmap)
While I am able to move the axis by editing line 290 of the heatmap.2 function, it the values then overwrite the actual heatmap.
if (is.null(srtRow) && is.null(colRow)) {
axis(4, iy, labels = labRow, las = 2, line = -0.5 + offsetRow,
tick = 0, cex.axis = cexRow, hadj = adjRow[1], padj = adjRow[2])
}
else {
if (is.null(srtRow) || is.numeric(srtRow)) {
xpd.orig <- par("xpd")
par(xpd = NA)
ypos <- axis(4, iy, labels = rep("", nr), las = 2, #change
line = -0.5, tick = 0)
text(x = par("usr")[2] + (1 + offsetRow) * strwidth("M"),
y = ypos, labels = labRow, adj = adjRow, cex = cexRow,
srt = srtRow, col = colRow)
par(xpd = xpd.orig)
}
I tried moving the location of the heatmap.2 by mucking about with the lwid and lhei options, but the overwrite problem persisted.
Thank you
Eric
I was studying Ray Tracing on http://www.devmaster.net/articles/raytracing_series/part1.php when I came across this piece of code:
void Engine::InitRender()
{
// set first line to draw to
m_CurrLine = 20;
// set pixel buffer address of first pixel
m_PPos = 20 * m_Width;
// screen plane in world space coordinates
m_WX1 = -4, m_WX2 = 4, m_WY1 = m_SY = 3, m_WY2 = -3;
// calculate deltas for interpolation
m_DX = (m_WX2 - m_WX1) / m_Width;
m_DY = (m_WY2 - m_WY1) / m_Height;
m_SY += 20 * m_DY;
// allocate space to store pointers to primitives for previous line
m_LastRow = new Primitive*[m_Width];
memset( m_LastRow, 0, m_Width * 4 );
}
I'm quite confused on how the author map screen coordinates to world coordinates...
Can anyone please tell me how the author derived these lines?
Or tell me how one would map screen coordinates to world coordinates?
// screen plane in world space coordinates
m_WX1 = -4, m_WX2 = 4, m_WY1 = m_SY = 3, m_WY2 = -3;
Thank you in advance!
EDIT: Here is relevant code from raytracer.cpp:
// render scene
vector3 o( 0, 0, -5 );
// initialize timer
int msecs = GetTickCount();
// reset last found primitive pointer
Primitive* lastprim = 0;
// render remaining lines
for(int y = m_CurrLine; y < (m_Height - 20); y++)
{
m_SX = m_WX1;
// render pixels for current line
for ( int x = 0; x < m_Width; x++ )
{
// fire primary ray
Color acc( 0, 0, 0 );
vector3 dir = vector3( m_SX, m_SY, 0 ) - o;
NORMALIZE( dir );
Ray r( o, dir );
float dist;
Primitive* prim = Raytrace( r, acc, 1, 1.0f, dist );
int red = (int)(acc.r * 256);
int green = (int)(acc.g * 256);
int blue = (int)(acc.b * 256);
if (red > 255) red = 255;
if (green > 255) green = 255;
if (blue > 255) blue = 255;
m_Dest[m_PPos++] = (red << 16) + (green << 8) + blue;
m_SX += m_DX;
}
m_SY += m_DY;
// see if we've been working to long already
if ((GetTickCount() - msecs) > 100)
{
// return control to windows so the screen gets updated
m_CurrLine = y + 1;
return false;
}
}
return true;
Therefore the camera is at (0,0,-5) and the screen onto which the world is being projected has top-left corner (-4,3,0) and bottom-right corner (4,-3,0).