From f8aa3adf7e6dd419929feb5059f89cb97a8631cf Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 18 Jun 2023 00:12:50 +0800 Subject: [PATCH] This closes #1553, the `AddChart` function support set primary titles - Update unit tests and documentation - Lint issues fixed --- chart.go | 18 ++++++++-- chart_test.go | 2 +- drawing.go | 93 ++++++++++++++++++++++++++++++++++---------------- numfmt.go | 16 ++++----- numfmt_test.go | 2 +- xmlChart.go | 4 ++- 6 files changed, 92 insertions(+), 43 deletions(-) diff --git a/chart.go b/chart.go index b4a73feb..65200e80 100644 --- a/chart.go +++ b/chart.go @@ -794,6 +794,8 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Maximum // Minimum // Font +// NumFmt +// Title // // The properties of 'YAxis' that can be set are: // @@ -805,6 +807,9 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Maximum // Minimum // Font +// LogBase +// NumFmt +// Title // // None: Disable axes. // @@ -813,14 +818,14 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // MinorGridLines: Specifies minor grid lines. // // MajorUnit: Specifies the distance between major ticks. Shall contain a -// positive floating-point number. The MajorUnit property is optional. The +// positive floating-point number. The 'MajorUnit' property is optional. The // default value is auto. // // TickLabelSkip: Specifies how many tick labels to skip between label that is // drawn. The 'TickLabelSkip' property is optional. The default value is auto. // // ReverseOrder: Specifies that the categories or values on reverse order -// (orientation of the chart). The ReverseOrder property is optional. The +// (orientation of the chart). The 'ReverseOrder' property is optional. The // default value is false. // // Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property @@ -841,6 +846,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Color // VertAlign // +// LogBase: Specifies logarithmic scale for the YAxis. +// +// NumFmt: Specifies that if linked to source and set custom number format code +// for axis. The 'NumFmt' property is optional. The default format code is +// 'General'. +// +// Title: Specifies that the primary horizontal or vertical axis title. The +// 'Title' property is optional. +// // Set chart size by 'Dimension' property. The 'Dimension' property is optional. // The default width is 480, and height is 290. // diff --git a/chart_test.go b/chart_test.go index ba17dbd6..4c359d7b 100644 --- a/chart_test.go +++ b/chart_test.go @@ -206,7 +206,7 @@ func TestAddChart(t *testing.T) { sheetName, cell string opts *Chart }{ - {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}}, + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}, Title: []RichTextRun{{Text: "Primary Horizontal Axis Title"}}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}, Title: []RichTextRun{{Text: "Primary Vertical Axis Title", Font: &Font{Color: "777777", Bold: true, Italic: true, Size: 12}}}}}}, {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index 40559feb..c7264177 100644 --- a/drawing.go +++ b/drawing.go @@ -66,41 +66,43 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { Title: &cTitle{ Tx: cTx{ Rich: &cRich{ - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Kern: 1200, - Strike: "noStrike", - U: "none", - Sz: 1400, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{ - Val: intPtr(65000), - }, - LumOff: &attrValInt{ - Val: intPtr(35000), + P: []aP{ + { + PPr: &aPPr{ + DefRPr: aRPr{ + Kern: 1200, + Strike: "noStrike", + U: "none", + Sz: 1400, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{ + Val: intPtr(65000), + }, + LumOff: &attrValInt{ + Val: intPtr(35000), + }, }, }, - }, - Ea: &aEa{ - Typeface: "+mn-ea", - }, - Cs: &aCs{ - Typeface: "+mn-cs", - }, - Latin: &xlsxCTTextFont{ - Typeface: "+mn-lt", + Ea: &aEa{ + Typeface: "+mn-ea", + }, + Cs: &aCs{ + Typeface: "+mn-cs", + }, + Latin: &xlsxCTTextFont{ + Typeface: "+mn-lt", + }, }, }, - }, - R: &aR{ - RPr: aRPr{ - Lang: "en-US", - AltLang: "en-US", + R: &aR{ + RPr: aRPr{ + Lang: "en-US", + AltLang: "en-US", + }, + T: opts.Title.Name, }, - T: opts.Title.Name, }, }, }, @@ -1059,6 +1061,7 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { NumFmt: &cNumFmt{FormatCode: "General"}, MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, + Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""), TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.YAxis), @@ -1110,6 +1113,7 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { }, Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])}, + Title: f.drawPlotAreaTitles(opts.YAxis.Title, "horz"), NumFmt: &cNumFmt{ FormatCode: chartValAxNumFmtFormatCode[opts.Type], }, @@ -1169,6 +1173,35 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { } } +// drawPlotAreaTitles provides a function to draw the c:title element. +func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle { + if len(runs) == 0 { + return nil + } + title := &cTitle{Tx: cTx{Rich: &cRich{}}, Overlay: &attrValBool{Val: boolPtr(false)}} + for _, run := range runs { + r := &aR{T: run.Text} + if run.Font != nil { + r.RPr.B, r.RPr.I = run.Font.Bold, run.Font.Italic + if run.Font.Color != "" { + r.RPr.SolidFill = &aSolidFill{SrgbClr: &attrValString{Val: stringPtr(run.Font.Color)}} + } + if run.Font.Size > 0 { + r.RPr.Sz = run.Font.Size * 100 + } + } + title.Tx.Rich.P = append(title.Tx.Rich.P, aP{ + PPr: &aPPr{DefRPr: aRPr{}}, + R: r, + EndParaRPr: &aEndParaRPr{Lang: "en-US", AltLang: "en-US"}, + }) + } + if vert == "horz" { + title.Tx.Rich.BodyPr = aBodyPr{Rot: -5400000, Vert: vert} + } + return title +} + // drawPlotAreaSpPr provides a function to draw the c:spPr element. func (f *File) drawPlotAreaSpPr() *cSpPr { return &cSpPr{ diff --git a/numfmt.go b/numfmt.go index 01232544..da387fb3 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1190,7 +1190,7 @@ func (nf *numberFormat) printNumberLiteral(text string) string { } for _, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err, changeNumFmtCode := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { + if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } result += nf.currencyString @@ -1326,7 +1326,7 @@ func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if err, changeNumFmtCode := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { + if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } nf.result += nf.currencyString @@ -1397,28 +1397,28 @@ func (nf *numberFormat) positiveHandler() string { // currencyLanguageHandler will be handling currency and language types tokens // for a number format expression. -func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (error, bool) { +func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (bool, error) { for _, part := range token.Parts { if inStrSlice(supportedTokenTypes, part.Token.TType, true) == -1 { - return ErrUnsupportedNumberFormat, false + return false, ErrUnsupportedNumberFormat } if part.Token.TType == nfp.TokenSubTypeLanguageInfo { if strings.EqualFold(part.Token.TValue, "F800") { // [$-x-sysdate] if nf.opts != nil && nf.opts.LongDatePattern != "" { nf.value = format(nf.value, nf.opts.LongDatePattern, nf.date1904, nf.cellType, nf.opts) - return nil, true + return true, nil } part.Token.TValue = "409" } if strings.EqualFold(part.Token.TValue, "F400") { // [$-x-systime] if nf.opts != nil && nf.opts.LongTimePattern != "" { nf.value = format(nf.value, nf.opts.LongTimePattern, nf.date1904, nf.cellType, nf.opts) - return nil, true + return true, nil } part.Token.TValue = "409" } if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { - return ErrUnsupportedNumberFormat, false + return false, ErrUnsupportedNumberFormat } nf.localCode = strings.ToUpper(part.Token.TValue) } @@ -1426,7 +1426,7 @@ func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (error, bool) { nf.currencyString = part.Token.TValue } } - return nil, false + return false, nil } // localAmPm return AM/PM name by supported language ID. diff --git a/numfmt_test.go b/numfmt_test.go index c49393f6..8cf7afad 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1093,7 +1093,7 @@ func TestNumFmt(t *testing.T) { } } nf := numberFormat{} - err, changeNumFmtCode := nf.currencyLanguageHandler(nfp.Token{Parts: []nfp.Part{{}}}) + changeNumFmtCode, err := nf.currencyLanguageHandler(nfp.Token{Parts: []nfp.Part{{}}}) assert.Equal(t, ErrUnsupportedNumberFormat, err) assert.False(t, changeNumFmtCode) } diff --git a/xmlChart.go b/xmlChart.go index 20b70517..8e9e46ce 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -74,7 +74,7 @@ type cTx struct { type cRich struct { BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` LstStyle string `xml:"a:lstStyle,omitempty"` - P aP `xml:"a:p"` + P []aP `xml:"a:p"` } // aBodyPr (Body Properties) directly maps the a:bodyPr element. This element @@ -351,6 +351,7 @@ type cAxs struct { AxPos *attrValString `xml:"axPos"` MajorGridlines *cChartLines `xml:"majorGridlines"` MinorGridlines *cChartLines `xml:"minorGridlines"` + Title *cTitle `xml:"title"` NumFmt *cNumFmt `xml:"numFmt"` MajorTickMark *attrValString `xml:"majorTickMark"` MinorTickMark *attrValString `xml:"minorTickMark"` @@ -539,6 +540,7 @@ type ChartAxis struct { Font Font LogBase float64 NumFmt ChartNumFmt + Title []RichTextRun } // ChartDimension directly maps the dimension of the chart.