I was wondering about programmatically adding a NSTableView inside a NSStackView using Swift 3/MacOS Sierra.
The idea would be to have say 2 NSTextFields aligned via the centerY axis in the .leading gravity space, then a tableview in the .center gravity space, then 2 more NSTextFields aligned via the centerY axis in the .trailing gravity space. The stack view would span the width of the NSView -- like a header.
Is this a good idea or should I avoid doing this? It has been very difficult to get it to look correct -- the table always has too large of a width despite adding constraints to try to pin it to a fixed width.
Any insight would be appreciated. I'm new to programming MacOS.
Thanks,
Here is the output in Interface Builder:
output of the headerview
Here is the code of the NSView I'm using:
The view controller is elsewhere but I'm not really having problems with the view controller -- it's displaying the data in the table correctly. It's just the sizing/positioning of the tableview (which I'm trying to do in the NSView via the NSStackView) is always wrong. It should have a width of 650 but instead has a width of 907 and I get the same error all the time in the debug console:
2017-09-12 17:43:36.041062-0500 RaceProgram[795:36958] [Layout] Detected missing constraints for < RacingProgram.RaceImportViewHeader: 0x6000001ccd50 >. It cannot be placed because there are not enough constraints to fully define the size and origin. Add the missing constraints, or set translatesAutoresizingMaskIntoConstraints=YES and constraints will be generated for you. If this view is laid out manually on macOS 10.12 and later, you may choose to not call [super layout] from your override. Set a breakpoint on DETECTED_MISSING_CONSTRAINTS to debug. This error will only be logged once.
import Cocoa
#IBDesignable
class RaceImportViewHeader: NSView {
// MARK: Properties
private var raceQualificationsTableView:NSTableView
private var raceImportHeaderStackView:NSStackView
private var raceNumberTitle: NSTextField
private var raceNumberValue: NSTextField
public var raceQualificationsTableRowHeight: CGFloat
#IBInspectable var borderColor:NSColor = .black
#IBInspectable var backgroundColor:NSColor = .lightGray
enum InitMethod {
case Coder(NSCoder)
case Frame(CGRect)
}
override convenience init(frame: CGRect) {
self.init(.Frame(frame))!
}
required convenience init?(coder: NSCoder) {
self.init(.Coder(coder))
}
private init?(_ initMethod: InitMethod) {
// Group together the initializers for this view class
raceQualificationsTableView = NSTableView()
raceImportHeaderStackView = NSStackView()
raceNumberTitle = NSTextField()
raceNumberValue = NSTextField()
raceQualificationsTableRowHeight = 17.0 // Initialize the row height for raceQualifications
switch initMethod {
case let .Coder(coder): super.init(coder: coder)
case let .Frame(frame): super.init(frame: frame)
}
self.translatesAutoresizingMaskIntoConstraints = false
drawUI()
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let viewSize: NSRect = self.frame
let newRect = NSRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height)
// Outline the Header --> Only for layout debug purposes
let path = NSBezierPath(rect: newRect)
backgroundColor.setFill()
path.fill()
borderColor.setStroke() // Set the stroke color
path.stroke() // Fill the stroke or border of the rectangle
}
// MARK: UI Construction
func drawUI() {
let viewFrame = self.frame // with respect to the super class
let viewBounds = self.bounds // with respect to the view
// MARK: Race Number Setup
func addRaceNumberTitle(startingPositionX: CGFloat) {
// This configures label for race Number
let width:CGFloat = 60.0 //Arbitrary at the moment
let height:CGFloat = 40.0
let leftPadding:CGFloat = 2.5 // The super view (frame)is the NSView in this case
let topPadding:CGFloat = (viewBounds.height - height)/2
let raceNumberTitleNSRect = NSRect(x: leftPadding + startingPositionX, y: viewBounds.height - height - topPadding, width: width, height: height)
//Swift.print("The raceNumberTitleNSRect title NSRect is \(raceNumberTitleNSRect)")
raceNumberTitle = NSTextField(frame: raceNumberTitleNSRect)
raceNumberTitle.stringValue = "Race\nNumber"
raceNumberTitle.maximumNumberOfLines = 2
raceNumberTitle.isEditable = false
raceNumberTitle.isBordered = false
raceNumberTitle.alignment = .center
raceNumberTitle.backgroundColor = .clear
raceNumberTitle.sizeToFit()
let updatedHeight = raceNumberTitle.frame.height
let newUpdatedPadding = (viewBounds.height - updatedHeight) / 2
let oldOriginX = raceNumberTitle.frame.origin.x
let newOriginY = viewBounds.height - updatedHeight - newUpdatedPadding
let newOrigin = NSPoint(x: oldOriginX, y: newOriginY)
raceNumberTitle.setFrameOrigin(newOrigin)
//addSubview(raceNumberTitle) // Add to view
raceImportHeaderStackView.addView(raceNumberTitle, in: .leading)
}
func addRaceNumberValue(startingPositionX: CGFloat) {
// This configures value label for race number
let width:CGFloat = 20.0 //Arbitrary at the moment
let height:CGFloat = 40.0
let leftPadding:CGFloat = 5.0 // The super view (frame)is the NSView in this case
let topPadding:CGFloat = (viewBounds.height - height)/2
let raceNumberInRect = NSRect(x: startingPositionX + leftPadding, y: viewBounds.height - height - topPadding, width: width, height: height)
Swift.print("The raceNumberInRect title NSRect is \(raceNumberInRect)")
raceNumberValue = NSTextField(frame: raceNumberInRect)
raceNumberValue.identifier = "raceNumber"
raceNumberValue.placeholderString = "1"
raceNumberValue.font = NSFont(name: "Impact", size: 20.0)
raceNumberValue.maximumNumberOfLines = 1
raceNumberValue.isEditable = false
raceNumberValue.isBordered = true
raceNumberValue.alignment = .center
raceNumberValue.backgroundColor = .clear
raceNumberValue.sizeToFit()
let updatedHeight = raceNumberValue.frame.height
let oldOriginX = raceNumberValue.frame.origin.x
let newUpdatedPadding = (viewBounds.height - updatedHeight) / 2
let newOriginY = viewBounds.height - updatedHeight - newUpdatedPadding
let newOrigin = NSPoint(x: oldOriginX, y: newOriginY)
raceNumberValue.setFrameOrigin(newOrigin)
//addSubview(raceNumberValue) // Add to view
raceImportHeaderStackView.addView(raceNumberValue, in: .leading)
}
// MARK: Race Qualifications Table Setup
func addRaceQualificationsTable(startingPositionX: CGFloat) {
// Padding variables
let leftPadding:CGFloat = 5.0
let topPadding:CGFloat = 5.0
// Table Properties
let width:CGFloat = 650.0
let height:CGFloat = 40
let tableRect = CGRect(x: startingPositionX + leftPadding, y: viewBounds.height - height - topPadding, width: width, height: height)
//let insetForTableView:CGFloat = 1.0
//let scrollRect = CGRect(x: tableRect.origin.x-insetForTableView, y: tableRect.origin.y-insetForTableView, width: tableRect.width+2*insetForTableView, height: tableRect.height+2*insetForTableView)
let tableNSSize = NSSize(width: tableRect.width, height: tableRect.height)
let scrollNSRect = NSScrollView.frameSize(forContentSize: tableNSSize, horizontalScrollerClass: nil, verticalScrollerClass: nil, borderType: .bezelBorder, controlSize: .regular, scrollerStyle: .legacy)
Swift.print("tableRect \(tableRect)")
Swift.print("scrollNSRect \(scrollNSRect)")
//Swift.print("scrollRect \(scrollRect)")
let scrollViewOrigin:CGPoint = tableRect.origin
let scrollViewNSSize:CGSize = scrollNSRect
let scrollRect = NSRect(origin: scrollViewOrigin, size: scrollViewNSSize)
Swift.print("scrollRect \(scrollRect)")
let tableScrollView = NSScrollView(frame: scrollRect)
raceQualificationsTableView = NSTableView(frame: tableRect)
raceQualificationsTableView.identifier = "raceQualificationsTable" // Setup identifier
raceQualificationsTableView.rowHeight = 20.0
Swift.print("instrinic size \(raceQualificationsTableView.intrinsicContentSize)")
//Swift.print("tableScrollView contentsize \(tableScrollView.contentSize)")
tableScrollView.documentView = raceQualificationsTableView
tableScrollView.autoresizingMask = .viewNotSizable
Swift.print("tableScroll content size \(tableScrollView.contentSize)")
//self.addSubview(tableScrollView)
raceImportHeaderStackView.addView(tableScrollView, in: .center)
}
func configureRaceQualificationsTable(showRaceNumberCol: Bool, showRaceCodeCol: Bool) {
let headerAlignment = NSTextAlignment.center // Easy way to change justification of headers
// MARK: Race Number Column Options
let raceNumberColumn = NSTableColumn(identifier: "raceNumberCol")
raceNumberColumn.title = "Race"
raceNumberColumn.minWidth = 40.0
raceNumberColumn.width = 40.0
raceNumberColumn.headerToolTip = "Race Number from the Imported Card"
raceNumberColumn.headerCell.alignment = headerAlignment
// Note: Word Race is always going to be wider than the race number value
// So size to Fit is appropriate here.
raceNumberColumn.sizeToFit()
if showRaceNumberCol {
// Option of not adding this to the table
raceQualificationsTableView.addTableColumn(raceNumberColumn)
}
// MARK: Driver Column Options
let breedColumn = NSTableColumn(identifier: "driverCol")
driverColumn.title = "Driver"
driverColumn.minWidth = 10
driverColumn.headerToolTip = "Driver information"
driverColumn.headerCell.alignment = headerAlignment
driverColumn.sizeToFit()
raceQualificationsTableView.addTableColumn(driverColumn)
// MARK: Race Code Column Options
let raceTypeCodeColumn = NSTableColumn(identifier: "raceTypeCodeCol")
raceTypeCodeColumn.title = "Race Code"
raceTypeCodeColumn.minWidth = 40
raceTypeCodeColumn.headerToolTip = "Race Classification Code"
raceTypeCodeColumn.headerCell.alignment = headerAlignment
raceTypeCodeColumn.sizeToFit()
if showRaceCodeCol {
// Option of not adding to the table
raceQualificationsTableView.addTableColumn(raceTypeCodeColumn)
}
// MARK: Race Type Code Description Options
let raceTypeCodeDescColumn = NSTableColumn(identifier: "raceTypeCodeDescCol")
raceTypeCodeDescColumn.title = "Race Desc"
raceTypeCodeDescColumn.minWidth = 50
raceTypeCodeDescColumn.width = 100
raceTypeCodeDescColumn.headerToolTip = "Race Classification Full Description"
raceTypeCodeDescColumn.headerCell.alignment = headerAlignment
raceQualificationsTableView.addTableColumn(raceTypeCodeDescColumn)
// MARK: Race Restriction Column Options
let raceRestrictionColumn = NSTableColumn(identifier: "raceRestrictionCol")
raceRestrictionColumn.title = "Restrictions"
raceRestrictionColumn.minWidth = 50
raceRestrictionColumn.width = 80
raceRestrictionColumn.headerToolTip = "Race Restrictions"
raceRestrictionColumn.headerCell.alignment = headerAlignment
raceQualificationsTableView.addTableColumn(raceRestrictionColumn)
// MARK: Sex Restriction Column Options
let sexRestrictionColumn = NSTableColumn(identifier: "sexRestrictionCol")
sexRestrictionColumn.title = "Sex"
sexRestrictionColumn.minWidth = 100
sexRestrictionColumn.width = 100
sexRestrictionColumn.headerToolTip = "Sex Restrictions"
sexRestrictionColumn.headerCell.alignment = headerAlignment
raceQualificationsTableView.addTableColumn(sexRestrictionColumn)
// MARK: Age Restriction Column Options
let ageRestrictionColumn = NSTableColumn(identifier: "ageRestrictionCol")
ageRestrictionColumn.title = "Age"
ageRestrictionColumn.minWidth = 100
ageRestrictionColumn.width = 100
ageRestrictionColumn.headerToolTip = "Age Restrictions"
ageRestrictionColumn.headerCell.alignment = headerAlignment
raceQualificationsTableView.addTableColumn(ageRestrictionColumn)
// MARK: Division Column Options
let divisionColumn = NSTableColumn(identifier: "divisionCol")
divisionColumn.title = "Division"
divisionColumn.minWidth = 50
let minDivisionColumnWidth = raceQualificationsTableView.frame.width - raceNumberColumn.width - driverColumn.width - raceTypeCodeColumn.width - raceTypeCodeDescColumn.width - raceRestrictionColumn.width - sexRestrictionColumn.width - ageRestrictionColumn.width
// Calculate the available room for the division column
if (showRaceCodeCol && showRaceNumberCol) {
// This is the minimum case
// No idea why we need the 25.0 manual adjustment
divisionColumn.width = minDivisionColumnWidth - 25.0
} else if (showRaceCodeCol && !showRaceNumberCol) {
// Add back race type code
// No idea why we need to manually adjust 53.5
divisionColumn.width = minDivisionColumnWidth + raceTypeCodeColumn.width - 53.5
} else if (!showRaceCodeCol && showRaceNumberCol) {
// Add back race number col
divisionColumn.width = minDivisionColumnWidth + raceNumberColumn.width
} else {
// Else it's the maximum space
// This code was making the frame too large -- it was increasing the
// the frame size of the column to 670.0 I put a manual reduction of
// 20 to keep the frame size the same. Not sure where this 20 is coming from.
divisionColumn.width = minDivisionColumnWidth + raceNumberColumn.width + raceTypeCodeColumn.width - 20.0
}
//Swift.print("The division column width is \(divisionColumn.width)")
divisionColumn.headerToolTip = "Division -- Unknown what this means"
divisionColumn.headerCell.alignment = headerAlignment
raceQualificationsTableView.addTableColumn(divisionColumn)
//Swift.print("raceQualificationsTableView.frame.width is \( raceQualificationsTableView.frame.width)")
}
// MARK: Race Distance Surface Course Setup
func addRaceDistanceSurfaceCourseTable(startingPositionX: CGFloat) {
// Table Properties
let width:CGFloat = 250.0
let height:CGFloat = 40.0
// Padding variables
let leftPadding:CGFloat = 5.0
let topPosition:CGFloat = (viewBounds.height - ((viewBounds.height - height)/2) - height)
let tableRect = CGRect(x: leftPadding + startingPositionX, y: topPosition, width: width, height: height)
let tableScrollView = NSScrollView(frame: tableRect)
raceDistanceSurfaceCourseTableView = NSTableView(frame: tableRect)
raceDistanceSurfaceCourseTableView.identifier = "raceDistanceSurfaceCourseTable" // Setup identifier
//raceDistanceSurfaceCourseTableView.rowHeight = 20.0
raceDistanceSurfaceCourseTableView.intercellSpacing = NSSize(width: 1.0, height: 1.0)
raceDistanceSurfaceCourseTableView.headerView = ImportRaceTableHeaders()
tableScrollView.documentView = raceDistanceSurfaceCourseTableView
//tableScrollView.hasVerticalScroller = false
//tableScrollView.verticalScroller = nil // Turn off vertical scrolling
//tableScrollView.verticalScrollElasticity = .none
//raceDistanceSurfaceCourseTableView = NSTableViewHeader
//self.addSubview(tableScrollView)
raceImportHeaderStackView.addView(raceDistanceSurfaceCourseTableView, in: .center)
}
// MARK: Construct the fields:
//configureHeaderView()
configureStackView()
addRaceNumberTitle(startingPositionX: 0.0) // Add the race number title
addRaceNumberValue(startingPositionX: raceNumberTitle.frame.origin.x + raceNumberTitle.frame.width) //Add the Race Number value text field
addRaceQualificationsTable(startingPositionX: raceNumberValue.frame.origin.x + raceNumberValue.frame.width)
configureRaceQualificationsTable(showRaceNumberCol: false, showRaceCodeCol: false)
}
// MARK: TableView Functions
func reloadTableViewData(identifier: String) {
Swift.print("Manual reload of data for identifier \(identifier)")
switch identifier {
case "raceQualificationsTable":
raceQualificationsTableView.reloadData()
case "raceDistanceSurfaceCourseTable":
raceDistanceSurfaceCourseTableView.reloadData()
default:
break
}
}
// MARK: Delegate/DataSources Outlets for TableViews
// Race Qualification Table (the header table)
#IBOutlet weak var raceQualificationsDelegate: NSTableViewDelegate? {
get {
return raceQualificationsTableView.delegate
}
set {
raceQualificationsTableView.delegate = newValue
}
}
#IBOutlet weak var raceQualificationsDataSource: NSTableViewDataSource? {
get {
return raceQualificationsTableView.dataSource
}
set {
raceQualificationsTableView.dataSource = newValue
}
}
// Race Distance Surface Course
#IBOutlet weak var raceDistanceSurfaceCourseDelegate: NSTableViewDelegate? {
get {
return raceDistanceSurfaceCourseTableView.delegate
}
set {
raceDistanceSurfaceCourseTableView.delegate = newValue
}
}
#IBOutlet weak var raceDistanceSurfaceCourseDataSource: NSTableViewDataSource? {
get {
return raceDistanceSurfaceCourseTableView.dataSource
}
set {
raceDistanceSurfaceCourseTableView.dataSource = newValue
}
}
// MARK: Label Outlets
#IBOutlet var raceNumber:String? {
get {
return raceNumberValue.stringValue
}
set {
raceNumberValue.stringValue = newValue!
}
}
}
Related
I am using the following code for my wibar tasklist:
s.mytasklist = awful.widget.tasklist
{
screen = s,
filter = awful.widget.tasklist.filter.allscreen,
buttons = tasklist_buttons,
layout = wibox.layout.fixed.horizontal(),
widget_template =
{
{
{
{
id = 'clienticon',
widget = awful.widget.clienticon,
},
margins = 0,
widget = wibox.container.margin,
},
{
id = 'text_role',
widget = wibox.widget.textbox,
},
layout = wibox.layout.fixed.horizontal,
},
id = 'background_role',
forced_width = 200,
forced_height = 60,
widget = wibox.container.background,
create_callback = function(self, c, index, objects)
self:get_children_by_id('clienticon')[1].client = c
end,
},
}
s.mytasklist_onlyminimised = awful.widget.tasklist
{
screen = s,
filter = awful.widget.tasklist.filter.minimizedcurrenttags,
buttons = tasklist_buttons,
style = {
shape_border_width = 1,
shape_border_color = '#333333',
shape = gears.shape.partially_rounded_rect,
},
}
Which makes the tasks on the tasklist have fixed width (as according to this answer)
My question is:
Is it possible to make the tasklist switch to wibox.layout.flex.horizontal when the tasklist is full of tasks?
While you could technically replace the tasklist from a client.connect_signal("tagged", function(c) if #c.screen.selected_tag:clients() >= some_number then <make_a_new_tasklist> end end) style code, that's pretty cheap and ugly.
So I think this would require to contribute a patch upstream to expose the taglist/tasklist constructor arguments as properties. The awful.widget.layoutlist already do it, but not the tag/tasklists.
I'm implementing a little app with Xamarin Forms for a web page, the thing is that in this web is a linear chart with multiple entries and if the user clicks on a point of the line shows info about that point, as you can see in the picture:
Web Line Chart
After some work, I could create a more or less similar line chart using the OxyPlot.Xamarin.Forms plugin with multiple entries which shows the points
My App Line Chart
This is my code:
OnPropertyChanged("GraphModel");
var model = new PlotModel
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.BottomCenter,
LegendOrientation = LegendOrientation.Horizontal,
LegendBorderThickness = 0
};
model.PlotType = PlotType.XY;
model.InvalidatePlot(false);
Dictionary<string, List<Prices>> values = HistoricData[Selected.ProductId];
int colorIndex = 0;
List<string> x_names = new List<string>();
foreach (var item in values.Keys)
{
if (item.ToUpper() == Selected.ProductName) { SelectedIndex = colorIndex; }
var lineSeries = new LineSeries()
{
Title = item,
MarkerType = MarkerType.Circle,
};
lineSeries.MarkerResolution = 3;
lineSeries.MarkerFill = OxyPlot.OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
lineSeries.MarkerStroke = OxyPlot.OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
lineSeries.MarkerSize = 3;
var points = new List<DataPoint>();
lineSeries.Color = OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
foreach (var price in values[item])
{
points.Add(new DataPoint(price.Week+price.Year, price.Price));
}
if (ButtonsVisibility.Count == 0)
{
lineSeries.IsVisible = (Selected.ProductName == item.ToUpper()) ? true : false;
}
else
{
lineSeries.IsVisible = ButtonsVisibility[colorIndex];
}
lineSeries.ItemsSource = points;
lineSeries.MarkerType = OxyPlot.MarkerType.Circle;
model.Series.Add(lineSeries);
colorIndex++;
}
NumButtons = colorIndex;
LinearAxis yaxis = new LinearAxis();
yaxis.Position = AxisPosition.Left;
yaxis.MajorGridlineStyle = LineStyle.Dot;
model.Axes.Add(yaxis);
LineChart = model;
OnPropertyChanged("GraphModel");
return LineChart;
My doubt is which property I should work with and show at least the value of a concrete point, I have seen the property OnTouchStarted but is only for all the LineSeries and not for a single point. I read in some articles that OxyPlot.Xamarin.Forms include a tracker. I added this line in my code:
lineSeries.TrackerFormatString = "X={2},\nY={4}";
Is supposed to show the x and y values on click but doesn't show anything, any suggestion?
Should show something like that: Tracker info on point
From the following example: Tracker Example
Updated Code
public PlotModel GetLineChart()
{
OnPropertyChanged("GraphModel");
var model = new PlotModel
{
LegendPlacement = LegendPlacement.Outside,
LegendPosition = LegendPosition.BottomCenter,
LegendOrientation = LegendOrientation.Horizontal,
LegendBorderThickness = 0
};
model.PlotType = PlotType.XY;
model.InvalidatePlot(false);
Dictionary<string, List<Prices>> values = HistoricData[Selected.ProductId];
int colorIndex = 0;
List<string> x_names = new List<string>();
foreach (var item in values.Keys)
{
if (item.ToUpper() == Selected.ProductName) { SelectedIndex = colorIndex; }
var lineSeries = new LineSeries()
{
Title = item,
MarkerType = MarkerType.Circle,
CanTrackerInterpolatePoints = false
};
lineSeries.MarkerResolution = 3;
lineSeries.MarkerFill = OxyPlot.OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
lineSeries.MarkerStroke = OxyPlot.OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
lineSeries.MarkerSize = 3;
var points = new List<DataPoint>();
lineSeries.Color = OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
foreach (var price in values[item])
{
points.Add(new DataPoint(price.Week+price.Year, price.Price));
}
if (ButtonsVisibility.Count == 0)
{
lineSeries.IsVisible = (Selected.ProductName == item.ToUpper()) ? true : false;
}
else
{
lineSeries.IsVisible = ButtonsVisibility[colorIndex];
}
lineSeries.ItemsSource = points;
lineSeries.MarkerType = OxyPlot.MarkerType.Circle;
lineSeries.TrackerFormatString = "X={2},\nY={4}";
lineSeries.TextColor = OxyPlot.OxyColor.Parse(SubCategoriesViewModel.AvailableColors[colorIndex]);
model.Series.Add(lineSeries);
colorIndex++;
}
NumButtons = colorIndex;
LinearAxis yaxis = new LinearAxis();
yaxis.Position = AxisPosition.Left;
//yaxis.StringFormat = "X={2},\nY={4}";
yaxis.MajorGridlineStyle = LineStyle.Dot;
model.Axes.Add(yaxis);
LineChart = model;
OnPropertyChanged("GraphModel");
return LineChart;
}
}
protected async override void OnAppearing()
{
await _viewModel.LinearViewModel.GetSubCategoryHistoricWeekPrices(App.ViewModel.LoginViewModel.SesionToken, FROM_DATE, TO_DATE);
Plot.Model = _viewModel.LinearViewModel.GetLineChart();
PlotController controller = new PlotController();
controller.UnbindAll();
controller.BindTouchDown(PlotCommands.PointsOnlyTrackTouch);
Plot.Controller = controller;
AddButtons();
}
Xaml Declaration for plot view:
<oxy:PlotView
Grid.Row="2"
Grid.RowSpan="2"
Grid.ColumnSpan="4"
x:Name="Plot" />
Your problem lies with following line.
lineSeries.TrackerKey = "X={2},\nY={4}";
When you use series.TrackerKey, you are specifying that you are using a CustomTracker, which in this case you are not. Custom trackers would be useful if you need to use different trackers for each series in the model.
In you case, you should remove that line and use only TrackerFormatString.
lineSeries.TrackerFormatString = "X={2},\nY={4}";
This would show the tooltip using the format string parameters, where {2} signifies X Value and {4} signifies Y. For your information, following are place holders.
{0} = Title of Series
{1} = Title of X-Axis
{2} = X Value
{3} = Title of Y-Axis
{4} = Y Value
If you need to include additional/custom information in your tool, your Data Point needs to be include that information. This where IDataPointProvider interface becomes handy. You could create a Custom DataPoint by implementing the interface and then you could include the same information in your tooltip as well.
Update Based On Comments
Additionally, to include "Touch", you can specify TouchDown in the PlotController. You can do so by defining the PlotController in your viewModel as following.
public PlotController CustomPlotController{get;set;}
You can define the property as follows.
CustomPlotController = new PlotController();
CustomPlotController.UnbindAll();
CustomPlotController.BindTouchDown(PlotCommands.PointsOnlyTrackTouch);
And in your Xaml
<oxy:Plot Controller="{Binding CustomPlotController}"......
class Flight{
var name:String?
var vocabulary:Vocabulary?
}
class Vocabulary{
var seatMapPlan:[Plan] = []
var foodPlan:[Plan] = []
}
class Plan{
var planName:String?
var planId:String?
}
var flightList:[Flight] = []
var plan1 = Plan()
plan1.planId = "planId1"
plan1.planName = "Planname1"
var plan2 = Plan()
plan2.planId = "planId2"
plan2.planName = "Planname2"
var plan3 = Plan()
plan3.planId = "planId3"
plan3.planName = "Planname3"
var plan4 = Plan()
plan4.planId = "planId4"
plan4.planName = "Planname4"
var plan5 = Plan()
plan5.planId = "planId5"
plan5.planName = "Planname5"
var plan6 = Plan()
plan6.planId = "planId6"
plan6.planName = "Planname6"
var flight1 = Flight()
flight1.name = "Flight1"
flight1.vocabulary = Vocabulary()
flight1.vocabulary?.seatMapPlan = [plan1, plan2]
flight1.vocabulary?.foodPlan = [plan3, plan4, plan5]
var flight2 = Flight()
flight2.name = "Flight2"
flight2.vocabulary = Vocabulary()
flight2.vocabulary?.seatMapPlan = [plan2, plan3]
flight2.vocabulary?.foodPlan = [plan3, plan4, plan5]
flightList=[flight1, flight2]
Problem 1:
I want to use flatmap,filter,custom unique func or Sets.formUnion to achieve a union of seatMapPlans. For this particular example it is
seatMapUnion = [plan1,plan2,plan3]
Because of nesting with the help of answered questions I am unable to achieve this.
Please give me a combination of filter,flatMap and map for resolving this particular problem.
Problem 2:
I have vice-versa scenarios too were i have to sort this array flightList on basis of plan(plan1 or multiple) selected. I want to sort this on basis of filter and map, but the nesting is making it difficult to achieve.
e.g. 1:
if the search parameter is plan1 for seatMapPlan. Then the result is flight1.
e.g. 2:
And if the search parameter is plan2 for seatMapPlan. Then the result is flight1,flight2.
For the first problem I would use sets. So first make Plan implement Hashable :
class Plan : Hashable {
var planName:String?
var planId:String?
public var hashValue: Int { return planName?.hashValue ?? 0 }
public static func ==(lhs: Plan, rhs: Plan) -> Bool { return lhs.planId == rhs.planId }
}
Then it's straightforward :
let set1 = Set<Plan>(flight1.vocabulary!.seatMapPlan)
let set2 = Set<Plan>(flight2.vocabulary!.seatMapPlan)
let union = set1.union(set2)
print(union.map { $0.planName! } )
It'll print:
["Planname2", "Planname1", "Planname3"]
Not sure I understand your second problem.
I have a crossfilter with the following data structure being inputted.
project | subproject | cost
data = [
["PrA", "SubPr1", 100],
["PrA", "SubPr2", 150],
["PrA", "SubPr3", 100],
["PrB", "SubPr4", 300],
["PrB", "SubPr5", 500],
["PrC", "SubPr6", 450]]
I can create a barchart that has the summed cost per project:
var ndx = crossfilter(data)
var projDim = ndx.dimension(function(d){return d.project;});
var projGroup = costDim.group().reduceSum(function(d){return d.budget;});
What I want to do is create a dc.js histogram by project cost...so {450: 2, 300: 1}, etc. As far as I can tell, crossfilter can have only attributes of each row be input for a dimension. Is there a way around this?
Accepting the challenge!
It is true, crossfilter does not support this kind of double-reduction, but if you are willing to accept a slight loss of efficiency, you can create "fake dimensions" and "fake groups" with the desired behavior. Luckily, dc.js doesn't use very much of the crossfilter API, so you don't have to implement too many methods.
The first part of the trick is to duplicate the dimension and group so that the new dimension and old dimension will each observe filtering on the other.
The second part is to create the fake groups and dimensions, which walk the bins of the copied group and rebin and refilter based on the values instead of the keys.
A start of a general solution is below. For some charts it is also necessary to implement group.top(), and it is usually okay to just forward that to group.all().
function values_dimension(dim, group) {
return {
filter: function(v) {
if(v !== null)
throw new Error("don't know how to do this!");
return dim.filter(null);
},
filterFunction: function(f) {
var f2 = [];
group.all().forEach(function(kv) {
if(f(kv.value))
f2.push(kv.key);
});
dim.filterFunction(function(k) {
return f2.indexOf(k) >= 0;
});
return this;
}
};
}
function values_group(group) {
return {
all: function() {
var byv = [];
group.all().forEach(function(kv) {
if(kv.value === 0)
return;
byv[kv.value] = (byv[kv.value] || 0) + 1;
});
var all2 = [];
byv.forEach(function(d, i) {
all2.push({key: i, value: d});
});
return all2;
}
};
}
// duplicate the dimension & group so each will observe filtering on the other
var projDim2 = ndx.dimension(function(d){return d.project;});
var projGroup2 = projDim2.group().reduceSum(function(d){return d.budget;});
var countBudgetDim = values_dimension(projDim2, projGroup2),
countBudgetGroup = values_group(projGroup2);
jsfiddle here: http://jsfiddle.net/gordonwoodhull/55zf7L1L/
JSFillde Link
Denormalize + Map-reduce. Note the data already include the cost per project as the 4th column ( and this can be pre-calculated easily). It's a hack, but hopefully an easy one in order to get DC.js and crossfilter works without too much change.
var data = [
["PrA", "SubPr1", 100, 450],
["PrA", "SubPr2", 150, 450],
["PrA", "SubPr3", 200, 450],
["PrB", "SubPr4", 300, 800],
["PrB", "SubPr5", 500, 800],
["PrC", "SubPr6", 450, 450]
];
var newdata = data.map(function (d) {
return {
project: d[0],
subproject: d[1],
budget: d[2],
cost: d[3]
};
})
var ndx = crossfilter(newdata),
costDim = ndx.dimension(function (d) {
return d.cost;
}),
visitedProj = {},
costGroup = costDim.group().reduce(function (p, v) {
if (visitedProj[v.project]) return p;
console.info(v.project);
visitedProj[v.project] = true;
return p + 1;
}, null, function () {
return 0;
});
dc.rowChart("#costChart")
.renderLabel(true)
.dimension(costDim)
.group(costGroup)
.xAxis().ticks(2);
dc.renderAll();
Map-Reduce can be very powerful and the API can be accessed from here. JSFillde Link
I am currently using a UILabel to display multiple lines of text. The line break most is set to NSLineBreakByWordWrapping so the the label automatically inserts line breaks. How can I detect where those line breaks are? Basically, I want to return a string with each \n break included.
Here is my work around for detecting line breaks inside UILabel
- (int)getLengthForString:(NSString *)str fromFrame:(CGRect)frame withFont:(UIFont *)font
{
int length = 1;
int lastSpace = 1;
NSString *cutText = [str substringToIndex:length];
CGSize textSize = [cutText sizeWithFont:font constrainedToSize:CGSizeMake(frame.size.width, frame.size.height + 500)];
while (textSize.height <= frame.size.height)
{
NSRange range = NSMakeRange (length, 1);
if ([[str substringWithRange:range] isEqualToString:#" "])
{
lastSpace = length;
}
length++;
cutText = [str substringToIndex:length];
textSize = [cutText sizeWithFont:font constrainedToSize:CGSizeMake(frame.size.width, frame.size.height + 500)];
}
return lastSpace;
}
-(NSString*) getLinebreaksWithString:(NSString*)labelText forWidth:(CGFloat)lblWidth forPoint:(CGPoint)point
{
//Create Label
UILabel *label = [[UILabel alloc] init];
label.text = labelText;
label.numberOfLines = 0;
label.lineBreakMode = NSLineBreakByWordWrapping;
//Set frame according to string
CGSize size = [label.text sizeWithFont:label.font
constrainedToSize:CGSizeMake(lblWidth, MAXFLOAT)
lineBreakMode:UILineBreakModeWordWrap];
[label setFrame:CGRectMake(point.x , point.y , size.width , size.height)];
//Add Label in current view
[self.view addSubview:label];
//Count the total number of lines for the Label which has NSLineBreakByWordWrapping line break mode
int numLines = (int)(label.frame.size.height/label.font.leading);
//Getting and dumping lines from text set on the Label
NSMutableArray *allLines = [[NSMutableArray alloc] init];
BOOL shouldLoop = YES;
while (shouldLoop)
{
//Getting length of the string from rect with font
int length = [self getLengthForString:labelText fromFrame:CGRectMake(point.x, point.y, size.width, size.height/numLines) withFont:label.font] + 1;
[allLines addObject:[labelText substringToIndex:length]];
labelText = [labelText substringFromIndex:length];
if(labelText.length<length)
shouldLoop = NO;
}
[allLines addObject:labelText];
//NSLog(#"\n\n%#\n",allLines);
return [allLines componentsJoinedByString:#"\n"];
}
How to use
NSString *labelText = #"We are what our thoughts have made us; so take care about what you think. Words are secondary. Thoughts live; they travel far.";
NSString *stringWithLineBreakChar = [self getLinebreaksWithString:labelText forWidth:200 forPoint:CGPointMake(15, 15)];
NSLog(#"\n\n%#",stringWithLineBreakChar);
You just need to set parameters required by getLinebreaksWithString and you will get a string with each "\n" break included.
OutPut
Here is my solution to this problem.
I iterate through all whitespace characters in the provided string, set substrings (from beginning to current whitespace) to my label and check the difference in the height. When the height changes I insert a new line character (simulates a line break).
func wordWrapFormattedStringFor(string: String) -> String {
// Save label state
let labelHidden = label.isHidden
let labelText = label.text
// Set text to current label and size it to fit the content
label.isHidden = true
label.text = string
label.sizeToFit()
label.layoutIfNeeded()
// Prepare array with indexes of all whitespace characters
let whitespaceIndexes: [String.Index] = {
let whitespacesSet = CharacterSet.whitespacesAndNewlines
var indexes: [String.Index] = []
string.unicodeScalars.enumerated().forEach { i, unicodeScalar in
if whitespacesSet.contains(unicodeScalar) {
let index = string.index(string.startIndex, offsetBy: i)
indexes.append(index)
}
}
// We want also the index after the last character so that when we make substrings later we include also the last word
// This index can only be used for substring to this index (not from or including, since it points to \0)
let index = string.index(string.startIndex, offsetBy: string.characters.count)
indexes.append(index)
return indexes
}()
var reformattedString = ""
let boundingSize = CGSize(width: label.bounds.width, height: CGFloat.greatestFiniteMagnitude)
let attributes = [NSFontAttributeName: font]
var runningHeight: CGFloat?
var previousIndex: String.Index?
whitespaceIndexes.forEach { index in
let string = string.substring(to: index)
let stringHeight = string.boundingRect(with: boundingSize, options: .usesLineFragmentOrigin, attributes: attributes, context: nil).height
var newString: String = {
if let previousIndex = previousIndex {
return string.substring(from: previousIndex)
} else {
return string
}
}()
if runningHeight == nil {
runningHeight = stringHeight
}
if runningHeight! < stringHeight {
// Check that we can create a new index with offset of 1 and that newString does not contain only a whitespace (for example if there are multiple consecutive whitespaces)
if newString.characters.count > 1 {
let splitIndex = newString.index(newString.startIndex, offsetBy: 1)
// Insert a new line between the whitespace and rest of the string
newString.insert("\n", at: splitIndex)
}
}
reformattedString += newString
runningHeight = stringHeight
previousIndex = index
}
// Restore label
label.text = labelText
label.isHidden = labelHidden
return reformattedString
}
Image - Comparison of output string with label text.
This solution worked really well in my case, hope it helps.