Compare commits

...

3 Commits

Author SHA1 Message Date
xuri 9c278365f2
This closes #1945, an error will be return if column header cell is empty in pivot table data range
- Update unit tests
2024-07-13 10:41:57 +08:00
wxy 307e533061
This closes #1942, fix percent sign missing in formatted result for zero numeric cell value (#1947)
- Updated unit tests
2024-07-12 08:07:19 +08:00
Patrick Wang 431c31029e
This closes #1944, add new TickLabelPosition field in the ChartAxis data type (#1946)
- Introduce new exported ChartTickLabelPositionType enumeration
- Update unit tests
2024-07-11 14:39:16 +08:00
8 changed files with 82 additions and 44 deletions

View File

@ -90,6 +90,19 @@ const (
ChartLineAutomatic
)
// ChartTickLabelPositionType is the type of supported chart tick label position
// types.
type ChartTickLabelPositionType byte
// This section defines the supported chart tick label position types
// enumeration.
const (
ChartTickLabelNextToAxis ChartTickLabelPositionType = iota
ChartTickLabelHigh
ChartTickLabelLow
ChartTickLabelNone
)
// This section defines the default value of chart properties.
var (
chartView3DRotX = map[ChartType]int{
@ -484,7 +497,13 @@ var (
true: "r",
false: "l",
}
valTickLblPos = map[ChartType]string{
tickLblPosVal = map[ChartTickLabelPositionType]string{
ChartTickLabelNextToAxis: "nextTo",
ChartTickLabelHigh: "high",
ChartTickLabelLow: "low",
ChartTickLabelNone: "none",
}
tickLblPosNone = map[ChartType]string{
Contour: "none",
WireframeContour: "none",
}

View File

@ -237,7 +237,7 @@ func TestAddChart(t *testing.T) {
{sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}},
{sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Scatter Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Doughnut Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}},
{sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}},
{sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, TickLabelPosition: ChartTickLabelLow}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}},
{sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Pie Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}},
// bar series chart

View File

@ -1001,7 +1001,7 @@ func (f *File) drawPlotAreaCatAx(pa *cPlotArea, opts *Chart) []*cAxs {
MajorTickMark: &attrValString{Val: stringPtr("none")},
MinorTickMark: &attrValString{Val: stringPtr("none")},
Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""),
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.XAxis.TickLabelPosition])},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(&opts.XAxis),
CrossAx: &attrValInt{Val: intPtr(100000001)},
@ -1063,7 +1063,7 @@ func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs {
},
MajorTickMark: &attrValString{Val: stringPtr("none")},
MinorTickMark: &attrValString{Val: stringPtr("none")},
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.YAxis.TickLabelPosition])},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(&opts.YAxis),
CrossAx: &attrValInt{Val: intPtr(100000000)},
@ -1079,7 +1079,7 @@ func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs {
if opts.YAxis.MinorGridLines {
ax.MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if pos, ok := valTickLblPos[opts.Type]; ok {
if pos, ok := tickLblPosNone[opts.Type]; ok {
ax.TickLblPos.Val = stringPtr(pos)
}
if opts.YAxis.MajorUnit != 0 {
@ -1115,7 +1115,7 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs {
},
Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)},
AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.YAxis.TickLabelPosition])},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(nil),
CrossAx: &attrValInt{Val: intPtr(100000001)},

View File

