Making waterfall chart using PowerPoint chart in VSTO - c#

I am making a
Waterfall chart in a PowerPoint add-in using VSTO.
PowerPoint.Slide slide = null;
PowerPoint.Shape shape = null;
PowerPoint.Chart chart = null;
This lines tells what kind of chart to make.
shape = slide.Shapes.AddChart(Office.XlChartType.xlColumnStacked, 200, 200, 300, 200);
This line here opens an Excel workbook which makes the chart when data is added into it.
chart = shape.Chart;
Here is an image of the chart that I create.
Now the problem is, I want the 2nd bar to be plotted form the end of the 1st bar i.e from 4 instead of 0 on the x-axis.
Like this.
Can someone tell me is there any way I can draw my series from the value of the previous series instead of 0 value of x-axis?

I don't think Excel or PowerPoint charts supports waterfalls out of the box. However, you can do this by keeping the chart type as stacked and adding a separate helper chart series that is invisible and pushes the other series up. Then it is all about calculating the different values for this helper chart series based on the previous values of the actual chart series.
By invisible, I imply making both the line and the fill none. The chart series will still be there, but you just won't see it unless you start editing the chart.

I created waterfall chart from Stacked column chart and used the Workbook object from the Chartdata of the Selected shape.
You can follow this link to check how to create waterfall chart manually..and then manipulate the Worksheet/datasheet of the chart through code to create Waterfall chart.
Here is a snippet of my code..
private void calculationAndFormatting(bool excelEvent,Excel.Worksheet Sheet)
{
//unregister from Excel Change Event
Sheet.Application.EnableEvents = false;
int lRow = 1;
lRow = iRowCount;
Sheet.Range["A1", "A" + lRow].Copy(Type.Missing);
Sheet.Range["B" + (lRow + 5)].PasteSpecial(Excel.XlPasteType.xlPasteAll, Excel.XlPasteSpecialOperation.xlPasteSpecialOperationNone, false, false);
Sheet.Range["B1", "B" + lRow].Copy(Type.Missing);
Sheet.Range["A" + (lRow + 5)].PasteSpecial(Excel.XlPasteType.xlPasteAll, Excel.XlPasteSpecialOperation.xlPasteSpecialOperationNone, false, false);
Sheet.Range["B1", "D1"].Copy(Type.Missing);
Sheet.Range["C" + (lRow + 5)].PasteSpecial(Excel.XlPasteType.xlPasteAll, Excel.XlPasteSpecialOperation.xlPasteSpecialOperationNone, false, false);
int ulRow = Sheet.UsedRange.Rows.Count;
int k=ulRow;
if (!excelEvent)
{
Sheet.Range["C" + (lRow + 6)].Value = "0";
Sheet.Range["C" + (ulRow + 1)].Value = "0";
Sheet.Range["A" + (ulRow + 1)].Value = "=SUM(A" + (lRow + 5) + ":" + "A" + ulRow + ")";
Sheet.Range["B" + (ulRow + 1)].Value = "Total Value";
k = ulRow + 1;
}
for (int i = lRow + 6; i <= k; i++)
{
if (Sheet.Range["A" + i].Value < 0)
{
if (i <= k-1 && i != lRow + 6)
{
Sheet.Range["C" + i].Formula = "=E" + (i - 1) + "+" + "A" + i;
}
Sheet.Range["D" + i].Formula = "=-A" + i;
Sheet.Range["E" + i].Formula = "=C" + i;
}
else
{
if (i <= k-1 && i != lRow + 6)
{
Sheet.Range["C" + i].Formula = "=E" + (i - 1);
}
Sheet.Range["D" + i].Formula = "=A" + i;
Sheet.Range["E" + i].Formula = "=C" + i + "+" + "D" + i;
}
}
string sourceCol = "='" + Sheet.Name + "'!$C$" + (lRow + 5) + ":$E$" + (k);
pChart.SetSourceData(sourceCol, PowerPoint.XlRowCol.xlColumns);
PowerPoint.Axis axis = pChart.Axes(PowerPoint.XlAxisType.xlValue);
axis.MaximumScale = 1.25 * (System.Double)Sheet.Range["D" + (k)].Value;
axis.MinimumScale = 0.0;
PowerPoint.Axis catAxis = pChart.Axes(PowerPoint.XlAxisType.xlCategory);
catAxis.CategoryNames = Sheet.Range["B" + (lRow + 6), "B" + (k)].Value;
applyFormatting();
drawLeaderLines(Sheet);
Sheet.Application.EnableEvents = true;
Sheet = null;
}

