How to align indexed Series in a Chart - c#

When setting a Series to be indexed by setting the Series.IsXValueIndexed to true the chart requires all Series to be aligned:
If you are displaying multiple series and at least one series uses
indexed X-values, then all series must be aligned — that is, have the
same number of data points—and the corresponding points must have the
same X-values.
How can you add the necessary Emtpy DataPoints to the slots they are missing in, in any of the Series?

This routine first collects all values in all Series in a collection of doubles.
Then it loops over all Series and over all values and inserts the missing empty DataPoints:
void AlignSeries(Chart chart)
{
var allValues = chart.Series.SelectMany(s => s.Points)
.Select(x=>x.XValue).Distinct().ToList();
foreach (Series series in chart.Series)
{
int px = 0; //insertion index
foreach(double d in allValues )
{
var p = series.Points.FirstOrDefault(x=> x.XValue == d);
if (p == null) // this value is missing
{
DataPoint dp = new DataPoint(d, double.NaN);
dp.IsEmpty = true;
series.Points.Insert(px, dp);
}
px++;
}
}
}
Note that the code assumes ..
that your x-values are correctly set, i.e. they were added as numbers or DateTimes. If you added them as strings they all are 0 and indexing makes no sense.
that the DataPoints were added in ascending order. This is not always the case, especially when plotting LineCharts. However indexing these makes no sense either.
Also note that you can set several options of how to treat Empty DataPoints in a Series by setting properties in the Series.EmptyPointStyle, which is derived from DataPointCustomProperties.
So you could set their Color like this:
someSeries.EmptyPointStyle.Color = Color.Red;

Related

chart control - string on x axis

How do I show string values on the x axis of a chart control ? the datapoints (x & y) are both double. I have an IEnumerable containing a list of objects with 2 properties. The name of a report and an integer value indicating how many times it was run. so the integer is on the y axis and the report name is on the x axis. But I only seem to have double options for the value types. So when I create my datapoint, Im geting an error
DataPoint p1 = new DataPoint();
p1.XValue = log.ReportName; ---> this is invalid
p1.YValues = new Double[] { log.ReportTotal };
how can this be done ?
Ive tried this
reportTotal.XValueType = ChartValueType.String;
and when I plot my datapoints, Ive done this
int i = 1;
foreach (var log in this._reportLogs)
{
DataPoint p1 = new DataPoint();
p1.XValue = (double)i;
p1.YValues = new Double[] { log.ReportFrequency };
i++;
reportTotal.Points.Add(p1);
}
but now all im getting along the x axis are the number of the i varaible, how on earth do I just get the content of the text property ?
this is how I made it work
Dictionary<string, int> ReportLogs = new Dictionary<string, int>();
foreach (var log in this._reportLogs)
ReportLogs.Add(log.ReportName, log.ReportFrequency);
foreach (KeyValuePair<string, int> log in ReportLogs)
reportTotal.Points.AddXY(log.Key, log.Value);
The labels along the axis are created from the x-values and their number is determined either automatically as they fit or by setting the Interval property of the x-axis.
So their placement will not necessarily coincide with the actual DataPoints.
And you can format them but only within the normal formatting rules, ie you can't get creative with expressions to convert a number to a custom string, which seems to be just what you want: show a report name according to its number.
Other examples could be show a city from its zip code or a person name acording to emp.ID..
To gain full control you may need to use CustomLabels. They are a little bit tricky but quite useful. See several of these posts for numerous examples!
Also note: If you change the x-value type to string (which is possible : yourseries.XValueType = ChartValueType.String) these strings would show directly but you would loose the actual x-values! Usually not recommended (but possibly fine in your case)!
So if you want to can do it; however you can't prepare the DataPoint like you did. Instead you need to use the Points.AddXY method.
This will convert the strings to double (resulting in zeroes) and copy the string into the labels..:
reportTotal.Points.AddXY(log.ReportName, new Double[] { log.ReportTotal });
Update
Another way would be to set the AxisLabel of each DataPoint. For all of these to show up you may need to set the yourxaxis.Interval = 1.
So in your loop simply add
p1.AxisLabel = log.ReportName;

How to add data to BoxPlot in WFA?

