So I have Custom view an I'm trying to draw line chart there. First I draw xGrid, yGrid and middleline. Here are my functions to do that:
var width: CGFloat = 400
var height: CGFloat = 200
var xLines: CGFloat = 20
var yLines: CGFloat = 10
let color = NSColor.blackColor()
func drawMiddleLine() {
let middleHeight = height / 2
var context = NSGraphicsContext.currentContext()?.CGContext
CGContextSetStrokeColorWithColor(context, color.CGColor)
CGContextSetLineWidth(context, 2)
CGContextMoveToPoint(context, 0, middleHeight)
CGContextAddLineToPoint(context, width, middleHeight)
CGContextStrokePath(context)
}
func drawYGrid() {
let space = height / yLines
var context = NSGraphicsContext.currentContext()?.CGContext
CGContextSetStrokeColorWithColor(context, color.CGColor)
for index in 0...Int(yLines) {
CGContextMoveToPoint(context, width, (CGFloat(index) * space))
CGContextAddLineToPoint(context, 0, (CGFloat(index) * space))
}
CGContextStrokePath(context)
}
func drawXGrid() {
let space = width / xLines
var context = NSGraphicsContext.currentContext()?.CGContext
CGContextSetStrokeColorWithColor(context, color.CGColor)
for index in 0...Int(xLines) {
CGContextMoveToPoint(context, (CGFloat(index) * space), self.bounds.height)
CGContextAddLineToPoint(context, CGFloat(index) * space, 0)
}
CGContextStrokePath(context)
}
Now I have basic grid which width is two times height. Now I want to scale my y-axis, so I'm going to scale my data (In this example, I only take max positive number):
func getMaxValue(data: Array<CGFloat>) ->CGFloat {
let max = maxElement(data)
return max
}
And now I scale my y-Axis:
func scaleYAxis(data: Array<CGFloat>) ->Array<CGFloat> {
let maxValue = getMaxValue(data)
var factor = height / maxValue / 4
var scaledY = data.map({datum -> CGFloat in
var newValue = datum * factor
return newValue
})
return scaledY
}
But when I'm drawing my line with too many datapoints, my drawing will get messed up, because there is too many datapoints. It's ok when there is about 50 datapoints.
For example, I want to get something like this in results: JavaScript linechart with many many datapoints
Any ideas how can I manage to get that in Swift?
And my drawing method is something like that:
func drawLine(data: Array<CGFloat>) {
var path = CGPathCreateMutable()
var middleHeight = height / 2
CGPathMoveToPoint(path, nil, 0, middleHeight)
var scaledY = sclaleYAxis(data)
for index in 0..<data.count {
var xSpot = data[index]
var ySpot = middleHeight + scaledY[index]
CGPathAddLineToPoint(path, nil, xSpot, ySpot)
}
var layer = CAShapeLayer()
layer.frame = self.bounds
layer.path = path
layer.strokeColor = NSColor.greenColor().CGColor
layer.fillColor = nil
layer.lineWidth = 3
self.layer?.addSublayer(layer)
// I Have lineLayerStore and I delete all lines when it is needed
lineLayerStore.append(layer)
}
I cannot understand what the exact problem is without seeing a screenshot but visibility of the datapoints depends on the number of pixels on the screen.
You cannot display more datapoints than the number of pixels, without letting some datapoints share the same pixel column or downsampling the data.
You have 3 options:
Make X axis scrollable and set scrollview's contentSize width to the
number data points.
Keep X axis width fixed, downsample the data to reduce the number of
datapoints to be displayed, then draw the chart regarding the
downsampled data.
Do nothing, try to draw all the datapoints within the fixed width,
if your calculations are correct some datapoints will overlap and
share the same pixel column in the chart.
Explanation Edit for 3rd option:
For displaying 1000 datapoints on a 200 pixels wide X axis, there will be 5 datapoints per pixel. So;
Datapoints 0, 1, 2, 3 and 4 will be drawn on 1st pixel column.
Datapoints 5, 6, 7, 8 and 9 will be drawn on 2nd pixel column.
Datapoints 10, 11, 12, 13 and 14 will be drawn on 3rd pixel column.
And so on.
For your example, there are 400 pixels and 1000 datapoints, it means 2.5 datapoint per pixel. So;
Datapoints 0, 1, 2 will be drawn on 1st pixel column.
Datapoints 3, 4 will be drawn on 2nd pixel column.
Datapoints 5, 6, 7 will be drawn on 3rd pixel column.
Datapoints 8, 9 will be drawn on 4th pixel column.
And going on like that.
Related
I recently stretched a gradient across the canvas using the ImageData data array; ie the ctx.getImageData() and ctx.putImageData() methods, and thought to myself, "this could be a really efficient way to animate a canvas full of moving objects". So I wrapped it into my main function along with the requestAnimationFrame(callback) statement, but that's when things got weird. The best I can do at describing is to say it's like the left most column of pixels on the canvas is duplicated in the right most column of pixels, and based on what coordinates you specify for the get and put ctx methods, this can have bizarre consequences.
I started out with the get and put methods targeting the canvas at 0, 0 like so:
imageData = ctx.getImageData( 0, 0, cvs.width, cvs.height );
// here I set the pixel colors according to their location
// on the canvas using nested for loops to target the
// the correct imageData array indexes.
ctx.putImageData( imageData, 0, 0 );
But I immediately noticed the right side of the canvas was wrong. Like the gradient has started over, and the last pixel just didn't get touched for some reason:
So scaled back my draw region changed the put ImageData coordinates to get some space between the drawn image and the edge of the canvas, and I changed the get coordinated to eliminate that line on the right edge of the canvas:
imageData = ctx.getImageData( 1, 1, cvs.width, cvs.height );
for ( var x = 0; x < cvs.width - 92; x++ ) {
for ( var y = 0; y < cvs.height - 92; y++ ) {
// array[ x + y * width ] = value / x; // or similar
}
}
ctx.putImageData( imageData, 2, 2 );
Pretty! But wrong... So I reproduced it in codepen. Can someone help me understand and overcome this behavior?
Note: The codepen has the scaled back draw area. If you change the get coordinates to 0 you'll see it basically behaves the same way as the first example but with white-space in between the expected square and the unexpected line. That said, I left the get at 1 and the put at zero for the most interesting behavior yet.
I've changed your code a little. In your double loop I am declaring a variable var i = (x + y*cvs.width)*4; This is only reducing the verbosity of your code so that I can see it better. The i variable represents the index of your pixel in the imageData.data array. Since you are doing
imageData.data[i - 4 ] ...
imageData.data[i - 3 ] ...
imageData.data[i - 2 ] ...
imageData.data[i - 1 ] ...
you are going one pixel backwards and the first pixel from every row appears as the last pixel of the previous row. So I've changed it from var i = (x + y*cvs.width)*4; to var i = 4 + (x + y*cvs.width)*4;.
When you are animating it, since the imageData is inside the test() function, you are recalculating the values of the imageData.data array in base of the last frame. So in the second frame you have that 1px line from the first frame copied again and moved 1px upward and 1px to the left.
I hope this is what you were asking.
var ctx, cvs, imageData;
cvs = document.getElementById('canv');
ctx = cvs.getContext('2d');
function test() {
// imageData = ctx.getImageData( 0, 0, cvs.width, cvs.height );
// produces a line on the right side of the screen
imageData = ctx.getImageData( 1, 1, cvs.width, cvs.height );
// bizzar reverse cascading
for (var x=0;x<cvs.width-92;x++) {
for (var y=0;y<cvs.height-92;y++) {
var i = 4+(x + y*cvs.width)*4;
imageData.data[i - 4 ] = Math.floor((255-y)-Math.floor(x/55)*55);
imageData.data[i - 3 ] = Math.floor(255/(cvs.height-92)*y);
imageData.data[i - 2 ] = Math.floor(255/(cvs.width-92)*x);
imageData.data[i - 1 ] = 255;
}
}
ctx.putImageData( imageData, 0, 0 );
requestAnimationFrame( test );
}
test();
canvas {
box-shadow: 0 0 2.5px 0 black;
}
<canvas id="canv" height="256" width="256"></canvas>
Here is problem in which I add zoombie sprite to the scene every one second. When I add another sub animated zoombie to the zoombie node, sometimes it loads animated texture, and other times appear red large X.
func addMonster() {
let zoombieSprite = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(40, 60))
// Determine where to spawn the monster along the Y axis
let actualY = randRange(lower: zoombieSprite.size.height, upper: size.height - zoombieSprite.size.height)
// Position the monster slightly off-screen along the right edge,
// and along a random position along the Y axis as calculated above
zoombieSprite.position = CGPoint(x: size.width + zoombieSprite.size.width/2, y: actualY)
zoombieSprite.physicsBody = SKPhysicsBody(rectangleOfSize: zoombieSprite.size) // 1
zoombieSprite.physicsBody?.dynamic = true // 2
zoombieSprite.physicsBody?.categoryBitMask = PhysicsCategory.Monster // 3
zoombieSprite.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile // 4
zoombieSprite.physicsBody?.collisionBitMask = PhysicsCategory.None // 5
addChild(zoombieSprite)
//zoombieSprite.addChild(createAnimatedZoombie())
let zoombieAnimation = SKAction.runBlock({
zoombieSprite.addChild(self.createAnimatedZoombie())
})
// Determine speed of the monster
let actualDuration = randRange(lower: 6.0, upper: 10.0)
//print("actualDuration = \(actualDuration)")
let actionMove = SKAction.moveTo(CGPoint(x: -zoombieSprite.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
// Create the actions
let actionMoveDone = SKAction.removeFromParent()
zoombieSprite.runAction(SKAction.sequence([zoombieAnimation ,actionMove,actionMoveDone]))
}
//MARK: - ANIMATE FRAME AND MOVE ZOOMBIE
func createAnimatedZoombie () -> SKSpriteNode {
let animatedZoobieNode = SKSpriteNode(texture: spriteArray[0])
let animationFrameAction = SKAction.animateWithTextures(spriteArray, timePerFrame: 0.2)
let durationTime = SKAction.waitForDuration(0.1)
let repeatAction = SKAction.repeatActionForever(animationFrameAction)
let quenceAction = SKAction.sequence([durationTime, repeatAction])
animatedZoobieNode.runAction(quenceAction)
return animatedZoobieNode
}
Thanks very much my respectable brother Joseph Lord and Thank God i solved my problem by just dividing sprite kit atlas array count property by 2 because in this folder i had put both #2x and #3x images so when i used to get number of images from this atlas folder it used to return the number which was addition of #2x and #3x images.
What's the best way of drawing multiple horizontal lines and labels for a simple line graph in either ChartJS or D3? I know that I could draw these as individual lines and then do a text overlay but I'm wondering if there is a simpler solution. Ideally I'd be able to create each of the labels below as one unit and move it anywhere.
If this is simpler in another JS graph library then feel free suggest.
Example below
To do it with Chart.js you have to extend the line chart
Chart.types.Line.extend({
name: "LineAlt",
initialize: function (data) {
// it's easier to programmatically update if you store the raw data in the object (vs. storing the geometric data)
this.marks = data.marks;
this.marks.xStart = Number(data.labels[0]);
this.marks.xStep = data.labels[1] - data.labels[0];
// make sure all our x labels are uniformly apart
if (!data.labels.every(function (e, i, arr) { return !i || ((e - arr[i - 1]) === this.marks.xStep); }, this))
throw "labels must be uniformly spaced";
Chart.types.Line.prototype.initialize.apply(this, arguments);
},
draw: function () {
Chart.types.Line.prototype.draw.apply(this, arguments);
// save existing context properties
var self = this;
var ctx = self.chart.ctx;
var scale = self.scale;
ctx.save();
// line properties
ctx.lineWidth = 1;
ctx.fillStyle = "#666";
ctx.strokeStyle = "#666";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.font = scale.font;
// draw marks
self.marks.forEach(function (mark) {
// assuming that the marks are always within the data range
var y = scale.calculateY(mark.y);
var x1 = scale.calculateX((mark.x1 - self.marks.xStart) / self.marks.xStep);
var x2 = scale.calculateX((mark.x2 - self.marks.xStart) / self.marks.xStep);
// draw line
ctx.beginPath();
ctx.moveTo(x1, y);
ctx.lineTo(x2, y);
// draw edges
ctx.moveTo(x1, y + 10);
ctx.lineTo(x1, y - 10);
ctx.moveTo(x2, y + 10);
ctx.lineTo(x2, y - 10);
ctx.stroke();
// draw text
ctx.fillText(mark.label, (x1 + x2) / 2, y + scale.fontSize * 1.5);
})
ctx.restore();
},
});
You pass in the data for drawing the lines like so
var data = {
...
marks: [
{
x1: 1.5,
x2: 3.5,
y: 50,
label: 'Label1'
},
{
x1: 5,
x2: 7,
y: 60,
label: 'Label2'
}
]
};
and you create the chart using this extended chart type
var myLineChart = new Chart(ctx).LineAlt(data);
You can update the lines like this
myLineChart.marks[0].y = 80;
myLineChart.marks[0].x1 = 9;
myLineChart.marks[0].x2 = 10;
and then call
myLineChart.update();
to reflect those changes on the canvas
Caveats
The (x axis) labels should be numeric and uniformly spaced.
The lines should be within the scale range of the y axis (alternatively you can do a scaleOverride to set the scale parameters so that the lines are within the y scale range)
Fiddle - http://jsfiddle.net/en92k763/2/
I would like to create a circular progress with slices where each slice is an arc.
I based my code on this answer:
Draw segments from a circle or donut
But I don't know how to copy it and rotate it 10 times.
And I would like to color it following a progress variable (in percent).
EDIT: I would like something like this
Any help please
Regards
You could use a circular path and set the strokeStart and StrokeEnd. Something like this:
let circlePath = UIBezierPath(ovalInRect: CGRect(x: 200, y: 200, width: 150, height: 150))
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = (360 * 0.125) / 360
for var i = 0; i < 8; i++ {
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
// start angle is number of segments * the segment angle
circleLayer.strokeStart = segmentAngle * CGFloat(i)
// end angle is the start plus one segment, minus a little to make a gap
// you'll have to play with this value to get it to look right at the size you need
let gapSize: CGFloat = 0.008
circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle - gapSize
circleLayer.lineWidth = 10
circleLayer.strokeColor = UIColor(red:0, green:0.004, blue:0.549, alpha:1).CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
// add the segment to the segments array and to the view
segments.insert(circleLayer, atIndex: i)
view.layer.addSublayer(segments[i])
}
Say the following scenario:
I have drawn a quadrilateral shape, which is a mask for a UIView. I denote the shape layer as maskLayer. maskLayer crops the bottom of the UIView asymmetrically.
But then I want to fully reveal my UIView in an animation. The animation should be left side of maskLayer drops down to the bottom of UIView, and .2 sec later my right side of maskLayer also drops down to the bottom of UIView, thus fully reveal the entity of UIView.
My approach is to drop down left line first, then right one as the following code:
//this quadrilateral will put down left corner to the bottom of screen
var path2 = UIBezierPath()
path2.moveToPoint(CGPointZero)
path2.addLineToPoint(CGPoint(x: 0, y: frame.height))
path2.addLineToPoint(CGPoint(x: frame.width, y: frame.height / goldRatio / goldRatio))
path2.addLineToPoint(CGPoint(x: frame.width, y: 0))
path2.closePath()
//this rectangle path will put down both corner to the bottom of screen
//thus fix the view to its original shape
var path3 = UIBezierPath()
path3.moveToPoint(CGPointZero)
path3.addLineToPoint(CGPoint(x: 0, y: frame.height))
path3.addLineToPoint(CGPoint(x: frame.width, y: frame.height))
path3.addLineToPoint(CGPoint(x: frame.width, y: 0))
path3.closePath()
I have spent 2 hours trying to figure it out to no avail. May you please give me some instructions about how to achieve just that.
The initial state is like the following:
The end state is like the following:
I truly appreciate your help!
Easiest way to do this... Cheat.
Don't try to animate the path of the shape it really doesn't need it.
What you should do is something like this.
Create your final state view. Rectangular, at the bottom of the screen with the UI on it etc...
This final state view will not move. It will always be here.
Now create another view and insert it as a sub view underneath the final state view. On this you can add a shape layer with the angular corner cut off.
Now all you need to do is animate the position of this angular view downward until it is completely below the final state view.
If they are the same colour the this will give the effect of animating the path of the shape.
To get the different speeds you could have a rectangluar shape layer rotated to 45 degrees. Then animate it to 0 degrees as the view slides down?
In fact, you could do this with a single shape layer that is rotated and moved.
To do this sort of animation, you would generally use a CADisplayLink (sort of like a timer, but linked to updates of the display rather than some arbitrary interval) to repeatedly change the path associated with desired shape. I think this is easiest if you use a CAShapeLayer and just change the path property of this shape.
To make this work, you need a function that represents the path at a given point of time (or easier, a path at an instant a certain percentageComplete along the animation duration). Since you have a regular shape (constantly the same number of points), you can simply interpolate between some array of startPoints and endPoints.
So, create the shape layer, capture the start time, start the display link, and then for every "tick" of the display link, calculate what percentage of the total animationDuration has passed, and update the shape layer's path accordingly:
class ViewController: UIViewController {
let animationDuration = 2.0
var displayLink: CADisplayLink!
var startTime: CFAbsoluteTime!
var shapeLayer: CAShapeLayer!
let goldRatio: CGFloat = 1.6180339887
var startPoints:[CGPoint]!
var endPoints:[CGPoint]!
override func viewDidLoad() {
super.viewDidLoad()
self.startAnimation()
}
func startAnimation() {
startPoints = [
CGPoint(x: 0, y: view.bounds.size.height),
CGPoint(x: 0, y: view.bounds.size.height - view.bounds.size.height / goldRatio),
CGPoint(x: view.bounds.size.width, y: 0),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height)
]
endPoints = [
CGPoint(x: 0, y: view.bounds.size.height),
CGPoint(x: 0, y: view.bounds.size.height * 0.75),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height * 0.75),
CGPoint(x: view.bounds.size.width, y: view.bounds.size.height)
]
assert(startPoints.count == endPoints.count, "Point counts don't match")
createShape()
startDisplayLink()
}
func startDisplayLink() {
displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
startTime = CFAbsoluteTimeGetCurrent()
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func stopDisplayLink() {
displayLink.invalidate()
displayLink = nil
}
func handleDisplayLink(displayLink: CADisplayLink) {
var percent = CGFloat((CFAbsoluteTimeGetCurrent() - startTime) / animationDuration)
if percent >= 1.0 {
percent = 1.0
stopDisplayLink()
}
updatePathBasedUponPercentComplete(percent)
}
func createShape() {
shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.blueColor().CGColor
shapeLayer.strokeColor = UIColor.clearColor().CGColor
updatePathBasedUponPercentComplete(0.0)
view.layer.addSublayer(shapeLayer)
}
func updatePathBasedUponPercentComplete(percentComplete: CGFloat) {
shapeLayer.path = pathBasedUponPercentComplete(percentComplete, frame: view.frame).CGPath
}
func pathBasedUponPercentComplete(percentComplete: CGFloat, frame: CGRect) -> UIBezierPath {
var path = UIBezierPath()
for i in 0 ..< startPoints.count {
let point = CGPoint(
x: startPoints[i].x + (endPoints[i].x - startPoints[i].x) * percentComplete,
y: startPoints[i].y + (endPoints[i].y - startPoints[i].y) * percentComplete
)
if i == 0 {
path.moveToPoint(point)
} else {
path.addLineToPoint(point)
}
}
path.closePath()
return path
}
}