Related

Points on chart graphic don't fit y-lines

Points on my second chart don't fit y-axis as you can see here:
Points values are exactly 50.0000, 49.9999, 49.9998 and 50.0001. But they are not on lines. And when I add point and with it increase number of values on y-axis, then points would fit y-axis, like in this picture.
Here is my code (sorry for Serbian text values)
TacnostVage tacnost = bazaPodataka.UcitajTacnostVage(Convert.ToString(dataGridView2.SelectedRows[0].Cells[2].Value), Convert.ToInt32(comboBox18.Text));
List<TestTacnostVage> testoviTacnost = bazaPodataka.UcitajTestoveTacnostVage(Convert.ToString(dataGridView2.SelectedRows[0].Cells[2].Value), Convert.ToInt32(comboBox18.Text));
chart2.ChartAreas.Clear();
chart2.Series.Clear();
prikažiToolStripMenuItem.DropDownItems.Clear();
tabeluToolStripMenuItem.DropDownItems.Clear();
string format = Convert.ToString(vaga.Podeljak);
format = format.Remove(format.Length - 1, 1) + "0";
if (testoviTacnost.Count != 0)
{
for (int i = 0; i < tacnost.NominalneMase.Count(); i++)
{
ChartArea area = new ChartArea();
Series series = new Series();
area.AxisY.MajorGrid.LineColor = Color.LightGray;
area.AxisX.MajorGrid.LineColor = Color.LightGray;
area.AxisY.LabelStyle.Format = format;
area.BorderColor = Color.LightGray;
area.BorderDashStyle = ChartDashStyle.Solid;
area.AxisY.Interval = vaga.Podeljak;
area.Name = "ChartArea" + (i + 1);
series.ChartType = SeriesChartType.Point;
series.ChartArea = "ChartArea" + (i + 1);
series.Name = "Tačka" + (i + 1);
string text = "";
TegoviTacnostVaga tegoviTacnost = bazaPodataka.UcitajTegoveTacnostVage(Convert.ToString(dataGridView2.SelectedRows[0].Cells[2].Value), Convert.ToInt32(comboBox18.Text), i);
if (tegoviTacnost != null)
{
for (int j = 0; j < tegoviTacnost.Proizvodjac.Count(); j++)
{
text += tegoviTacnost.Proizvodjac[j] + " ";
text += tegoviTacnost.SerijskiBrojevi[j] + " ";
text += tegoviTacnost.NominalneMase[j] + "g";
text += (j == tegoviTacnost.Proizvodjac.Count() - 1 ? "" : "\n");
}
}
series.LegendText = (text == "" ? "Nema podataka" : text);
for (int j = 0; j < testoviTacnost.Count(); j++)
series.Points.AddXY(testoviTacnost[j].RedniBrojTesta, testoviTacnost[j].RezultatiMerenja[i]);
area.AxisY.StripLines.Add(new StripLine() { BorderColor = Color.Red, IntervalOffset = (tacnost.RezultatiMerenja[i].Average() + koeficijentTacnost * ponovljivost.ReferentnaVrednost), Text = "Gornja granica: " + Convert.ToDouble(tacnost.RezultatiMerenja[i].Average() + koeficijentTacnost * ponovljivost.ReferentnaVrednost).ToString(format) });
area.AxisY.StripLines.Add(new StripLine() { BorderColor = Color.Red, IntervalOffset = (tacnost.RezultatiMerenja[i].Average() - koeficijentTacnost * ponovljivost.ReferentnaVrednost), Text = "Donja granica: " + Convert.ToDouble(tacnost.RezultatiMerenja[i].Average() - koeficijentTacnost * ponovljivost.ReferentnaVrednost).ToString(format) });
area.AxisY.Maximum = area.AxisY.StripLines[0].IntervalOffset + area.AxisY.Interval;
if (series.Points.FindMaxByValue().YValues[0] >= area.AxisY.Maximum)
area.AxisY.Maximum = series.Points.FindMaxByValue().YValues[0] + area.AxisY.Interval;
area.AxisY.Minimum = area.AxisY.StripLines[1].IntervalOffset - area.AxisY.Interval;
if (series.Points.FindMinByValue().YValues[0] <= area.AxisY.Minimum)
area.AxisY.Minimum = series.Points.FindMinByValue().YValues[0] - area.AxisY.Interval;
chart2.ChartAreas.Add(area);
chart2.Series.Add(series);
}
}
I found solution, but I'm not sure if this explanation is true. The problem was Axis-Y maximum. Charts Axis-Y interval was 0.0001 (4 decimals), but in my code, I put maximum to be StripLines IntervalOffset (which was more than 4 decimals) plus Charts Interval (in result that is more than 4 decimals). So probably this happens when your Chars Axis-Y Maximum and your Interval (if you set Interval) have different number of decimals. So I just simply rounded Strip Lines InvervalOffset to 4 decimals (in this case), and put Axis-Y Maximum to have 4 decimals like Interval has.