I'm creating manually a boxplot chart. I have 4 double[] arrays with some calculations results that i want show on chart. I don't know how to connect correctly my arrays with chart Series.
Here is my chart:
Chart chart = new Chart();
chart.Series.Add("S1");
chart.Series.Add("S2");
chart.Series.Add("S3");
chart.Series.Add("S4");
chart.ChartAreas.Add("ChartArea1");
chart.ChartAreas[0].Visible = true;
chart.ChartAreas[0].Position.Auto = true;
chart.Series[0].ChartType = SeriesChartType.BoxPlot;
chart.Series[1].ChartType = SeriesChartType.BoxPlot;
chart.Series[2].ChartType = SeriesChartType.BoxPlot;
chart.Series[3].ChartType = SeriesChartType.BoxPlot;
chart.Parent = this;
chart.Visible = true;
double[] yValues = { 2, 3, 4, 5, 4, 5, 5, 2, 1, 9, 20, 4 };//example values
chart.Series["S1"].Points.DataBindY(yValues);
This is what i get:
As result i want get something like that:
You have tried to bind your data to a BoxPlot series. But this has only resulted in binding the first Y-value, which means you have created a set of 'lower whiskers'. All other 5 Y-values are empty, ie are 0. Hence the meager graphics you see..
MSDN: The values for a box are typically calculated values from
data that is present in another series. One box symbol (DataPoint
object) is associated with one data series.
The data for a Box Plot series may still be populated using
data binding, or by employing the Series.Points member (a
DataPointCollection object).
Let's look at all of these options:
Use regular data binding. This is what you tried but with a wrong syntax.
You can also add the DataPoints of the box plot series itself one by one with AddY or AddXY, supplying all 6 Y-Values, i.e. feeding in the results of a statistical analysis. Here only one array is used and it contains the six y-values.
Or you can use one or more data series and let the chart summarize those data in one box per series. The series are quite normal, read may be Point, Line or whatever.. They can even be invisible and of course you can use data binding for them.. - Once some data series are in place you can define the BoxPlot series and 'bind' it to the data series by seting its ["BoxPlotSeries"] special property to a string into which you concatenate the Series' Names...
Option 1. Use regular data binding to feed in the stats you have.
This is what you tried. The correct way is a little surprising, though; your data need to be ordered like this:
The outer array (or IEnumerable) must have the six y-values; the six inner arrays should contain one value for each of the data sets you want to show in a box. Let's look at an example with three data sets of faked stats:
double[][] yValues = {
new[]{ 15.6, 24.4, 36.1 }, // Lower whiskers
new[]{ 46.2, 52.2, 91.9 }, // Upper whiskers
new[]{ 22.3, 27.2, 55.9 }, // Lower boxes
new[]{ 33.2, 44.4, 77.9 }, // Upper boxes
new[]{ 25.2, 38.4, 68.5 }, // Averages and means
new[]{ 27.4, 32.4, 66.9 } // Medians
};
This is how it looks after binding it to a BoxPlot series S1
S1.Points.DataBindY(yValues);
You can also create individual series; see the update at the bottom for this!
Option 2: Feed in the BoxPlot data yourself
Let's look at an example of the first way: Here we need to have the statistics ready.. First I create a random data structure; it is a List of double arrays each with 6 elements:
Random R = new Random(23);
List<double[]> yValues = new List<double[]>();
for (int i = 0; i < 8; i++)
{
{ R.Next(5), R.Next(5) + 20, R.Next(5) + 3,
R.Next(5) + 10, R.Next(5) + 5, R.Next(5) + 7 });
}
Now we add those fake statistics to the BoxPlot series:
S1.ChartType = SeriesChartType.BoxPlot;
S1.Points.Clear();
for (int i = 0; i < yValues.Count; i++ ) S1.Points.Add(new DataPoint(i, yValues[i]));
Note that each DataPoint is created from an array of 6 doubles!
Here is the result:
The chart now shows stats on 8 data sets, all faked ;-)
Option 3a: Associate some data series with the BoxPlot and let it do the math
The other usage is to let the chart to the math: It can calculate the statistics for any number of data series you have and create one box for each.
We will use the same data set as before, but now they are used to create 6 data series, each with 8 points:
for (int i = 0; i < 6; i++)
{
Series ds = chart.Series.Add("D" + (i+1)); // set a name D1, D2..
dx.ChartType = SeriesChartType.Line;
dx.Points.DataBindY(yValues.Select(x => x[i]).ToArray());
}
They are bound to the same numbers as above, but these now have a different meaning. Therefore the results will and should not look alike!!
In fact you can look at the boxes and graphs and see how they nicely fit.
Note the Names I gave the data series; we will need them now, when we 'bind' them to the BoxPlot series:
S1.ChartType = SeriesChartType.BoxPlot;
S1["BoxPlotSeries"] = "D1;D2;D3;D4;D5;D6"; // box plot binding!
S1.LegendText = "BoxPlot";
I use quotes for the 'binding' because it isn't real data binding; instead the data series are only associated with the BoxPlot series with the sprecial property string "BoxPlotSeries".
Your example has more than one BoxPlot series; here the same rules apply.
Do have a look at this post which shows another use of BoxPlot including setting individual colors..
Option 3b: Associate some data series with DataPoints you add to the BoxPlot series; here too it will do the math for us
While option 3a seems rather simple I found no way to color the boxes. Here is how we can do that:
First we force the chart to copy the default colors to the series. While we're at it, let's also hide the Legend item:
S1.Color = Color.Transparent;
S1.LegendText = " ";
chart.ApplyPaletteColors()
Then we create one DataPoint in out BoxPlot series S1 for each data series; not how my series are ordered: 0=The BoxPlot, 1-6 some data series! You may need to adapt this!
for (int i = 1; i < chart.Series.Count; i++)
{
DataPoint dp = new DataPoint();
S1.Points.Add(dp);
dp["BoxPlotSeries"] = "D" + i; // names D1-D6
dp.Color = chart.Series[i].Color;
}
Here is the result:
Update:
Your example actually shows three boxplot series; hence the colors and most notably: the clustered (i.e. gapless) display.
This would be a way to bind the above data (from option 1) to three individual boxes:
for (int i = 0; i < 3; i++)
{
Series bps = chart.Series.Add("BoxPlotSeries" + i);
bps.ChartType = SeriesChartType.BoxPlot;
var yValOne = yValues.Select(x => new[] { x[i] }).ToArray();
bps.Points.DataBindY(yValOne);
}
Final note: Your example code contains an array with 12 doubles and also 4 boxplot series. This makes no sense. You can add the 12 values to a normal series and associate it with one boxplot series using option 3, though..

