I have a chart of name versus age, where name is on the x axis and value on Y axis.
The problem is the names are getting overlapped on the x axis which do not look good.
I could not find any formatting for showing truncated name values on x axis and complete as we zoom in.
Is there any way to show names with ellipses or other formatting where the names will not overlap?
The alterTickMark: method on SChartDelegate allows you to modify a tickMark (and its corresponding tickLabel) before they are added to the axis.
You could potentially check the axisRange in this step, and decide if the range.span is small enough that you could display labels truncated or in full.
E.g.
-(void)sChart:(ShinobiChart *)chart alterTickMark:(SChartTickMark *)tickMark beforeAddingToAxis:(SChartAxis *)axis
{
if (!axis.isXAxis)
return;
if ([axis.axisRange.span doubleValue] > 5)
{
NSString *shortText = [tickMark.tickLabel.text substringToIndex:3];
tickMark.tickLabel.text = [NSString stringWithFormat:#"%#...", shortText];
//Resize, but maintain centering
CGPoint center = tickMark.tickLabel.center;
[tickMark.tickLabel sizeToFit];
tickMark.tickLabel.center = center;
}
}
As full disclosure, I work for ShinobiControls.
If you need to adjust tick label width in column series and zooming is enabled (which makes more space for label after zoom gesture), just implement SChart delegate method:
- (void)sChart:(ShinobiChart *)chart alterTickMark:(SChartTickMark *)tickMark beforeAddingToAxis:(SChartAxis *)axis
{
if (axis.isXAxis) {
// Adjusting tickmark labels
UILabel *label = [tickMark tickLabel];
CGFloat maxLabelWidth = axis.axisFrame.size.width / [axis.axisRange.span floatValue];
CGRect labelFrame = label.frame;
labelFrame.size.width = maxLabelWidth;
[label setFrame:labelFrame];
}
}
Look at the longestLabelStringOn delegate method. Example:
func sChart(_ chart: ShinobiChart, longestLabelStringOn axis: SChartAxis) -> String? {
if axis == chart.xAxis{
return "Longest Possible String"
}
else{
return ""
}
}
Related
I am trying to render string using the CoreText api. Each character is rendered in the CAShapeLayer and i am getting the layer offset (x coordinate) for each character like this:
let offset = CTLineGetOffsetForStringIndex(line, glyphIndex, nil)
But the resulting offset doesn't seem to respect kerning between letters. Here is a an image of what i mean - the top label is a UILabel, the bottom one is my custom rendering:
As you can see the "A" letter in my custom label is placed further from the "W" letter.
I would really appreciate any help.
Thanks in advance.
In case someone has the same problem, i used an approach from this place: https://github.com/MichMich/XMCircleType/blob/master/XMCircleType/Views/XMCircleTypeView.m, the kerningForCharacter function. Here is the function itself:
- (float)kerningForCharacter:(NSString *)currentCharacter afterCharacter:(NSString *)previousCharacter
{
//Create a unique cache key
NSString *kerningCacheKey = [NSString stringWithFormat:#"%#%#", previousCharacter, currentCharacter];
//Look for kerning in the cache dictionary
NSNumber *cachedKerning = [self.kerningCacheDictionary objectForKey:kerningCacheKey];
//If kerning is found: return.
if (cachedKerning) {
return [cachedKerning floatValue];
}
//Otherwise, calculate.
float totalSize = [[NSString stringWithFormat:#"%#%#", previousCharacter, currentCharacter] sizeWithAttributes:self.textAttributes].width;
float currentCharacterSize = [currentCharacter sizeWithAttributes:self.textAttributes].width;
float previousCharacterSize = [previousCharacter sizeWithAttributes:self.textAttributes].width;
float kerning = (currentCharacterSize + previousCharacterSize) - totalSize;
//Store kerning in cache.
[self.kerningCacheDictionary setValue:#(kerning) forKey:kerningCacheKey];
//Return kerning.
return kerning;
}
I want to know if it is possible to have custom spacing between bars after some fixed interval using Coreplot iOS library.
Like in the image below, after each 7 bars an unusual barspace is shown.
And if it is possible can you please guide how can this be achieved ?
CPTBarPlot has the code to manage this.
-(BOOL)barAtRecordIndex:(NSUInteger)idx basePoint:(CGPoint *)basePoint tipPoint:(CGPoint *)tipPoint
Basically gets the bar and sets its basePoint and tipPoint.
At the end, it is using barOffsetLength to offset each bar based on its index.
CGFloat barOffsetLength = [self lengthInView:self.barOffset] * self.barOffsetScale;
For vertical bars, in your case, its offsetting the x coord of base and tip point. These are usually the same. Here you have the choice of adding your own offset.
Simply, here's what you need to do there in the same function:
CGFloat barOffsetLength = [self lengthInView:self.barOffset] * self.barOffsetScale;
if ([self.dataSource hasGapBeforeIndex:idx]) {
offsetGap += [self.dataSource gapValue];
}
// Offset
if ( horizontalBars ) {
basePoint->y += barOffsetLength;
tipPoint->y += barOffsetLength;
}
else {
//HERO
basePoint->x += barOffsetLength + offsetGap;
tipPoint->x += barOffsetLength + offsetGap;
}
Here, you introduce a new variable in CPTBarPlot called offsetGap which gets increments everytime you introduce a gap. (be careful, this needs to be reset to zero when you change the dataset).
Also, in CPTPlotDataSource introduce
- (BOOL) hasGapBeforeIndex:(NSUInteger)index;
- (CGFloat) gapValue;
and implement it in your View Controller. Now you can introduce the gap anywhere.
PS: This obviously is a hack and upsets the axis labels and other things that might also need adjustment, but gives an overview anyway.
I played around with the sample app to achieve this.
You need to modify the positioning in your Core Plot data source method for the x axis
- (NSNumber *) numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)idx
and take into account where you want the spacing to occur. If you still don't get it, please post some code and I'll show you on that.
Logic example :
I want to represent the data for a month, lets say one that has 30 days, but at each 5 days, I want a pause at each 5 days. So instead of returning 30 in
- (NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot
, you return 34, and at indexes 6, 11, 16, 21 and 26 you return 0 for the method above.
You can extend this if you want not that much space for the 'pauses' and return double the amount of days (60), minus 4 (because for the pauses you return only for one record the value 0) and return for each 2 records the corresponding value in your data source. This can be again extended to your needed multiplier. I hope you got what I mean.
Thanks to #zakishaheen answer I managed to achieve this, but I broke label position and scroll content size 😄. This implementation is hacky thats why I decided not to continue with fixing it, its more just an example.
I created custom CustomOffsetBarPlot class and apply some Objective-C runtime magic.
- (BOOL)superImplementation:(SEL)selector idx:(NSUInteger)idx basePoint:(nonnull CGPoint *)basePoint tipPoint:(nonnull CGPoint *)tipPoint {
Class granny = [self superclass];
BOOL(* grannyImp)(id,SEL,NSUInteger,CGPoint*, CGPoint*) = (BOOL (*)(id,SEL,NSUInteger,CGPoint*, CGPoint*))class_getMethodImplementation(granny, selector);
return grannyImp(self, selector, idx, basePoint, tipPoint);
}
-(BOOL)barAtRecordIndex:(NSUInteger)idx basePoint:(nonnull CGPoint *)basePoint tipPoint:(nonnull CGPoint *)tipPoint {
SEL selector = _cmd;
CGPoint originBasePointStart = *basePoint;
CGPoint originTipPointStart = *tipPoint;
[self superImplementation:selector idx:0 basePoint:&originBasePointStart tipPoint:&originTipPointStart];
BOOL result = [self superImplementation:selector idx:idx basePoint:basePoint tipPoint:tipPoint];
Class granny = [self class];
SEL lengthView = NSSelectorFromString(#"lengthInView:");
CGFloat(* grannyImp)(id,SEL,NSDecimal) = (CGFloat (*)(id,SEL,NSDecimal))class_getMethodImplementation(granny, lengthView);
CGFloat barOffsetLengthOrigin = grannyImp(self, selector, self.barOffset.decimalValue);
NSInteger barOffsetLength = originBasePointStart.x + idx * 18 + idx * 5; // idx * 5 - your offset
basePoint->x = barOffsetLength;
tipPoint->x = barOffsetLength;
return result;
}
I am trying to create a layout for my iPhone and iPad app that automatically reflows based on dynamic data.
A bit more concrete, this means that I have several objects. Each object has a certain type and associated data. The data is what should be displayed, the type determines how it should be displayed. A type could be text, meaning it should be displayed as simple text without the possibility for interaction. Another type could be date, meaning it should be displayed as a control that lets the user select a date, when tapped.
Now, the problem is that the objects I want to display are dynamic in number and associated data, so I can't position the controls at static locations.
This jsfiddle shows what kind of layout I mean. Depending on the width of the div, the content automatically re-flows and the date input control appears in the middle of the text.
I couldn't find any control that supports a scenario like this - so how would I go about achieving this?
Here is an example of using CoreText to solve a partially similar problem. Perhaps that will point you in that direction.
- (CFArrayRef)copyRectangularPathsForPath:(CGPathRef)path
height:(CGFloat)height {
CFMutableArrayRef paths = CFArrayCreateMutable(NULL, 0,
&kCFTypeArrayCallBacks);
// First, check if we're a rectangle. If so, we can skip the hard parts.
CGRect rect;
if (CGPathIsRect(path, &rect)) {
CFArrayAppendValue(paths, path);
}
else {
// Build up the boxes one line at a time. If two boxes have the
// same width and offset, then merge them.
CGRect boundingBox = CGPathGetPathBoundingBox(path);
CGRect frameRect = CGRectZero;
for (CGFloat y = CGRectGetMaxY(boundingBox) - height;
y > height; y -= height) {
CGRect lineRect =
CGRectMake(CGRectGetMinX(boundingBox), y,
CGRectGetWidth(boundingBox), height);
CGContextAddRect(UIGraphicsGetCurrentContext(), lineRect);
// Do the math with full precision so we don't drift,
// but do final render on pixel boundaries.
lineRect = CGRectIntegral(clipRectToPath(lineRect, path));
CGContextAddRect(UIGraphicsGetCurrentContext(), lineRect);
if (! CGRectIsEmpty(lineRect)) {
if (CGRectIsEmpty(frameRect)) {
frameRect = lineRect;
}
else if (frameRect.origin.x == lineRect.origin.x &&
frameRect.size.width == lineRect.size.width) {
frameRect = CGRectMake(lineRect.origin.x, lineRect.origin.y, lineRect.size.width,
CGRectGetMaxY(frameRect) - CGRectGetMinY(lineRect));
}
else {
CGMutablePathRef framePath =
CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
CFArrayAppendValue(paths, framePath);
CFRelease(framePath);
frameRect = lineRect;
}
}
}
if (! CGRectIsEmpty(frameRect)) {
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
CFArrayAppendValue(paths, framePath);
CFRelease(framePath);
}
}
return paths;
}
I don't understand mapRectThatFits in the slightest. Here is a simple line of code:
MKMapRect zoomRectNorm = [mapView mapRectThatFits:zoomRect];
// BREAKPOINT HERE
Now lets look at the debugger.
Print zoomRect:
(lldb) p zoomRect
(MKMapRect) $1 = {
(MKMapPoint) origin = {
(double) x = 4.2997e+07
(double) y = 9.36865e+07
}
(MKMapSize) size = {
(double) width = 26493.1
(double) height = 148685
}
}
Print zoomRectNorm:
(lldb) p zoomRectNorm
(MKMapRect) $2 = {
(MKMapPoint) origin = {
(double) x = 4.29283e+07
(double) y = 9.36379e+07
}
(MKMapSize) size = {
(double) width = 163840
(double) height = 245760
}
}
So it adjusted the aspect ratio to 2:3 but it did not maintain the width, the height, or the origin!?
According to the documentation it should return:
A map rectangle that is still centered on the same point of the map
but whose width and height are adjusted to fit in the map view’s
frame.
Whats the deal? I would expect it to maintain the origin (as stated in the docs) and at least one of the width/height?
MapRect that fits will zoom out until it hits a zoom level that can contain your region do that the tiles are displayed in their native resolution.
It gives you back the map rect that you would get if you used setVisibleMapRect on the mapview. The center should be the same. The origin probably won't. You'll have to think about the difference between origin and center to understand why. The other thing to understand is that, although you ask for a specific map rect to be set, the mapview will always set its own idea of what is best. Its idea of what is best is the one that allows it to display tiles without zooming in or out.
In my app, I use a UIPopoverController and I use the presentPopoverFromRect API. What I am doing now is just setting it to the frame of my whole UISegmentedControl. However I want to be more precise than this. Is there any way to get the frame of a specific index in the UISegmentedControl?
Thanks!
For our project we needed the actual frames of each segment, frame division wasn't enough. Here's a function I wrote that calculates the exact frame for every segment. Be aware that it accesses the segmented control actual subviews, so it might break in any iOS update.
- (CGRect)segmentFrameForIndex:(NSInteger)index inSegmentedControl:(UISegmentedControl *)control
{
// WARNING: This function gets frame from UISegment objects, undocumented subviews of UISegmentedControl.
// May break in iOS updates.
NSMutableArray *segments = [NSMutableArray arrayWithCapacity:self.numberOfSegments];
for (UIView *view in control.subviews) {
if ([NSStringFromClass([view class]) isEqualToString:#"UISegment"]) {
[segments addObject:view];
}
}
NSArray *sorted = [segments sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
if (a.frame.origin.x < b.frame.origin.x) {
return NSOrderedAscending;
} else if (a.frame.origin.x > b.frame.origin.x) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
return [[sorted objectAtIndex:index] frame];
}
If the segments are equal, why not just divide the width of the control by the number of the selected segment (+1 because numbering starts at 0)?
EDIT: Like this
-(void)showPopover:(id)sender {
if ((UISegmentedControl*)sender.selectedSegmentIndex == 0)
[self.popover presentPopoverFromRect:CGRectMake(self.segmentedControl.frame.size.width/6, self.segmentedControl.frame.origin.y, aWidth, aHeight)]
}
It's over 6 (I'm assuming a 3 segment implementation), because you have to get the center of the segment, and 3 would put it on the lines. And if you do some simple math here (let's assume the whole control is 60 px wide), then 60/3 yeilds 20. Because each segment is 20 px wide, the width of 60 over six yields the correct answer 10.
This can be done much easier without matching class names, providing that you use no custom segmented control (Swift version)
extension NSSegmentedControl {
func frameForSegment(segment: Int) -> NSRect {
var left : CGFloat = 0
for i in 0..<segmentCount {
let w = widthForSegment(i)
if i == segment {
let off = CGFloat(i) + 2 // Account for separators and border.
return NSRect(x: left + off, y: bounds.minY, width: w, height: bounds.height)
}
left += w
}
return NSZeroRect
}
}
If somebody is searching for a solution for NSSegmentedControl, I've slightly modified #erik-aigner's answer. For UISegmentedControl it should work similarly.
This version improves the geometry computation for spacing and the code in general. Disclaimer: It was tested only for a segmented control placed in a toolbar.
import AppKit
public extension NSSegmentedControl {
/// The width of a single horizontal border.
public static let horizontalBorderWidth: CGFloat = 3
/// The height of a single vertical border.
public static let verticalBorderWidth: CGFloat = 2
/// The horizontal spacing between segments.
public static let horizontalSegmentSpacing: CGFloat = 1
/// Returns the frame of the specified segment.
///
/// - Parameter segment: The index of the segment whose frame should be computed.
/// - Returns: The frame of the segment or `.zero` if an invalid segment index is passed in.
public func frame(forSegment segment: Int) -> NSRect {
let y = bounds.minY - NSSegmentedControl.verticalBorderWidth
let height = bounds.height
var left = NSSegmentedControl.horizontalBorderWidth
for index in 0..<segmentCount {
let width = self.width(forSegment: index)
if index == segment {
return NSRect(x: left, y: y, width: width, height: height)
}
left += NSSegmentedControl.horizontalSegmentSpacing
left += width
}
return .zero
}
}
Swift 5.3 version of #CodaFi's answer:
func showPopover(_ sender: Any?) {
if sender?.selectedSegmentIndex as? UISegmentedControl == 0 {
popover.presentPopover(fromRect: CGRect(x: segmentedControl.frame.size.width / 6, y: segmentedControl.frame.origin.y, width: aWidth, height: aHeight))
}
}
For me widthForSegment returns 0, so the other answers always fail to produce frame. I think a better solution is to just get the frame of a subview:
public extension UISegmentedControl {
func frame(forSegment segment: Int) -> CGRect {
subviews[segment].frame
}
}