diff --git a/chart.go b/chart.go index 0fea7712..83b65376 100644 --- a/chart.go +++ b/chart.go @@ -803,6 +803,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // MajorGridLines // MinorGridLines // MajorUnit +// Secondary // ReverseOrder // Maximum // Minimum @@ -821,6 +822,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // positive floating-point number. The 'MajorUnit' property is optional. The // default value is auto. // +// Secondary: Specifies the current series vertical axis as the secondary axis, +// this only works for the second and later chart in the combo chart. The +// default value is false. +// // TickLabelSkip: Specifies how many tick labels to skip between label that is // drawn. The 'TickLabelSkip' property is optional. The default value is auto. // diff --git a/chart_test.go b/chart_test.go index 4c359d7b..49b83551 100644 --- a/chart_test.go +++ b/chart_test.go @@ -238,7 +238,7 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}}, - {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, + {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, // area series chart {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, @@ -280,7 +280,7 @@ func TestAddChart(t *testing.T) { {"I1", Doughnut, "Clustered Column - Doughnut Chart"}, } for _, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) + assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}, YAxis: ChartAxis{Secondary: true}})) } stackedAreaCombo := map[string][]interface{}{ "A16": {Line, "Stacked Area - Line Chart"}, diff --git a/drawing.go b/drawing.go index c7264177..9e2c9f30 100644 --- a/drawing.go +++ b/drawing.go @@ -277,13 +277,10 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { VaryColors: &attrValBool{ Val: opts.VaryColors, }, - Ser: f.drawChartSeries(opts), - Shape: f.drawChartShape(opts), - DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + Ser: f.drawChartSeries(opts), + Shape: f.drawChartShape(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), Overlap: &attrValInt{Val: intPtr(100)}, } var ok bool @@ -542,10 +539,7 @@ func (f *File) drawLineChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -565,10 +559,7 @@ func (f *File) drawLine3DChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -658,10 +649,7 @@ func (f *File) drawRadarChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -681,10 +669,7 @@ func (f *File) drawScatterChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(opts), ValAx: f.drawPlotAreaValAx(opts), @@ -698,9 +683,9 @@ func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea { Surface3DChart: &cCharts{ Ser: f.drawChartSeries(opts), AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, + {Val: intPtr(100000000)}, + {Val: intPtr(100000001)}, + {Val: intPtr(100000005)}, }, }, CatAx: f.drawPlotAreaCatAx(opts), @@ -720,9 +705,9 @@ func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { SurfaceChart: &cCharts{ Ser: f.drawChartSeries(opts), AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, + {Val: intPtr(100000000)}, + {Val: intPtr(100000001)}, + {Val: intPtr(100000005)}, }, }, CatAx: f.drawPlotAreaCatAx(opts), @@ -745,10 +730,7 @@ func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { }, Ser: f.drawChartSeries(opts), DLbls: f.drawChartDLbls(opts), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, + AxID: f.genAxID(opts), }, ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]}, } @@ -1050,7 +1032,7 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: intPtr(754001152)}, + AxID: &attrValInt{Val: intPtr(100000000)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, Max: max, @@ -1065,7 +1047,7 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.YAxis), - CrossAx: &attrValInt{Val: intPtr(753999904)}, + CrossAx: &attrValInt{Val: intPtr(100000001)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, Auto: &attrValBool{Val: boolPtr(true)}, LblAlgn: &attrValString{Val: stringPtr("ctr")}, @@ -1085,6 +1067,28 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { if opts.XAxis.TickLabelSkip != 0 { axs[0].TickLblSkip = &attrValInt{Val: intPtr(opts.XAxis.TickLabelSkip)} } + if opts.order > 0 && opts.YAxis.Secondary { + axs = append(axs, &cAxs{ + AxID: &attrValInt{Val: intPtr(opts.XAxis.axID)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(true)}, + AxPos: &attrValString{Val: stringPtr("b")}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(&opts.YAxis), + CrossAx: &attrValInt{Val: intPtr(opts.YAxis.axID)}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, + }) + } return axs } @@ -1104,7 +1108,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: intPtr(753999904)}, + AxID: &attrValInt{Val: intPtr(100000001)}, Scaling: &cScaling{ LogBase: logBase, Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, @@ -1122,7 +1126,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.XAxis), - CrossAx: &attrValInt{Val: intPtr(754001152)}, + CrossAx: &attrValInt{Val: intPtr(100000000)}, Crosses: &attrValString{Val: stringPtr("autoZero")}, CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, }, @@ -1142,6 +1146,26 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { if opts.YAxis.MajorUnit != 0 { axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(opts.YAxis.MajorUnit)} } + if opts.order > 0 && opts.YAxis.Secondary { + axs = append(axs, &cAxs{ + AxID: &attrValInt{Val: intPtr(opts.YAxis.axID)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr("r")}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(&opts.XAxis), + CrossAx: &attrValInt{Val: intPtr(opts.XAxis.axID)}, + Crosses: &attrValString{Val: stringPtr("max")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, + }) + } return axs } @@ -1157,7 +1181,7 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { } return []*cAxs{ { - AxID: &attrValInt{Val: intPtr(832256642)}, + AxID: &attrValInt{Val: intPtr(100000005)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, Max: max, @@ -1168,7 +1192,7 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(nil), - CrossAx: &attrValInt{Val: intPtr(753999904)}, + CrossAx: &attrValInt{Val: intPtr(100000001)}, }, } } @@ -1467,3 +1491,13 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error f.Drawings.Store(drawingXML, wsDr) return err } + +// genAxID provides a function to generate ID for primary and secondary +// horizontal or vertical axis. +func (f *File) genAxID(opts *Chart) []*attrValInt { + opts.XAxis.axID, opts.YAxis.axID = 100000000, 100000001 + if opts.order > 0 && opts.YAxis.Secondary { + opts.XAxis.axID, opts.YAxis.axID = 100000003, 100000004 + } + return []*attrValInt{{Val: intPtr(opts.XAxis.axID)}, {Val: intPtr(opts.YAxis.axID)}} +} diff --git a/xmlChart.go b/xmlChart.go index 8e9e46ce..7be783bd 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -535,12 +535,14 @@ type ChartAxis struct { MajorUnit float64 TickLabelSkip int ReverseOrder bool + Secondary bool Maximum *float64 Minimum *float64 Font Font LogBase float64 NumFmt ChartNumFmt Title []RichTextRun + axID int } // ChartDimension directly maps the dimension of the chart.