Having trouble loading "neighbours" of a specific tile into a List

this is my first time asking questions here, i'm actually having a problem in my script:
I'm trying to add the neighbors of a tile into a list, but i keep getting the NullReferenceException: Object reference not set to an instance of an object when trying to print what's inside the list.
To note that the first "list.Add" is actually working, the others aren't.
To test it i'm always trying to find something that exists, all Tile_(x)_(y) exists.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class TileStats : MonoBehaviour {
public int x;
public int y;
public List<GameObject> neighbours;
public List<GameObject> populateNeighbours ()
{
List<GameObject> list = new List<GameObject>();
//If we're at x, y.
//Left one is at x-1,y
list.Add(GameObject.Find("Tile_" + (x-1) + "_" + y));
//Right one is at x+1,y
list.Add(GameObject.Find("Tile_" + (x+1) + "_" + y));
//Bottom ones are at x,y-1 and x+1,y-1
list.Add(GameObject.Find("Tile_" + x + "_" + (y-1)));
list.Add(GameObject.Find("Tile_" + (x+1) + "_" + (y-1)));
//Top ones are at x,y+1 and x+1,y+1
list.Add(GameObject.Find("Tile_" + x + "_" + (y+1)));
list.Add(GameObject.Find("Tile_" + (x+1) + "_" + (y+1)));
return list;
}
}
...
Tile_GO.GetComponent<TileStats>().x = x;
Tile_GO.GetComponent<TileStats>().y = y;
Tile_GO.GetComponent<TileStats>().neighbours=Tile_GO.GetComponent<TileStats>().populateNeighbours();
...
The error is thrown when i try to print the input:
foreach (GameObject obj in ourHitObject.GetComponent().neighbours) {
print("Name: " + obj.name + "Atual: " + i++);
}
SOLVED
#AnthonyLeal I looked at the code and that is the problem. You are searching for GameObject while do they don't exist. Just think about it like this: Your generated a gameObject in your for loop then called the populateNeighbours() function in that for loop. That populateNeighbours() function does not operate on the generated GameObject in that loop. It looks for other GameObjects that are not yet generated. You did this many many times leading to over 500 errors. – Programmer
The problem is probably in the code itself, when i try to add the
object to the list, reading it should be fine, for example If i add to
the list 6x"Left ones", ( x-1) ( y), it will run correctly. If i try
to add any other ( right, top, bottom ) the error is thrown when
printing.
This is happening because the first GameObject.Find("Tile_" + (x-1) + "_" + y) found the GameObject in the scene. The rest of the GameObject.Find method failed. When it fails, it returns null. That null is added to the List even though there is nothing there.
This is something no one can fix for you because we can't see the name of GameObjects in your scene. You have to fix it yourself by checking if the GameObject.Find returns null before adding it to the List. Replace your populateNeighbours() function with the function below to easily debug your code. It won't add the GaeObject if it is null and it will print the name of GameObject it couldn't find. You can then use it to check if that GameObject actually exist in the scene.
Also, Don't use foreach to loop over List in Unity. Use for loop instead.
public List<GameObject> populateNeighbours()
{
List<GameObject> list = new List<GameObject>();
//If we're at x, y.
//Left one is at x-1,y
GameObject tempObj = GameObject.Find("Tile_" + (x - 1) + "_" + y);
if (tempObj == null)
Debug.Log("Tile_" + (x - 1) + "_" + y + " Does NOT exist");
else
list.Add(tempObj);
//Right one is at x+1,y
tempObj = GameObject.Find("Tile_" + (x + 1) + "_" + y);
if (tempObj == null)
Debug.Log("Tile_" + (x + 1) + "_" + y + " Does NOT exist");
else
list.Add(tempObj);
//Bottom ones are at x,y-1 and x+1,y-1
tempObj = GameObject.Find("Tile_" + x + "_" + (y - 1));
if (tempObj == null)
Debug.Log("Tile_" + x + "_" + (y - 1) + " Does NOT exist");
else
list.Add(tempObj);
tempObj = GameObject.Find("Tile_" + (x + 1) + "_" + (y - 1));
if (tempObj == null)
Debug.Log("Tile_" + (x + 1) + "_" + (y - 1) + " Does NOT exist");
else
list.Add(tempObj);
//Top ones are at x,y+1 and x+1,y+1
tempObj = GameObject.Find("Tile_" + x + "_" + (y + 1));
if (tempObj == null)
Debug.Log("Tile_" + x + "_" + (y + 1) + " Does NOT exist");
else
list.Add(tempObj);
tempObj = GameObject.Find("Tile_" + (x + 1) + "_" + (y + 1));
if (tempObj == null)
Debug.Log("Tile_" + (x + 1) + "_" + (y + 1) + " Does NOT exist");
else
list.Add(tempObj);
return list;
}