@ -4799,10 +4799,8 @@ func format(value, numFmt string, date1904 bool, cellType CellType, opts *Option
switch section.Type {
case nfp.TokenSectionPositive:
return nf.alignmentHandler(nf.positiveHandler())
case nfp.TokenSectionNegative:
return nf.alignmentHandler(nf.negativeHandler())
default:
return nf.alignmentHandler(nf.zeroHandler())
return nf.alignmentHandler(nf.negativeHandler())
}
}
return nf.alignmentHandler(nf.textHandler())
@ -7108,11 +7106,6 @@ func (nf *numberFormat) negativeHandler() (result string) {
return nf.numberHandler()
}
// zeroHandler will be handling zero selection for a number format expression.
func (nf *numberFormat) zeroHandler() string {
return nf.value
}
// textHandler will be handling text selection for a number format expression.
func (nf *numberFormat) textHandler() (result string) {
for _, token := range nf.section[nf.sectionIdx].Items {
@ -7137,21 +7130,18 @@ func (nf *numberFormat) getValueSectionType(value string) (float64, string) {
return 0, nfp.TokenSectionText
}
number, _ := strconv.ParseFloat(value, 64)
if number > 0 {
if number >= 0 {
return number, nfp.TokenSectionPositive
}
if number < 0 {
var hasNeg bool
for _, sec := range nf.section {
if sec.Type == nfp.TokenSectionNegative {
hasNeg = true
}
var hasNeg bool
for _, sec := range nf.section {
if sec.Type == nfp.TokenSectionNegative {
hasNeg = true
}
if !hasNeg {
nf.usePositive = true
return number, nfp.TokenSectionPositive
}
return number, nfp.TokenSectionNegative
}
return number, nfp.TokenSectionZero
if !hasNeg {
nf.usePositive = true
return number, nfp.TokenSectionPositive
}
return number, nfp.TokenSectionNegative
}

View File

@ -69,7 +69,10 @@ func TestNumFmt(t *testing.T) {
{"0.97952546296296295", "h:m", "23:30"},
{"43528", "mmmm", "March"},
{"43528", "dddd", "Monday"},
{"0", ";;;", "0"},
{"0", ";;;", ""},
{"0", "0%", "0%"},
{"0", "0.0%", "0.0%"},
{"0", "0.00%", "0.00%"},
{"43528", "[$-409]MM/DD/YYYY", "03/04/2019"},
{"43528", "[$-409]MM/DD/YYYY am/pm", "03/04/2019 AM"},
{"43528", "[$-111]MM/DD/YYYY", "43528"},

View File

@ -260,6 +260,9 @@ func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) {
if err != nil {
return order, err
}
if name == "" {
return order, ErrParameterInvalid
}
order = append(order, name)
}
return order, nil
@ -272,8 +275,10 @@ func (f *File) addPivotCache(opts *PivotTableOptions) error {
if err != nil {
return newPivotTableDataRangeError(err.Error())
}
// data range has been checked
order, _ := f.getTableFieldsOrder(opts)
order, err := f.getTableFieldsOrder(opts)
if err != nil {
return newPivotTableDataRangeError(err.Error())
}
topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pc := xlsxPivotCacheDefinition{

View File

@ -300,6 +300,7 @@ func TestPivotTable(t *testing.T) {
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot table with unsupported charset content types.
f = NewFile()
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
@ -393,6 +394,25 @@ func TestPivotTableDataRange(t *testing.T) {
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels")
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist")
t.Run("data_range_with_empty_column", func(t *testing.T) {
// Test add pivot table with data range doesn't organized as a list with labeled columns
f := NewFile()
// Create some data in a sheet
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "", "Type"}))
for row := 2; row < 32; row++ {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
}
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Type"}},
}))
})
}
func TestParseFormatPivotTableSet(t *testing.T) {

View File

@ -530,20 +530,21 @@ type ChartNumFmt struct {
// ChartAxis directly maps the format settings of the chart axis.
type ChartAxis struct {
None bool
MajorGridLines bool
MinorGridLines bool
MajorUnit float64
TickLabelSkip int
ReverseOrder bool
Secondary bool
Maximum *float64
Minimum *float64
Font Font
LogBase float64
NumFmt ChartNumFmt
Title []RichTextRun
axID int
None bool
MajorGridLines bool
MinorGridLines bool
MajorUnit float64
TickLabelPosition ChartTickLabelPositionType
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.