I am having difficulties with autoscaling with a c# WinForms chart. I thought by setting the Minimum and Maximum properties of ChartArea.AxisX it would take care of the scaling for me.
In the example below I show a column chart that tracks the number of accidents that occur in a workplace over a given year. Each month, an email will be sent out to the managers of the workplace showing them an updated chart with that month's entries added to it. When the first email is sent out, the columns are very large. Each month, as more workplace accidents occur, the columns of the chart begin to shrink.
In other words, if you make the fakeAccidentsData array have just one or two elements, then the columns are massive. If you have more elements in fakeAccidentsData then they are smaller. I want the columns in this chart to remain a fixed, narrow width so that as more elements are added the columns won't change in size. So there should be enough room to accomodate 365 columns (one for each day of the year). If 365 columns are too small to be seen then I am willing to change it to go by week instead (so I would need space for 52 columns in that case).
Can this be accomplished?
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms.DataVisualization.Charting;
namespace MinimalExample
{
class WorkplaceAccident
{
public DateTime Timestamp;
}
class ChartExample
{
private readonly Series _series;
private readonly ChartArea _chartArea;
private readonly Chart _chart;
public IEnumerable<WorkplaceAccident> Accidents
{
set
{
var accidentCount = new Dictionary<DateTime, int>();
foreach (var accident in value)
{
DateTime accidentDate = accident.Timestamp.Date;
if (accidentCount.ContainsKey(accidentDate))
{
++accidentCount[accidentDate];
}
else accidentCount.Add(accidentDate, 1);
}
foreach(KeyValuePair<DateTime, int> pair in accidentCount)
{
Console.WriteLine("[Debug info] Adding " + pair.Key + ", " + pair.Value);
_series.Points.AddXY(pair.Key, pair.Value);
}
}
}
public ChartExample(int year)
{
_series = new Series();
_series.ChartType = SeriesChartType.Column;
_chartArea = new ChartArea();
_chartArea.AxisY.Interval = 1;
_chartArea.AxisY.IsMarginVisible = false;
_chartArea.AxisY.Title = "Number of workplace accidents";
_chartArea.AxisX.MajorGrid.Enabled = false;
_chartArea.AxisX.Minimum = new DateTime(year, 1, 1).ToOADate();
_chartArea.AxisX.Maximum = new DateTime(year, 12, 31).ToOADate();
_chart = new Chart();
_chart.ChartAreas.Add(_chartArea);
_chart.Series.Add(_series);
_chart.Size = new Size(800, 400);
}
public void SaveAsPng(string filename)
{
_chart.SaveImage(filename, ChartImageFormat.Png);
}
public static void Main()
{
WorkplaceAccident[] fakeAccidentsData = new WorkplaceAccident[]
{
new WorkplaceAccident { Timestamp = DateTime.Now.AddDays(-1) },
new WorkplaceAccident { Timestamp = DateTime.Now.AddDays(-1) },
new WorkplaceAccident { Timestamp = DateTime.Now.AddDays(-5) },
new WorkplaceAccident { Timestamp = DateTime.Now.AddMonths(-1) }
};
var chart = new ChartExample(2020) { Accidents = fakeAccidentsData };
chart.SaveAsPng(#"C:\Users\Home\Documents\chart.png");
Console.Write("Press any key to terminate... "); Console.ReadKey();
}
}
}
Related
I have over 1,000 records and I am using this to find the highest value of (profit * volume).
In this case its "DEF" but then I have to open excel and sort by volume and find the range that produces the highest profit.. say excel column 200 to column 800 and then I'm left with say from volume 13450 to volume 85120 is the best range for profits.. how can I code something like that in C# so that I can stop using excel.
public class Stock {
public string StockSymbol { get; set; }
public double Profit { get; set; }
public int Volume { get; set; }
public Stock(string Symbol, double p, int v) {
StockSymbol = Symbol;
Profit = p;
Volume = v;
}
}
private ConcurrentDictionary<string, Stock> StockData = new();
private void button1_Click(object sender, EventArgs e) {
StockData["ABC"] = new Stock("ABC", 50, 14000);
StockData["DEF"] = new Stock("DEF", 50, 105000);
StockData["GHI"] = new Stock("GHI", -70, 123080);
StockData["JKL"] = new Stock("JKL", -70, 56500);
StockData["MNO"] = new Stock("MNO", 50, 23500);
var DictionaryItem = StockData.OrderByDescending((u) => u.Value.Profit * u.Value.Volume).First();
MessageBox.Show( DictionaryItem.Value.StockSymbol + " " + DictionaryItem.Value.Profit);
}
I wrote up something that may or may not meet your requirements. It uses random to seed a set of test data (you can ignore all of that).
private void GetStockRange()
{
var stocks = new Stock[200];
var stockChars = Enumerable.Range(0, 26).Select(n => ((char)n + 64).ToString()).ToArray();
var random = new Random(DateTime.Now.Millisecond);
for (int i = 0; i < stocks.Length; i++)
{
stocks[i] = new Stock(stockChars[random.Next(0, 26)], random.NextDouble() * random.Next(-250, 250), random.Next(1, 2000));
}
var highestPerformaceSelectionCount = 3;
var highestPerformanceIndices = stocks
.OrderByDescending(stock => stock.Performance)
.Take(Math.Max(2, highestPerformaceSelectionCount))
.Select(stock => Array.IndexOf(stocks, stock))
.OrderBy(i => i);
var startOfRange = highestPerformanceIndices.First();
var endOfRange = highestPerformanceIndices.Last();
var rangeCount = endOfRange - startOfRange + 1;
var stocksRange = stocks
.Skip(startOfRange)
.Take(rangeCount);
var totalPerformance = stocks.Sum(stock => stock.Performance);
var rangedPerformance = stocksRange.Sum(stock => stock.Performance);
MessageBox.Show(
"Range Start: " + startOfRange + "\r\n" +
"Range End: " + endOfRange + "\r\n" +
"Range Cnt: " + rangeCount + "\r\n" +
"Total P: " + totalPerformance + "\r\n" +
"Range P: " + rangedPerformance
);
}
The basics of this algorithm to get some of the highest performance points (configured using highestPerformanceSelectionCount, min of 2), and using those indices, construct a range which contains those items. Then take a sum of that range to get the total for that range.
Not sure if I am way off from your question. This may also not be the best way to handle the range. I wanted to post what I had before heading home.
I also added a Performance property to the stock class, which is simply Profit * Volume
EDIT
There is a mistake in the use of the selected indices. The indices selected should be used against the ordered set in order to produce correct ranged results.
Rather than taking the stocksRange from the original unsorted array, instead create the range from the ordered set.
var stocksRange = stocks
.OrderByDescending(stock => stock.Performance)
.Skip(startOfRange)
.Take(rangeCount);
The indices should be gathered from the ordered set as well. Caching the ordered set is probably the easiest route to go.
As is generally the case, there are any number of ways you can go about this.
First, your sorting code above (the OrderByDescending call). It does what you appear to want, more or less, in that it produces an ordered sequence of KeyValuePair<string, Stock> that you can then choose from. Personally I'd just have sorted StockData.Values to avoid all that .Value indirection. Once sorted you can take the top performer as you're doing, or use the .Take() method to grab the top n items:
var byPerformance = StockData.Values.OrderByDescending(s => s.Profit * s.Volume);
var topPerformer = byPerformance.First();
var top10 = byPerformance.Take(10).ToArray();
If you want to filter by a particular performance value or range then it helps to pre-calculate the number and do your filtering on that. Either store (or calculate) the Performance value in the class, calculate it in the class with a computed property, or tag the Stock records with a calculated performance using an intermediate type:
Store in the class
public class Stock {
// marking these 'init' makes them read-only after creation
public string StockSymbol { get; init; }
public double Profit { get; init; }
public int Volume { get; init; }
public double Performance { get; init; }
public Stock(string symbol, double profit, int volume)
{
StockSymbol = symbol;
Profit = profit;
Volume = volume;
Performance = profit * volume;
}
}
Calculate in class
public class Stock
{
public string StockSymbol { get; set; }
public double Profit { get; set; }
public int Volume { get; set; }
public double Performance => Profit * Volume;
// you know what the constructor looks like
}
Intermediate Type (with range filtering)
// let's look for those million-dollar items
var minPerformance = 1000000d;
var highPerformance = StockData.Values
// create a stream of intermediate types with the performance
.Select(s => new { Performance = s.Profit * s.Volume, Stock = s })
// sort them
.OrderByDescending(i => i.Performance)
// filter by our criteria
.Where(i => i.Performance >= minPerformance)
// pull out the stocks themselves
.Select(i => i.Value)
// and fix into an array so we don't have to do this repeatedly
.ToArray();
Ultimately though you'll probably end up looking for ways to store the data between runs, update the values and so forth. I strongly suggest looking at starting with a database and learning how to do things there. It's basically the same, you just end up with a lot more flexibility in the way you handle the data. The code to do the actual queries looks basically the same.
Once you have the data in your program, there are very few limits on how you can manipulate it. Anything you can do in Excel with the data, you can do in C#. Usually easily, sometimes with a little work.
LINQ (Language-Integrated Native Query) makes a lot of those manipulations trivial, with extensions for all sorts of things. You can take the average performance (with .Average()) and then filter on those that perform 10% above it with some simple math. If the data follows some sort of Normal Distribution you can add your own extension (or use this one) to figure out the standard deviation... and now we're doing statistics!
The basic concepts of LINQ, and the database languages it was roughly based on, give you plenty of expressive power. And Stack Overflow is full of people who can help you figure out how to get there.
try following :
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
List<Stock> stocks = null;
public Form1()
{
InitializeComponent();
stocks = new List<Stock>() {
new Stock("ABC", 50, 14000),
new Stock("DEF", 50, 105000),
new Stock("GHI", -70, 123080),
new Stock("JKL", -70, 56500),
new Stock("MNO", 50, 23500)
};
}
private void button1_Click(object sender, EventArgs e)
{
DataTable dt = Stock.GetTable(stocks);
dataGridView1.DataSource = dt;
}
}
public class Stock {
public string StockSymbol { get; set; }
public double Profit { get; set; }
public int Volume { get; set; }
public Stock(string Symbol, double p, int v) {
StockSymbol = Symbol;
Profit = p;
Volume = v;
}
public static DataTable GetTable(List<Stock> stocks)
{
DataTable dt = new DataTable();
dt.Columns.Add("Symbol", typeof(string));
dt.Columns.Add("Profit", typeof(int));
dt.Columns.Add("Volume", typeof(int));
dt.Columns.Add("Volume x Profit", typeof(int));
foreach(Stock stock in stocks)
{
dt.Rows.Add(new object[] { stock.StockSymbol, stock.Profit, stock.Volume, stock.Profit * stock.Volume });
}
dt = dt.AsEnumerable().OrderByDescending(x => x.Field<int>("Volume x Profit")).CopyToDataTable();
return dt;
}
}
}
I have a Chart (RangeBarChart) with a ChartArea attached to it. The ChartArea has one Series attached to it. I also have a List<string>, this stringlist is filled with the values I want the Axislabels on my AxisX to have. In my example screenshot, this stringlist contains 6 strings.
All the 6 labels are named correct, but at the upper and lower limits of my AxisX, the labels are numbered as well, see screenshot (upper part, '-1' and the '6' labels are undesired).
Screenshot link: https://imgur.com/a/pwYF4yl
I add data to my chart with two lines of code in a foreach loop. I found that when I comment out these two lines, the extra numbers on my axis don't appear, but obviously this is a non-solution, as I'm also not showing any data. I also can't delete the labels manually in code because I have no pointIndices pointing to them. When looking at my series.Points[] collection in the debugger, all their X-values are between 0 and 5 (also in screenshot).
How can I get rid of these labels?
I recreated the unwanted behaviour in a quick test project, just add a Chart called 'chart' in the designer and copy this code in the code part of your main form and you can recreate the problem for yourself.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace XAxisLabelsNumbersTest
{
public partial class Form1 : Form
{
ChartArea ca;
Series serie;
List<string> xLabels = new List<string> {"Label1", "Label2", "Label3", "Label4", "Label5", "Label6"};
List<myObj> myObjList = new List<myObj>();
public class myObj
{
public Int32 begin { get; set; }
public Int32 end { get; set; }
public int xcord { get; set; }
public int pointIndex { get; set; }
public string label { get; set; }
public myObj(Int32 begin, Int32 end, string label)
{
this.begin = begin;
this.end = end;
this.label = label;
}
}
public Form1()
{
InitializeComponent();
// Setting some properties regarding chart behaviour
ca = chart.ChartAreas.Add("ca");
serie = chart.Series.Add("serie");
serie.ChartArea = ca.Name;
serie.ChartType = SeriesChartType.RangeBar;
serie.XValueType = ChartValueType.String;
serie.YValueType = ChartValueType.Int32;
serie["PixelPointWidth"] = "10";
//ca.AxisY.LabelStyle.Format = "HH:mm tt";
ca.AxisX.MajorGrid.Interval = 1;
ca.AxisY.Minimum = 0;
ca.AxisY.Maximum = 6;
Title title = new Title("Title");
chart.Titles.Add(title);
title.DockedToChartArea = ca.Name;
title.IsDockedInsideChartArea = false;
title.Font = new Font("Serif", 18);
ca.AxisX.LabelAutoFitStyle = LabelAutoFitStyles.None;
ca.AxisX.LabelStyle.Font = new Font("Trebuchet MS", ca.AxisX.LabelAutoFitMaxFontSize, FontStyle.Bold);
ca.AxisX.LabelStyle.Interval = 1;
// Create Labels from xLabels
for (int i = 0; i < xLabels.Count; i++)
{
int pi = serie.Points.AddXY(i, null, null);
serie.Points[pi].AxisLabel = xLabels[i];
}
// Fill myObjList with testdata
myObjList.Add(new myObj(0, 1, "Label1"));
myObjList.Add(new myObj(1, 2, "Label2"));
myObjList.Add(new myObj(2, 3, "Label3"));
myObjList.Add(new myObj(3, 4, "Label4"));
myObjList.Add(new myObj(4, 5, "Label5"));
myObjList.Add(new myObj(5, 6, "Label6"));
// Fill serie with data from myObjList
// Comment out this foreach block and the weird label numbering is gone...
foreach (myObj myObj in myObjList)
{
myObj.xcord = xLabels.FindIndex(Label => Label.Equals(myObj.label));
myObj.pointIndex = serie.Points.AddXY(myObj.xcord, myObj.begin, myObj.end);
}
}
}
}
You can hide both 'endlabels' by setting this axis property: Axis.LabelStyle.IsEndLabelVisible:
ca.Axis.LabelStyle.IsEndLabelVisible = false;
I'm having trouble grasping the use of GTK's TreeView and so I am running through some examples I have found online. Putting them together I am unable to work out how to extract the information in the view. I am operating the TreeView as a simple List with columns for the presentation of tabled data.
I am getting confused between the roles of models, stores and the actual treeview itself.
I am working from the example here
using System;
using System.Collections;
using Gtk;
public class Actress
{
public string Name;
public string Place;
public int Year;
public Actress(string name, string place, int year)
{
Name = name;
Place = place;
Year = year;
}
}
public class SharpApp : Window
{
ListStore store;
Statusbar statusbar;
enum Column
{
Name,
Place,
Year
}
Actress[] actresses =
{
new Actress("Jessica Alba", "Pomona", 1981),
new Actress("Sigourney Weaver", "New York", 1949),
new Actress("Angelina Jolie", "Los Angeles", 1975),
new Actress("Natalie Portman", "Jerusalem", 1981),
new Actress("Rachel Weissz", "London", 1971),
new Actress("Scarlett Johansson", "New York", 1984)
};
public SharpApp() : base ("ListView")
{
BorderWidth = 8;
SetDefaultSize(350, 250);
SetPosition(WindowPosition.Center);
DeleteEvent += delegate { Application.Quit(); };
VBox vbox = new VBox(false, 8);
ScrolledWindow sw = new ScrolledWindow();
sw.ShadowType = ShadowType.EtchedIn;
sw.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
vbox.PackStart(sw, true, true, 0);
store = CreateModel();
TreeView treeView = new TreeView(store);
treeView.RulesHint = true;
treeView.RowActivated += OnRowActivated;
sw.Add(treeView);
AddColumns(treeView);
statusbar = new Statusbar();
vbox.PackStart(statusbar, false, false, 0);
Add(vbox);
ShowAll();
}
void OnRowActivated (object sender, RowActivatedArgs args) {
TreeIter iter;
TreeView view = (TreeView) sender;
if (view.Model.GetIter(out iter, args.Path)) {
string row = (string) view.Model.GetValue(iter, (int) Column.Name );
row += ", " + (string) view.Model.GetValue(iter, (int) Column.Place );
row += ", " + view.Model.GetValue(iter, (int) Column.Year );
statusbar.Push(0, row);
}
// *** if I can dump treeview to the console here I'll be happy ***
// *** I'd prefer a foreach or do/while ***
}
void AddColumns(TreeView treeView)
{
CellRendererText rendererText = new CellRendererText();
TreeViewColumn column = new TreeViewColumn("Name", rendererText,
"text", Column.Name);
column.SortColumnId = (int) Column.Name;
treeView.AppendColumn(column);
rendererText = new CellRendererText();
column = new TreeViewColumn("Place", rendererText,
"text", Column.Place);
column.SortColumnId = (int) Column.Place;
treeView.AppendColumn(column);
rendererText = new CellRendererText();
column = new TreeViewColumn("Year", rendererText,
"text", Column.Year);
column.SortColumnId = (int) Column.Year;
treeView.AppendColumn(column);
}
ListStore CreateModel()
{
ListStore store = new ListStore( typeof(string),
typeof(string), typeof(int) );
foreach (Actress act in actresses) {
store.AppendValues(act.Name, act.Place, act.Year );
}
return store;
}
public static void Main()
{
Application.Init();
new SharpApp();
Application.Run();
}
}
An array of actresses is created at the top of the code, and that is used to populate a ListStore (towards the end of the code) that is in turn used to populate the TreeView.
I want to iterate over the treeview's data and update the columns within it.
To this end, since OnRowActivated is a nice place to dump information, I'd like to dump out the entire treeview contents to the console when a node is selected.
Any help in guiding me to get this done (and hopefully understanding it in the process) would be appreciated.
Whilst certainly not elegant I found the answer which also updates some of the data in the model. I'd be happy if anyone could improve upon this.
TreeIter iter;
TreeModel model = tv.Model;
if (model.GetIterFirst(out iter))
{
do
{
newYear = ModifyYear((string)model.GetValue(iter, (int)Column.Year));
Console.WriteLine("Updating: {0} -- {1} {2}",
(string)model.GetValue(iter, (int)Column.Name),
model.GetValue(iter, (int)Column.Year),
newYear );
model.SetValue(iter, (int)Column.Year, (int)newYear);
} while (model.IterNext(ref iter));
}
private int SumOfNodes()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> SumOfHardware = t.GetHardware().FindAll(x => x.Nodes != 0);
int SumOfNodes = 0;
foreach (Hardware i in SumOfHardware )
{
SumOfNodes += i.Nodes;
}
return SumOfNodes;
}
private int SumOfRepeaters()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> SumOfHardware = t.GetHardware().FindAll(x => x.Repeaters != 0);
int SumOfRepeaters = 0;
foreach (Hardware i in SumOfHardware)
{
SumOfRepeaters += i.Repeaters;
}
return SumOfRepeaters;
}
private int SumOfHubs()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> SumOfHardware = t.GetHardware().FindAll(x => x.Hubs != 0);
int SumOfHubs= 0;
foreach (Hardware i in SumOfHardware)
{
SumOfHubs += i.Hubs;
}
return SumOfHubs;
}
private string Month()
{
DateTime now = DateTime.Now;
string month = DateTime.Now.ToString("MMMM");
return month;
}
private void DisplayData()
{
SeriesCollection = new SeriesCollection
{
new ColumnSeries
{
Title = "Nodes",
Values = new ChartValues<int> { SumOfNodes() }
},
};
SeriesCollection.Add
(
new ColumnSeries
{
Title = "Repeaters",
Values = new ChartValues<int> { SumOfRepeaters() }
}
);
SeriesCollection.Add
(
new ColumnSeries
{
Title = "Hubs",
Values = new ChartValues<int> { SumOfHubs() }
}
);
Labels = new[] { Month() };
Formatter = value => value.ToString("N");
DataContext = this;
}
enter image description here
At this points I've managed to create an app that adds/removes and updates my items on my database. I'm also planning to add some stats (Started off with graph visualisation) but I'm facing an issue.
I want to seperate columns based on months. So for example as seen by the image attached no matter how many items i add , remove or update the total amount for each item is added to Decemeber. But when January comes any newly added modification to the quantity of my items I would like to see adjacent to the Decemeber one.
P.S.: There is alot of code repitition which will accounted for later on.
I've managed to put something together regarding my answer above which may be usefull for someone in the future
What I've done was to
1) Get my data from my db.
2) Find the Month of interest using LINQ queries **
instead of selecting the items (i.e repeaters, nodes)
**
3) By doing so it also accounts for the items I'm interested in during the month of interest.
4) Do an incremental foreach loop as shown in my code above.
5) Change the methodd i want to display (i.e DisplayData to DisplayDecemberData) and use in the same way as above.
6) Create further methods for the subsequent months and just add the additional information in the ChartValues object.
See Below
private int DecemberNodes()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> decembernodes = t.GetHardware().FindAll(x => x.Date.Month==12);
int DecemberSumOfNodes = 0;
foreach (Hardware i in decembernodes)
{
DecemberSumOfNodes += i.Nodes;
}
return DecemberSumOfNodes;
}
private int JanuaryNodes()
{
ManufacturingDataModel MDM = new ManufacturingDataModel();
Test t = new Test(MDM);
List<Hardware> januarynodes = t.GetHardware().FindAll(x => x.Date.Month==01);
int JanuarySumOfNodes = 0;
foreach (Hardware i in januarynodes)
{
JanuarySumOfNodes += i.Nodes;
}
private void DisplayDecemberData()
{
SeriesCollection = new SeriesCollection
{
new ColumnSeries
{
Title = "Nodes",
Values = new ChartValues<int> { DecemberNodes() }
},
};
private void DisplayJanuaryData()
{
SeriesCollection = new SeriesCollection
{
new ColumnSeries
{
Title = "Nodes",
**Values = new ChartValues<int> { DecemberNodes(), JanuaryNodes() }**
},
};
There is some code repetition and I'm pretty sure there is a more code concise way of actually doing it but for now this seems to do the trick. When I simplify the code i will post it.
We have a function that is informed that we received an item for a specific timestamp.
The purpose of this is to wait that for one specific timestamp, we wait that we receive every item that we are expecting, then push the notification further once we are "synchronized" with all items.
Currently, we have a Dictionary<DateTime, TimeSlot> to store the non-synchronized TimeSlot(TimeSlot = list of all items we received for a specific timestamp).
//Let's assume that this method is not called concurrently, and only once per "MyItem"
public void HandleItemReceived(DateTime timestamp, MyItem item){
TimeSlot slot;
//_pendingTimeSlot is a Dictionary<DateTime,TimeSlot>
if(!_pendingTimeSlot.TryGetValue(timestamp, out slot)){
slot = new TimeSlot(timestamp);
_pendingTimeSlot.Add(timestamp,slot );
//Sometimes we don't receive all the items for one timestamps, which may leads to some ghost-incomplete TimeSlot
if(_pendingTimeSlot.Count>_capacity){
TimeSlot oldestTimeSlot = _pendingTimeSlot.OrderBy(t=>t.Key).Select(t=>t.Value).First();
_pendingTimeSlot.Remove(oldestTimeSlot.TimeStamp);
//Additional work here to log/handle this case
}
}
slot.HandleItemReceived(item);
if(slot.IsComplete){
PushTimeSlotSyncronized(slot);
_pendingTimeSlot.Remove(slot.TimeStamp);
}
}
We have severals instances of this "Synchronizer" in parallels for differents group of items.
It's working fine, except when the system is under heavy loads, we have more incomplete TimeSlot, and the application uses a lot more CPU. The profiler seems to indicate that the Compare of the LINQ query is taking a lot of time(most of the time). So I'm trying to find some structure to hold those references(replace the dictionary)
Here are some metrics:
We have several(variable, but between 10 to 20) instances of this Synchronizer
The current maximum capacity(_capacity) of the synchronizer is 500 items
The shortest interval that we can have between two different timestamp is 100ms(so 10 new Dictionary entry per seconds for each Synchronizer)(most case are more 1 item/second)
For each timestamp, we expect to receive 300-500 items.
So we will do, for one Synchronizer, per second(worst case):
1 Add
500 Get
3-5 Sorts
What would be my best move? I thought to the SortedDictionary But I didn't find any documentation showing me how to take the first element according to the key.
The first thing you can try is eliminating the OrderBy - all you need is the minimum key, no need to sort for getting that:
if (_pendingTimeSlot.Count > _capacity) {
// No Enumerable.Min(DateTime), so doing it manually
var oldestTimeStamp = DateTime.MaxValue;
foreach (var key in _pendingTimeSlot.Keys)
if (oldestTimeStamp > key) oldestTimestamp = key;
_pendingTimeSlot.Remove(oldestTimeStamp);
//Additional work here to log/handle this case
}
What about SortedDictionary, it is an option for sure, although it will consume much more memory. Since it's sorted, you can use simply sortedDictionary.First() to take the key value pair with the minimum key (hence the oldest element in your case).
UPDATE: Here is a hybrid approach using dictionary for fast lookups and ordered double linked list for the other scenarios.
class MyItem
{
// ...
}
class TimeSlot
{
public readonly DateTime TimeStamp;
public TimeSlot(DateTime timeStamp)
{
TimeStamp = timeStamp;
// ...
}
public bool IsComplete = false;
public void HandleItemReceived(MyItem item)
{
// ...
}
// Dedicated members
public TimeSlot PrevPending, NextPending;
}
class Synhronizer
{
const int _capacity = 500;
Dictionary<DateTime, TimeSlot> pendingSlotMap = new Dictionary<DateTime, TimeSlot>(_capacity + 1);
TimeSlot firstPending, lastPending;
//Let's assume that this method is not called concurrently, and only once per "MyItem"
public void HandleItemReceived(DateTime timeStamp, MyItem item)
{
TimeSlot slot;
if (!pendingSlotMap.TryGetValue(timeStamp, out slot))
{
slot = new TimeSlot(timeStamp);
Add(slot);
//Sometimes we don't receive all the items for one timestamps, which may leads to some ghost-incomplete TimeSlot
if (pendingSlotMap.Count > _capacity)
{
// Remove the oldest, which in this case is the first
var oldestSlot = firstPending;
Remove(oldestSlot);
//Additional work here to log/handle this case
}
}
slot.HandleItemReceived(item);
if (slot.IsComplete)
{
PushTimeSlotSyncronized(slot);
Remove(slot);
}
}
void Add(TimeSlot slot)
{
pendingSlotMap.Add(slot.TimeStamp, slot);
// Starting from the end, search for a first slot having TimeStamp < slot.TimeStamp
// If the TimeStamps almost come in order, this is O(1) op.
var after = lastPending;
while (after != null && after.TimeStamp > slot.TimeStamp)
after = after.PrevPending;
// Insert the new slot after the found one (if any).
if (after != null)
{
slot.PrevPending = after;
slot.NextPending = after.NextPending;
after.NextPending = slot;
if (slot.NextPending == null) lastPending = slot;
}
else
{
if (firstPending == null)
firstPending = lastPending = slot;
else
{
slot.NextPending = firstPending;
firstPending.PrevPending = slot;
firstPending = slot;
}
}
}
void Remove(TimeSlot slot)
{
pendingSlotMap.Remove(slot.TimeStamp);
if (slot.NextPending != null)
slot.NextPending.PrevPending = slot.PrevPending;
else
lastPending = slot.PrevPending;
if (slot.PrevPending != null)
slot.PrevPending.NextPending = slot.NextPending;
else
firstPending = slot;
slot.PrevPending = slot.NextPending = null;
}
void PushTimeSlotSyncronized(TimeSlot slot)
{
// ...
}
}
Some additional usages:
Iterating from oldest to newest:
for (var slot = firstPending; slot != null; slot = slot.NextPending)
{
// do something
}
Iterating from oldest to newest and removing items based on a criteria:
for (TimeSlot slot = firstPending, nextSlot; slot != null; slot = nextSlot)
{
nextSlot = slot.NextPending;
if (ShouldRemove(slot))
Remove(slot);
}
Same for reverse scenarios, but using lastPending and PrevPending members instead.
Here is simple sample. The insert method in a list eliminates swapping elements.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
List<Data> inputs = new List<Data>() {
new Data() { date = DateTime.Parse("10/22/15 6:00AM"), data = "abc"},
new Data() { date = DateTime.Parse("10/22/15 4:00AM"), data = "def"},
new Data() { date = DateTime.Parse("10/22/15 6:30AM"), data = "ghi"},
new Data() { date = DateTime.Parse("10/22/15 12:00AM"), data = "jkl"},
new Data() { date = DateTime.Parse("10/22/15 3:00AM"), data = "mno"},
new Data() { date = DateTime.Parse("10/22/15 2:00AM"), data = "pqr"},
};
Data data = new Data();
foreach (Data input in inputs)
{
data.Add(input);
}
}
}
public class Data
{
public static List<Data> sortedData = new List<Data>();
public DateTime date { get; set; }
public string data { get; set;}
public void Add(Data newData)
{
if(sortedData.Count == 0)
{
sortedData.Add(newData);
}
else
{
Boolean added = false;
for(int index = sortedData.Count - 1; index >= 0; index--)
{
if(newData.date > sortedData[index].date)
{
sortedData.Insert(index + 1, newData);
added = true;
break;
}
}
if (added == false)
{
sortedData.Insert(0, newData);
}
}
}
}
}