I want change variable to value of hashtables in arraylist

I made [Hashtable hash] such as
hash(i, 1)
hash(j, 2)
Also I made an [arraylist sArray] which include "i" or "j" such as
sArray[0] : hello
sArray[1] : first number is i
sArray[2] : second number is j
sArray[3] : bye
Now, I want to change the "i" and "j" in the sArray to the values of the hash.
How can I do it?
If I understand properly, I think this is the code in c#
//Your example
int i = 1;
int j = 2;
var hash = new System.Collections.Hashtable();
hash[i] = -173.5;
hash[j] = 37;
var sArray = new System.Collections.ArrayList();
sArray.Add("hello");
sArray.Add("first number is " + hash[i].ToString());
sArray.Add("second number is " + hash[j].ToString());
sArray.Add("bye");
// more general, you could have different i and j position
i = 3;
j = 4;
hash[i] = 33.3;
hash[j] = -44.4;
sArray[1] = "number in " + i.ToString() + " position is " + hash[i].ToString();
sArray[2] = "number in " + j.ToString() + " position is " + hash[j].ToString();
// I think following option is more easy to read and fast if iterated
i = 5;
j = 6;
hash[i] = 55.5;
hash[j] = -66.6;
sArray[1] = String.Format("number in {0} position is {1}", i, hash[i]);
sArray[2] = String.Format("number in {0} position is {1}", j, hash[j]);

ListBox and Label will not gradually display the numbers going through the loop

I'm trying to show the numbers of 2 dice on each roll.
When I run the program
lblDice1 and lblDice2 will only show the numbers of the last roll and listBox lstResults will only display after the for loop ends. How do I gradually show the result of each roll.
Here is my code
private void btnRoll_Click(object sender, EventArgs e)
{
for (int i = 1; i < 101; i++)
{
Dice1 = rnd.Next(1, 7);
Dice2 = rnd.Next(1, 7);
lblDice1.Text = Convert.ToString(Dice1);
lblDice2.Text = Convert.ToString(Dice2);
strOut = Convert.ToString(Dice1);
strOut2 = Convert.ToString(Dice2);
lblRollNum.Text = Convert.ToString(i);
if (Dice1 == Dice2)
lstResults.Items.Add(i + "\t" + strOut + "\t" + strOut2 + "\t" + "Same Dice Value");
else
lstResults.Items.Add(i + "\t" + strOut + "\t" + strOut2);
System.Threading.Thread.Sleep(1000);
}
}
What type of app are you using, WinForm or WPF?
If you are WPF, your loop will be tying up the UI thread so the UI will not render until the loop has finished.
You could use something like MVVM, http://galasoft.ch/, to bind the UI to the changing data in your model.

Calculation from Zoomable Graph C#