Creating a sudoku. Should I use a while statement for this code?

I'm making a sudoku in Windows Form Application.
I have 81 textboxes and I have named them all textBox1a, textBox1b... textBox2a, textBox2b...
I want to make it so that if any of the textboxes, in any of the rows, is equal to any other textbox in the same row, then both will get the background color red while the textboxes are equal.
I tried using this code just for test:
private void textBox1a_TextChanged_1(object sender, EventArgs e)
{
while (textBox1a.Text == textBox1b.Text)
{
textBox1a.BackColor = System.Drawing.Color.Red;
textBox1b.BackColor = System.Drawing.Color.Red;
}
It didn't work, and I don't know where I should put all this code, I know I shouldn't have it in the textboxes.
Should I use a code similar to this or is it totally wrong?
You want to iterate over the collection of text boxes just once, comparing it to those that haven't yet been compared against. If you have your textboxes in an array (let's call it textBoxes), and know which one was just changed (e.g. from the textChanged handler), you could do:
void highlightDuplicates(int i) // i is the index of the box that was changed
{
int iVal = textBoxes[i].Text;
for (int j = 0; j < 82; j++)
{
// don't compare to self
if (i == j) return;
if (textBoxes[j].Text == iVal)
{
textBoxes[i].BackgroundColor = System.Drawing.Color.Red;
textBoxes[j].BackgroundColor = System.Drawing.Color.Red;
}
}
}
If you wanted to get fancier, you could put your data in something like: Dictionary<int, TextBox>, where the key is the value and the TextBox is a reference to the text box with that value. Then you can quickly test for duplicate values with Dictionary.Contains() and color the matching text box by getting its value.
I think your current code would result in an infinite loop. The textboxes' values can't change while you are still in the event handler, so that loop would never exit.
If all of your boxes are named according to one convention, you could do something like this. More than one input can use the same handler, so you can just assign this handler to all the boxes.
The following code is not tested and may contain errors
private void textBox_TextChanged(object sender, EventArgs e){
var thisBox = sender as TextBox;
//given name like "textBox1a"
var boxNumber = thisBox.Name.SubString(7,1);
var boxLetter = thisBox.Name.SubString(8,1);
//numbers (horizontal?)
for(int i = 1; i<=9; i++){
if(i.ToString() == boxNumber)
continue; //don't compare to self
var otherBox = Page.FindControl("textBox" + i + boxLetter) as TextBox;
if (otherBox.Text == thisBox.Text)
{
thisBox.BackColor = System.Drawing.Color.Red;
otherBox.BackColor = System.Drawing.Color.Red;
}
}
//letters (vertical?)
for(int i = 1; i<=9; i++){
var j = ConvertNumberToLetter(i); //up to you how to do this
if(j == boxLetter)
continue; //don't compare to self
var otherBox = Page.FindControl("textBox" + boxNumber + j) as TextBox;
if (otherBox.Text == thisBox.Text)
{
thisBox.BackColor = System.Drawing.Color.Red;
otherBox.BackColor = System.Drawing.Color.Red;
}
}
}
I believe you will be more effective if create an Array (or a List) of Integers and compare them in memory, against compare them in UI (User Interface).
For instance, you could:
1) Create an Array of 81 integers.
2) Everytime the user input a new number, you search for it in that Array. If found, set the textbox as RED, otherwise, add the new value to that array.
3) The ENTER event may be allocated fot the entire Textboxes (utilize the Handles keyword with all Textboxes; like handles Text1.enter, Text2.enter, Text3.enter ... and so forth)
Something like:
int[] NumbersByUser = new int[81];
Private Sub Textbox1.Enter(sender as object, e as EventArgs) handles Textbox1.Enter, Textbox2.Enter, Textbox3.enter ...
int UserEntry = Convert.ToInt32(Sender.text);
int ValorSelecionado = Array.Find(NumbersByUser, Z => (Z == UserEntry));
if (ValorSelecionado > 0) {
Sender.forecolor = Red;
}
else
{
NumbersByUser(Index) = UserEntry;
}
You should have a 2 dimensional array of numbers (could be one dimensional, but 2 makes more sense) let's assume its called Values. I suggest that you have each textbox have a incrementing number (starting top left, going right, then next row). Now you can do the following:
All TextBox Changed events can point to the same function. The function then takes the tag to figure out the position in the 2dim array. (X coordinate is TAG % 9 and Y coordinate is TAG / 9)
In the callback you can loop over the textboxes and colorize all boxes as you like. First do the "check row" loop (pseudo code)
var currentTextBox = ((TextBox)sender)
var x = ((int)currentTextBox.Tag) % 9
var y = ((int)currentTextBox.Tag) / 9
// First assign the current value to the backing store
Values[currentTextBox] = int.parse(currentTextBox.Text)
// assuming variable x holding the column and y holding the row of current box
// Array to hold the status of a number (is it already used?)
bool isUsed[9] = {false, false, ...}
for(int col = 0; col <= 9; i++)
{
// do not compare with self
if(col == x) continue;
isUsed[textBox] = true;
}
// now we have the status of all other boxes
if( isUsed[Values[x,y]] ) currentTextBox.Background = Red else currentTextBox.Background = Green
// now repeat the procedure for the column iterating the rows and for the blocks
I would suggest a dynamic approach to this. Consider each board item as a cell (this would be it's own class). The class would contain a numeric value and other properties that could be useful (i.e. a list of possible values).
You would then create 3 collections of the cells, these would be:
A collection of rows of 9 cells (for tracking each row)
A collection of columns of 9 cells (for tracking each column)
A collection of 3x3 cells
These collections would share references - each cell object would appear once in each collection. Each cell could also contain a reference to each of the 3 collections.
Now, when a cell value is changed, you can get references to each of the 3 collections and then apply a standard set of Sudoku logic against any of those collections.
You then have some display logic that can walk the boards of cells and output to the display (your View) your values.
Enjoy - this is a fun project.

Aligning range column data with x axis labels in chart control

I'm trying to do a range column plot of a set of agents' tasks using the Chart control in C# .NET. I plot agent number across the x axis and task time along the y axis. My only problem is that the column data will not align properly with the agent numbers on the x axis. Does anyone know how to align the columns with their corresponding x axis labels?
Here is an image of my graph:
Here is my code:
chartSchedule.Titles.Add("Agent / Task Schedule");
chartSchedule.ChartAreas[0].AxisX.Title = "Agent";
chartSchedule.ChartAreas[0].AxisY.Title = "Time";
int index = 0;
foreach ( Agent a in _agents )
{
// Create a series for each agent and set up display details
Series agentSeries = chartSchedule.Series.Add("Agent " + a.Id);
agentSeries.ChartType = SeriesChartType.RangeColumn;
// Alternate colours of series lines
if ( index % 2 > 0 )
agentSeries.Color = Color.DodgerBlue;
else
agentSeries.Color = Color.Blue;
// Display start and end columns of every task
List<DataPoint> timeData = new List<DataPoint>();
foreach ( NodeTask t in a.AssignedTasks )
{
agentSeries.Points.AddXY(index + 1, t.StartTime, t.EndTime);
}
index++;
}
The reason for the seeming 'misalignment' is that you are adding a total of five series but each has only one (set of) Datapoint(s) per X-Value.
This existent DataPoint is then combined wih the the four non-existent DataPoints and the five of them are displayed side by side as one block centered at the X-Values/Labels. This looks only right for the middle Series, which actually has the middle point.
You could add a few the other Points to see the effect..:
agentSeries.Points.AddXY(1, 1, 4);
agentSeries.Points.AddXY(2, 1, 2);
agentSeries.Points.AddXY(4, 1, 3);
So the most natural solution is to not add Series with missing data.
Not sure if you are happy with this solution or if there is a better way to do it, but the result looks not so bad..
I have done away with adding all those series and instead add all data to one and same Series.
To create the Legends I hide the regular one by setting its color to transparent. (It needs to be there.) Then I add new Legend CustomItems and give them the colors and names as you did.
Here is the code I used, except for the actual data, which I have simulated:
chartSchedule.Series.Clear();
ChartArea CA = chartSchedule.ChartAreas[0];
chartSchedule.Titles.Add("Agent / Task Schedule");
chartSchedule.ChartAreas[0].AxisX.Title = "Agent";
chartSchedule.ChartAreas[0].AxisY.Title = "Time";
// our only Series
Series agentSeries = chartSchedule.Series.Add(" " );
agentSeries.ChartType = SeriesChartType.RangeColumn;
agentSeries.Color = Color.Transparent; // hide the default series entry!
agentSeries["PixelPointWidth"] = "20"; // <- your choice of width!
int index = 0;
foreach (Agent a in _agents)
{
// Alternate colours
Color color = index % 2 == 0 ? Color.DodgerBlue : Color.Blue;
// Display start and end columns of every task
List<DataPoint> timeData = new List<DataPoint>(); ///???
foreach (NodeTask t in a.AssignedTasks)
{
int p = agentSeries.Points.AddXY(index +1, t.StartTime, t.EndTime);
agentSeries.Points[p].Color = color;
}
chartSchedule.Legends[0].CustomItems.Add(color, "Agent " + index);
index++;
}

How to filter or drop a value based on the previous one using Deedle in C#?

I am dealing with data from sensors. Sometimes these sensors have blackouts and brownouts, in consequence I can have the following kind of Time Series in a Frame, let's call it "myData":
[7.438984; 0,000002; 7.512345; 0.000000; 7.634912; 0.005123; 7.845627...]
Because I need only 3 decimals precision, I rounded the data from the frame:
var myRoundedData = myData.ColumnApply((Series<DateTime, double> numbers) => numbers.Select(kvp => Math.Round(kvp.Value, 3)));
I get the columns from the frame and filtered the Zeros "0.000":
var myFilteredTimeSeries = from kvp in myTimeSeries where kvp.Value != 0.000 select kvp;
So, my Time Series is partially filtered:
[7.439; 7.512; 7.635; 0.006; 7.846...]
However, the value "0.006" is not valid!
How could I implement an elegant filtering syntax based on the previous value, something like a "percent limit" in the rate of change:
if (0.006 / 7.635) * 100 < 0.1 then ---> drop / delete(0.006)
If you want to look just at the previous/next value, then you can shift the series by one and zip it with the original. This will give you a series of pairs (a value together with the previous/next value):
var r = actual.ZipInner(actual.Shift(1));
If you want to look at more elements around the specified one, then you'll need one of the windowing functions provided by Deedle:
Floating windows and chunking
The simplest example would be to use WindowInto to get a value together with 4 values before it:
var res = values.WindowInto(5, win =>
// 'win' is a series with the values - do something clever here!
);
One of the keys is to stay focused in methods that involve the value and its "neighbourhood", just like #tomaspetricek pointed before (Thanks!).
My goal was to find a "free-of-noise" time stamp or keys to build a Frame and perform an AddColumn operation, which is by nature a JoinKind.Left operation.
To solve the problem I used the Pairwise() method to get focused on "Item1" (current value), and "Item2" (next value) as follows:
double filterSensibility = 5.0 // % percentage
var myBooleanFilteredTimeSeries = myTimeSeries.Pairwise().Select(kvp => (kvp.Value.Item2 / kvp.Value.Item1) * 100 < filterSensibility);
Here I could write the relation I wanted! (see question) Then based on the Time Series (example) posted before I got:
myBooleanFilteredTimeSeries = [FALSE; FALSE; FALSE, TRUE; FALSE...]
TRUE means that this value is noisy! So I get only the FALSE boolean values with:
var myDateKeysModel = from kvp in myBooleanFilteredTimeSeries where kvp.Value == false select kvp;
I created a frame from this last Time Series:
myCleanDateTimeKeysFrame = Frame.FromRecords(myDateKeysModel);
Finally, I add the original (noisy) Time Series to the previously created Frame:
myCleanDateTimeKeysFrame.AddColumn("Column Title", myOrginalTimeSeries);
...et voilà!

Categories