/// <summary> /// Finds the nth available column in a <see cref="TableData" /><br /> /// This is simply the nth non-excluded column in the TableData /// </summary> /// <param name="tableData">The Table to be searched.</param> /// <param name="which">The which.</param> /// <returns> /// A <see cref="TableColumn" /> /// </returns> /// <exception cref="InvalidOperationException"></exception> private static int FindNonExcludedColumnIndex(TableData tableData, int which) { // Find the index of the first column which is not marked as Exluded int foundColumnCount = 0; int foundColumnIdx = -1; for (int idx = 0; idx < tableData.Columns.Count; idx++) { TableColumn col = tableData.Columns[idx]; ChartExcludeOption colExcludedOption = col.ChartOptions.GetOptionOrDefault <ChartExcludeOption>(); // No exclude option set, or exclude option set to false if (colExcludedOption == null || colExcludedOption.Exclude == false) { foundColumnCount++; if (foundColumnCount == which) { foundColumnIdx = idx; break; } } } if (foundColumnIdx == -1) { throw new InvalidOperationException(string.Format("The first {0} (read 1 as first, 2 as second etc) non-excluded column does not exist in the TableData.", which)); } return(foundColumnIdx); }
/// <summary> /// Processes the dynamic chart. /// </summary> /// <param name="tableDataSheetName">Name of the table data sheet.</param> /// <param name="tableData">The table data.</param> /// <param name="chartModel">The chart model.</param> /// <returns></returns> /// <exception cref="ArgumentNullException">chartModel</exception> private static bool ProcessDynamicChart(string tableDataSheetName, TableData tableData, ChartModel chartModel) { if (chartModel == null) { throw new ArgumentNullException("chartModel"); } TempDiagnostics.Output(string.Format("Processing chart in sheet '{0}'", tableDataSheetName)); var seriesFactory = new SeriesFactory(chartModel); if (seriesFactory.SourceSeriesCount > 0) { if (tableData.TreatRowAsSeries == false) { // A COLUMN of data represents a series... // Find the Range that representes the first Axis Data for all of the series... CompositeRangeReference category1AxisRange = DetermineCategory1AxisRange(tableData, tableData.TreatRowAsSeries, tableDataSheetName); foreach (TableColumn column in tableData.Columns) { // Skip if we are on the category column if (column.DataRegion.ExcelColumnStart == category1AxisRange.MinColumnIndex) { continue; } // Get information about the series that will be based on the column var seriesInfo = new ChartSeriesInfo(column); // We need a column that has not been actively excluded if (seriesInfo.BaseOnChartSeriesIndex >= 0 && !seriesInfo.SuppressSeries) { // Get the template series or get a copy if already used OpenXmlCompositeElement clonedSeries = seriesFactory.GetOrCloneSourceSeries(seriesInfo.BaseOnChartSeriesIndex); //TODO: Allow ability to assign colour palettes (as opposed to colours) to dynamically generated series Color?seriesColour = seriesInfo.SeriesColour; if (!seriesColour.HasValue) { int useCount = seriesFactory.GetUseCount(seriesInfo.BaseOnChartSeriesIndex); seriesColour = new Color?((ColourPalette.GetColour(ColourPaletteType.GamTechnicalChartPalette, useCount - 1))); } var seriesTextRange = new CompositeRangeReference(new RangeReference(tableDataSheetName, (uint)column.DataRegion.ExcelRowStart, (uint)column.DataRegion.ExcelColumnStart)); // Series data is first column only var seriesValuesRange = new CompositeRangeReference(new RangeReference(tableDataSheetName, (uint)column.DataRegion.ExcelRowStart + 1, (uint)column.DataRegion.ExcelColumnStart, (uint)column.DataRegion.ExcelRowEnd, (uint)column.DataRegion.ExcelColumnStart)); // Determine data ranges to be used within chart series. ChartDataRangeInfo dataRangeInfo = new ChartDataRangeInfo { SeriesTextRange = seriesTextRange, CategoryAxisDataRange = category1AxisRange, SeriesValuesRange = seriesValuesRange, }; // update all formula on the series UpdateChartSeriesDataReferences(clonedSeries, dataRangeInfo, new SolidColorBrush(seriesColour.Value)); } } } else { // A ROW of data represents a series... // Find the Range that representes the first Axis Data for all of the series... CompositeRangeReference category1AxisRange = DetermineCategory1AxisRange(tableData, tableData.TreatRowAsSeries, tableDataSheetName); TableColumn seriesTextColumn = tableData.Columns[FindNonExcludedColumnIndex(tableData, 1)]; // Build a list of columns to include in the series var seriesTableColumns = new List <TableColumn>(); int seriesValuesColumnIndex = FindNonExcludedColumnIndex(tableData, 2); TableColumn firstSeriesValuesColumn = tableData.Columns[seriesValuesColumnIndex]; seriesTableColumns.Add(firstSeriesValuesColumn); // Count up to last column, excluding those marked for exclusion while (seriesValuesColumnIndex < (tableData.Columns.Count - 1)) { seriesValuesColumnIndex++; TableColumn tableColumn = tableData.Columns[seriesValuesColumnIndex]; ChartExcludeOption excludeOption = tableColumn.ChartOptions.GetOptionOrDefault <ChartExcludeOption>(); if (excludeOption == null || excludeOption.Exclude == false) { seriesTableColumns.Add(tableColumn); } } foreach (TableDataRowInfo rowInfo in tableData.RowData) { // Extract chart series related properties from row data var seriesInfo = new ChartSeriesInfo(rowInfo.RowData); // Determine where data has been written into Excel uint rowIndex = (uint)tableData.MapContainer.ExcelRowStart + rowInfo.TableRowIndex - 1; // Skip if we are on the category column if (rowIndex == category1AxisRange.MinRowIndex) { continue; } // We need a column that has not been actively excluded if (seriesInfo.BaseOnChartSeriesIndex >= 0 && !seriesInfo.SuppressSeries) { // Get the template series or get a copy if already used OpenXmlCompositeElement clonedSeries = seriesFactory.GetOrCloneSourceSeries(seriesInfo.BaseOnChartSeriesIndex); //TODO: Allow ability to assign colour palettes (as opposed to colours) to dynamically generated series Color?seriesColour = seriesInfo.SeriesColour; if (!seriesColour.HasValue) { int useCount = seriesFactory.GetUseCount(seriesInfo.BaseOnChartSeriesIndex); seriesColour = new Color?((ColourPalette.GetColour(ColourPaletteType.GamTechnicalChartPalette, useCount - 1))); } // SeriesText is the series heading used in legends var seriesTextRange = new CompositeRangeReference(new RangeReference(tableDataSheetName, rowIndex, (uint)seriesTextColumn.DataRegion.ExcelColumnStart)); // From column after category to last column in table. var seriesValuesRange = new CompositeRangeReference(); foreach (TableColumn tableColumn in seriesTableColumns) { seriesValuesRange.Update(tableDataSheetName, rowIndex, (uint)tableColumn.DataRegion.ExcelColumnStart, rowIndex, (uint)tableColumn.DataRegion.ExcelColumnStart); } // Determine data ranges to be used within chart series. ChartDataRangeInfo dataRangeInfo = new ChartDataRangeInfo { SeriesTextRange = seriesTextRange, CategoryAxisDataRange = category1AxisRange, SeriesValuesRange = seriesValuesRange, }; // update all formula on the series ( I know we're constantly swapping between brush and color here.... Address later) UpdateChartSeriesDataReferences(clonedSeries, dataRangeInfo, new SolidColorBrush(seriesColour.Value)); } } } // Remove all un-used template series and set the order of remaining. uint seriesIndex = 0; for (int idx = 0; idx < seriesFactory.SourceSeriesCount; idx++) { OpenXmlCompositeElement templateSeries = seriesFactory.GetSourceSeriesElement(idx); // Value is the use-count, if zero then the template needs removing. if (seriesFactory.GetUseCount(idx) == 0) { // Remove the template series. templateSeries.Remove(); } else { // Set template series index and order to initial value, then set clones var templateIndex = templateSeries.Descendants <DrawingCharts.Index>().FirstOrDefault(); if (templateIndex != null) { templateIndex.Val = seriesIndex; } var templateOrder = templateSeries.Descendants <DrawingCharts.Order>().FirstOrDefault(); if (templateOrder != null) { templateOrder.Val = seriesIndex; } seriesIndex++; OpenXmlElement lastElement = templateSeries; foreach (var clonedElement in seriesFactory.GetClonedSeriesElements(idx)) { var index = clonedElement.Descendants <DrawingCharts.Index>().FirstOrDefault(); if (index != null) { index.Val = seriesIndex; } var order = clonedElement.Descendants <DrawingCharts.Order>().FirstOrDefault(); if (order != null) { order.Val = seriesIndex; } lastElement.InsertAfterSelf <OpenXmlElement>(clonedElement); lastElement = clonedElement; seriesIndex++; } } } } return(true); }
/// <summary> /// Determines the Category (or X1) Axis column within the table by doing the following: /// 1. If there are no columns, then returns null /// 2. Iterates over each column, if 'IsCategory1Axis == true', then returns that column.<br /> /// 3. If no column with 'IsCategory1Axis == true', then assumes the first column, hence returns column 0. /// </summary> /// <param name="tableData">The table to be tested</param> /// <param name="treatRowAsSeries">if set to <c>true</c> [treat row as series].</param> /// <param name="dataSheetName">Name of the data sheet.</param> /// <returns></returns> /// <exception cref="InvalidOperationException">There are no non-excluded columns which can be used as the 'Category 1' axis in chart</exception> private static CompositeRangeReference DetermineCategory1AxisRange(TableData tableData, bool treatRowAsSeries, string dataSheetName) { if (treatRowAsSeries) { int rowIndex = -1; // When rows are treated as series, the Category 1 Axis is the row that contains the row headings, // unless a row exists which is explicitly marked as being the Category 1 Axis foreach (TableDataRowInfo row in tableData.RowData) { // Get series related information about the row var rowInfo = new ChartSeriesInfo(row.RowData); if (rowInfo.IsCategory1Axis) { rowIndex = (int)row.TableRowIndex; } } if (rowIndex == -1) { // No row marked to be used as the Category 1 Axis, so we assume the table heading row rowIndex = (int)tableData.MapContainer.ExcelRowStart; } // Now we've determined which row will be used as the 'Category 1 Axis', // we need to find the first non-excluded column in which the values reside. int category1AxisColumnIndex = FindNonExcludedColumnIndex(tableData, 2); TableColumn firstColumn = tableData.Columns[category1AxisColumnIndex]; var rangeReference = new CompositeRangeReference ( dataSheetName, (uint)rowIndex, (uint)firstColumn.DataRegion.ExcelColumnStart, (uint)rowIndex, (uint)firstColumn.DataRegion.ExcelColumnStart ); // Count up to last column excluding those marked excluded while (category1AxisColumnIndex < (tableData.Columns.Count - 1)) { category1AxisColumnIndex++; TableColumn tableColumn = tableData.Columns[category1AxisColumnIndex]; ChartExcludeOption excludeOption = tableColumn.ChartOptions.GetOptionOrDefault <ChartExcludeOption>(); if (excludeOption == null || excludeOption.Exclude == false) { rangeReference.Update(dataSheetName, (uint)rowIndex, (uint)tableColumn.DataRegion.ExcelColumnStart, (uint)rowIndex, (uint)tableColumn.DataRegion.ExcelColumnStart); } } // Again (see above), we are currently ignoring excluded column in the table from this range reference. return(rangeReference); } else { // Axis Data Range is first non-excluded column data, unless a column is explitly marked as being the Category 1 Axis TableColumn dataColumn = null; if (tableData.Columns.Count == 0) { return(null); } TableColumn firstNonExcludedColumn = null; foreach (TableColumn column in tableData.Columns) { var colInfo = new ChartSeriesInfo(column); if (colInfo.IsCategory1Axis) { dataColumn = column; break; } if (firstNonExcludedColumn == null && !colInfo.SuppressSeries) { firstNonExcludedColumn = column; } } if (dataColumn == null) { dataColumn = firstNonExcludedColumn; } // Throw exception if no axis column defined. if (dataColumn == null) { throw new InvalidOperationException("There are no non-excluded columns which can be used as the 'Category 1' axis in chart"); } return(new CompositeRangeReference ( new RangeReference(dataSheetName, (uint)dataColumn.DataRegion.ExcelRowStart + 1, (uint)dataColumn.DataRegion.ExcelColumnStart, (uint)dataColumn.DataRegion.ExcelRowEnd, (uint)dataColumn.DataRegion.ExcelColumnEnd) )); } }