Basically I have a graph that is bound from a DataTable which source is from a DataGridView. I have zoomable functions on the graph and I need it use X Axis SelectionStart and SelectionEnd in order to calculate the block of data that is selected.
So I have some minimums maximums and averages placed in a richtextbox on another tab. As shown below in the code:
//To show the Data from file into the TextBox
richTextBox1.AppendText("\n" + "Heart Average : " + HRM.Active.DataRows.Average(r => r.HeartRate) + "\n");
richTextBox1.AppendText("\n" + "Heart Minimum : " + HRM.Active.DataRows.Min(r => r.HeartRate) + "\n");
richTextBox1.AppendText("\n" + "Heart Maximum : " + HRM.Active.DataRows.Max(r => r.HeartRate) + "\n");
richTextBox1.AppendText("\n" + "Average Speed (KM/H): " + HRM.Active.DataRows.Average(r => r.Speed) + "\n");
richTextBox1.AppendText("\n" + "Maximum Speed : " + HRM.Active.DataRows.Max(r => r.Speed) + "\n");
richTextBox1.AppendText(Environment.NewLine + " - (MPH): " + "");
richTextBox1.AppendText("\n" + "Average Power: " + HRM.Active.DataRows.Average(r => r.Power) + "\n");
richTextBox1.AppendText("\n" + "Maximum Power : " + HRM.Active.DataRows.Max(r => r.Power) + "\n");
richTextBox1.AppendText("\n" + "Average Altitude (KM/H): " + HRM.Active.DataRows.Average(r => r.Altitude) + "\n");
richTextBox1.AppendText("\n" + "Maximum Altitude : " + HRM.Active.DataRows.Max(r => r.Altitude) + "\n");
richTextBox1.AppendText("\n" + "Cadence Average : " + HRM.Active.DataRows.Average(r => r.Cadence) + "\n");
richTextBox1.AppendText("\n" + "Cadence Maximum : " + HRM.Active.DataRows.Max(r => r.Cadence) + "\n");
richTextBox1.AppendText("\n" + "Pressure Average : " + HRM.Active.DataRows.Average(r => r.Pressure) + "\n");
richTextBox1.AppendText("\n" + "Pressure Maximum : " + HRM.Active.DataRows.Max(r => r.Pressure) + "\n");
Now in the image below you can see the image of the Graph and the data it shows on there, Here is the code which binds the datatable to the graph.
protected void drawChart()
{
DataTable dt = new DataTable();
dt.Clear();
foreach (DataGridViewColumn col in dataGridView1.Columns)
{
dt.Columns.Add(col.HeaderText);
}
foreach (DataGridViewRow row in dataGridView1.Rows)
{
DataRow dRow = dt.NewRow();
foreach (DataGridViewCell cell in row.Cells)
{
dRow[cell.ColumnIndex] = cell.Value;
}
dt.Rows.Add(dRow);
}
Now what I need to do is have another textbox near the Graph and every time I zoom and the grey block comes out it displays the minimums maximiums and averages for the block i have selected! Then when I zoom out it resets to the original.
If you do not understand what I mean just message me and I will give more information.
To update a statistical calculation based on the visible Points after zooming or scrolling you need know two things: When should you do it and which points are visible.
The event to use is AxisViewChanged, that's easy. After you have hooked it up you can call a function to update your statistics:
private void chart1_AxisViewChanged(object sender, ViewEventArgs e)
{
updateStats();
}
The difficult part is to know which portion of the Points collection is visible.
At first glance you may think that the ViewEventArgs parm helps; after all it has such promising data as: NewPosition and NewSize.
But looking closer you will see that both are doubles, so unless your XValues have been set counting up from 0 by 1, they won't index into the Points collection.
Instead we must do a little searching for those values, from left for the minimum and from the right for the maximum. Here is a function to do so:
int getVisiblePoint(Chart chart, Series series, bool first)
{
Series S = series;
ChartArea CA = chart.ChartAreas[S.ChartArea];
DataPoint pt = null;
if (first) pt = S.Points.Select(x => x)
.Where(x => x.XValue >= CA.AxisX.ScaleView.ViewMinimum)
.DefaultIfEmpty(S.Points.First()).First();
else pt = S.Points.Select(x => x)
.Where(x => x.XValue <= CA.AxisX.ScaleView.ViewMaximum)
.DefaultIfEmpty(S.Points.Last()).Last();
return S.Points.IndexOf(pt);
}
From here on things get a lot easier; we can do statistics on our series, maybe like this:
void updateStats()
{
int firstPt = getVisiblePoint(chart1, chart1.Series[0], true);
int lastPt = getVisiblePoint(chart1, chart1.Series[0], false);
int sCount = chart1.Series.Count;
double[] avg = new double[sCount];
double[] min = new double[sCount];
double[] max = new double[sCount];
for (int i = 0; i < sCount; i++)
{
Series S = chart1.Series[i];
avg[i] = getAverage(S, firstPt, lastPt);
min[i] = getMixMax(S, firstPt, lastPt, true);
max[i] = getMixMax(S, firstPt, lastPt, false);
}
// insert code to display the data here!
}
using simple functions to do the math:
double getAverage(Series series, int first, int last)
{
double sum = 0;
for (int i = first; i < last; i++) sum += series.Points[i].YValues[0];
return sum / (last - first + 1);
}
double getMixMax(Series series, int first, int last, bool min)
{
double val = 0;
for (int i = first; i < last; i++)
{
double v = series.Points[i].YValues[0];
if ( (min && val > v) || (!min && val >= v)) val = v;
}
return val;
}

Categories

Resources