excelize/drawing.go
xuri 688808b2b4
This closes #1819, formula calculation engine support array formulas
- Improve the defined name and table name validation rules
- Rename internal variable names to avoid the same with Go 1.21's built-in min and max functions
- Simplify data type conversion in internal code
- Update GitHub Actions workflow configuration, test on Go 1.22.x, and disable Go module cache
- Update dependencies module
2024-02-26 02:22:51 +08:00

1551 lines
46 KiB
Go

// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.18 or later.
package excelize
import (
"bytes"
"encoding/xml"
"io"
"reflect"
"strconv"
"strings"
)
// prepareDrawing provides a function to prepare drawing ID and XML by given
// drawingID, worksheet name and default drawingXML.
func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
if ws.Drawing != nil {
// The worksheet already has a picture or chart relationships, use the
// relationships drawing ../drawings/drawing%d.xml or /xl/drawings/drawing%d.xml.
sheetRelationshipsDrawingXML = strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "/xl/drawings/", "../drawings/")
drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml"))
drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl")
} else {
// Add first picture for given sheet.
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetDrawing(sheet, rID)
}
return drawingID, drawingXML
}
// prepareChartSheetDrawing provides a function to prepare drawing ID and XML
// by given drawingID, worksheet name and default drawingXML.
func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet string) {
sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
// Only allow one chart in a chartsheet.
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/chartsheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "")
f.addSheetNameSpace(sheet, SourceRelationship)
cs.Drawing = &xlsxDrawing{
RID: "rId" + strconv.Itoa(rID),
}
}
// addChart provides a function to create chart as xl/charts/chart%d.xml by
// given format sets.
func (f *File) addChart(opts *Chart, comboCharts []*Chart) {
count := f.countCharts()
xlsxChartSpace := xlsxChartSpace{
XMLNSa: NameSpaceDrawingML.Value,
Date1904: &attrValBool{Val: boolPtr(false)},
Lang: &attrValString{Val: stringPtr("en-US")},
RoundedCorners: &attrValBool{Val: boolPtr(false)},
Chart: cChart{
Title: f.drawPlotAreaTitles(opts.Title, ""),
View3D: &cView3D{
RotX: &attrValInt{Val: intPtr(chartView3DRotX[opts.Type])},
RotY: &attrValInt{Val: intPtr(chartView3DRotY[opts.Type])},
Perspective: &attrValInt{Val: intPtr(chartView3DPerspective[opts.Type])},
RAngAx: &attrValInt{Val: intPtr(chartView3DRAngAx[opts.Type])},
},
Floor: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
},
SideWall: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
},
BackWall: &cThicknessSpPr{
Thickness: &attrValInt{Val: intPtr(0)},
},
PlotArea: &cPlotArea{},
Legend: &cLegend{
LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[opts.Legend.Position])},
Overlay: &attrValBool{Val: boolPtr(false)},
},
PlotVisOnly: &attrValBool{Val: boolPtr(false)},
DispBlanksAs: &attrValString{Val: stringPtr(opts.ShowBlanksAs)},
ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)},
},
SpPr: &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "bg1"},
},
Ln: f.drawChartLn(&opts.Border),
},
PrintSettings: &cPrintSettings{
PageMargins: &cPageMargins{
B: 0.75,
L: 0.7,
R: 0.7,
T: 0.7,
Header: 0.3,
Footer: 0.3,
},
},
}
xlsxChartSpace.SpPr = f.drawShapeFill(opts.Fill, xlsxChartSpace.SpPr)
plotAreaFunc := map[ChartType]func(*Chart) *cPlotArea{
Area: f.drawBaseChart,
AreaStacked: f.drawBaseChart,
AreaPercentStacked: f.drawBaseChart,
Area3D: f.drawBaseChart,
Area3DStacked: f.drawBaseChart,
Area3DPercentStacked: f.drawBaseChart,
Bar: f.drawBaseChart,
BarStacked: f.drawBaseChart,
BarPercentStacked: f.drawBaseChart,
Bar3DClustered: f.drawBaseChart,
Bar3DStacked: f.drawBaseChart,
Bar3DPercentStacked: f.drawBaseChart,
Bar3DConeClustered: f.drawBaseChart,
Bar3DConeStacked: f.drawBaseChart,
Bar3DConePercentStacked: f.drawBaseChart,
Bar3DPyramidClustered: f.drawBaseChart,
Bar3DPyramidStacked: f.drawBaseChart,
Bar3DPyramidPercentStacked: f.drawBaseChart,
Bar3DCylinderClustered: f.drawBaseChart,
Bar3DCylinderStacked: f.drawBaseChart,
Bar3DCylinderPercentStacked: f.drawBaseChart,
Col: f.drawBaseChart,
ColStacked: f.drawBaseChart,
ColPercentStacked: f.drawBaseChart,
Col3D: f.drawBaseChart,
Col3DClustered: f.drawBaseChart,
Col3DStacked: f.drawBaseChart,
Col3DPercentStacked: f.drawBaseChart,
Col3DCone: f.drawBaseChart,
Col3DConeClustered: f.drawBaseChart,
Col3DConeStacked: f.drawBaseChart,
Col3DConePercentStacked: f.drawBaseChart,
Col3DPyramid: f.drawBaseChart,
Col3DPyramidClustered: f.drawBaseChart,
Col3DPyramidStacked: f.drawBaseChart,
Col3DPyramidPercentStacked: f.drawBaseChart,
Col3DCylinder: f.drawBaseChart,
Col3DCylinderClustered: f.drawBaseChart,
Col3DCylinderStacked: f.drawBaseChart,
Col3DCylinderPercentStacked: f.drawBaseChart,
Doughnut: f.drawDoughnutChart,
Line: f.drawLineChart,
Line3D: f.drawLine3DChart,
Pie: f.drawPieChart,
Pie3D: f.drawPie3DChart,
PieOfPie: f.drawPieOfPieChart,
BarOfPie: f.drawBarOfPieChart,
Radar: f.drawRadarChart,
Scatter: f.drawScatterChart,
Surface3D: f.drawSurface3DChart,
WireframeSurface3D: f.drawSurface3DChart,
Contour: f.drawSurfaceChart,
WireframeContour: f.drawSurfaceChart,
Bubble: f.drawBubbleChart,
Bubble3D: f.drawBubbleChart,
}
if opts.Legend.Position == "none" {
xlsxChartSpace.Chart.Legend = nil
}
xlsxChartSpace.Chart.PlotArea.SpPr = f.drawShapeFill(opts.PlotArea.Fill, xlsxChartSpace.Chart.PlotArea.SpPr)
addChart := func(c, p *cPlotArea) {
immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem()
for i := 0; i < mutable.NumField(); i++ {
field := mutable.Field(i)
if field.IsNil() {
continue
}
immutable.FieldByName(mutable.Type().Field(i).Name).Set(field)
}
}
addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](opts))
order := len(opts.Series)
for idx := range comboCharts {
comboCharts[idx].order = order
addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx]))
order += len(comboCharts[idx].Series)
}
chart, _ := xml.Marshal(xlsxChartSpace)
media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml"
f.saveFileList(media, chart)
}
// drawBaseChart provides a function to draw the c:plotArea element for bar,
// and column series charts by given format sets.
func (f *File) drawBaseChart(opts *Chart) *cPlotArea {
c := cCharts{
BarDir: &attrValString{
Val: stringPtr("col"),
},
Grouping: &attrValString{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
Shape: f.drawChartShape(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
Overlap: &attrValInt{Val: intPtr(100)},
}
var ok bool
if *c.BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok {
c.BarDir = nil
}
if *c.Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok {
c.Overlap = nil
}
catAx := f.drawPlotAreaCatAx(opts)
valAx := f.drawPlotAreaValAx(opts)
charts := map[ChartType]*cPlotArea{
Area: {
AreaChart: &c,
CatAx: catAx,
ValAx: valAx,
},
AreaStacked: {
AreaChart: &c,
CatAx: catAx,
ValAx: valAx,
},
AreaPercentStacked: {
AreaChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Area3D: {
Area3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Area3DStacked: {
Area3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Area3DPercentStacked: {
Area3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar: {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
BarStacked: {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
BarPercentStacked: {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DPercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DConeClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DConeStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DConePercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DPyramidClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DPyramidStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DPyramidPercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DCylinderClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DCylinderStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bar3DCylinderPercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col: {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
ColStacked: {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
ColPercentStacked: {
BarChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3D: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DPercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DCone: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DConeClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DConeStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DConePercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DPyramid: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DPyramidClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DPyramidStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DPyramidPercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DCylinder: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DCylinderClustered: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DCylinderStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Col3DCylinderPercentStacked: {
Bar3DChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bubble: {
BubbleChart: &c,
CatAx: catAx,
ValAx: valAx,
},
Bubble3D: {
BubbleChart: &c,
CatAx: catAx,
ValAx: valAx,
},
}
return charts[opts.Type]
}
// drawDoughnutChart provides a function to draw the c:plotArea element for
// doughnut chart by given format sets.
func (f *File) drawDoughnutChart(opts *Chart) *cPlotArea {
holeSize := 75
if opts.HoleSize > 0 && opts.HoleSize <= 90 {
holeSize = opts.HoleSize
}
return &cPlotArea{
DoughnutChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
HoleSize: &attrValInt{Val: intPtr(holeSize)},
},
}
}
// drawLineChart provides a function to draw the c:plotArea element for line
// chart by given format sets.
func (f *File) drawLineChart(opts *Chart) *cPlotArea {
return &cPlotArea{
LineChart: &cCharts{
Grouping: &attrValString{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawLine3DChart provides a function to draw the c:plotArea element for line
// chart by given format sets.
func (f *File) drawLine3DChart(opts *Chart) *cPlotArea {
return &cPlotArea{
Line3DChart: &cCharts{
Grouping: &attrValString{
Val: stringPtr(plotAreaChartGrouping[opts.Type]),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawPieChart provides a function to draw the c:plotArea element for pie
// chart by given format sets.
func (f *File) drawPieChart(opts *Chart) *cPlotArea {
return &cPlotArea{
PieChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
},
}
}
// drawPie3DChart provides a function to draw the c:plotArea element for 3D
// pie chart by given format sets.
func (f *File) drawPie3DChart(opts *Chart) *cPlotArea {
return &cPlotArea{
Pie3DChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
},
}
}
// drawPieOfPieChart provides a function to draw the c:plotArea element for
// pie chart by given format sets.
func (f *File) drawPieOfPieChart(opts *Chart) *cPlotArea {
var splitPos *attrValInt
if opts.PlotArea.SecondPlotValues > 0 {
splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)}
}
return &cPlotArea{
OfPieChart: &cCharts{
OfPieType: &attrValString{
Val: stringPtr("pie"),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
SplitPos: splitPos,
SerLines: &attrValString{},
},
}
}
// drawBarOfPieChart provides a function to draw the c:plotArea element for
// pie chart by given format sets.
func (f *File) drawBarOfPieChart(opts *Chart) *cPlotArea {
var splitPos *attrValInt
if opts.PlotArea.SecondPlotValues > 0 {
splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)}
}
return &cPlotArea{
OfPieChart: &cCharts{
OfPieType: &attrValString{
Val: stringPtr("bar"),
},
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
SplitPos: splitPos,
Ser: f.drawChartSeries(opts),
SerLines: &attrValString{},
},
}
}
// drawRadarChart provides a function to draw the c:plotArea element for radar
// chart by given format sets.
func (f *File) drawRadarChart(opts *Chart) *cPlotArea {
return &cPlotArea{
RadarChart: &cCharts{
RadarStyle: &attrValString{
Val: stringPtr("marker"),
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawScatterChart provides a function to draw the c:plotArea element for
// scatter chart by given format sets.
func (f *File) drawScatterChart(opts *Chart) *cPlotArea {
return &cPlotArea{
ScatterChart: &cCharts{
ScatterStyle: &attrValString{
Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker
},
VaryColors: &attrValBool{
Val: boolPtr(false),
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
}
}
// drawSurface3DChart provides a function to draw the c:surface3DChart element by
// given format sets.
func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
Surface3DChart: &cCharts{
Ser: f.drawChartSeries(opts),
AxID: []*attrValInt{
{Val: intPtr(100000000)},
{Val: intPtr(100000001)},
{Val: intPtr(100000005)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
SerAx: f.drawPlotAreaSerAx(opts),
}
if opts.Type == WireframeSurface3D {
plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)}
}
return plotArea
}
// drawSurfaceChart provides a function to draw the c:surfaceChart element by
// given format sets.
func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
SurfaceChart: &cCharts{
Ser: f.drawChartSeries(opts),
AxID: []*attrValInt{
{Val: intPtr(100000000)},
{Val: intPtr(100000001)},
{Val: intPtr(100000005)},
},
},
CatAx: f.drawPlotAreaCatAx(opts),
ValAx: f.drawPlotAreaValAx(opts),
SerAx: f.drawPlotAreaSerAx(opts),
}
if opts.Type == WireframeContour {
plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)}
}
return plotArea
}
// drawBubbleChart provides a function to draw the c:bubbleChart element by
// given format sets.
func (f *File) drawBubbleChart(opts *Chart) *cPlotArea {
plotArea := &cPlotArea{
BubbleChart: &cCharts{
VaryColors: &attrValBool{
Val: opts.VaryColors,
},
Ser: f.drawChartSeries(opts),
DLbls: f.drawChartDLbls(opts),
AxID: f.genAxID(opts),
},
ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]},
}
if opts.BubbleSize > 0 && opts.BubbleSize <= 300 {
plotArea.BubbleChart.BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))}
}
return plotArea
}
// drawChartShape provides a function to draw the c:shape element by given
// format sets.
func (f *File) drawChartShape(opts *Chart) *attrValString {
shapes := map[ChartType]string{
Bar3DConeClustered: "cone",
Bar3DConeStacked: "cone",
Bar3DConePercentStacked: "cone",
Bar3DPyramidClustered: "pyramid",
Bar3DPyramidStacked: "pyramid",
Bar3DPyramidPercentStacked: "pyramid",
Bar3DCylinderClustered: "cylinder",
Bar3DCylinderStacked: "cylinder",
Bar3DCylinderPercentStacked: "cylinder",
Col3DCone: "cone",
Col3DConeClustered: "cone",
Col3DConeStacked: "cone",
Col3DConePercentStacked: "cone",
Col3DPyramid: "pyramid",
Col3DPyramidClustered: "pyramid",
Col3DPyramidStacked: "pyramid",
Col3DPyramidPercentStacked: "pyramid",
Col3DCylinder: "cylinder",
Col3DCylinderClustered: "cylinder",
Col3DCylinderStacked: "cylinder",
Col3DCylinderPercentStacked: "cylinder",
}
if shape, ok := shapes[opts.Type]; ok {
return &attrValString{Val: stringPtr(shape)}
}
return nil
}
// drawChartSeries provides a function to draw the c:ser element by given
// format sets.
func (f *File) drawChartSeries(opts *Chart) *[]cSer {
var ser []cSer
for k := range opts.Series {
ser = append(ser, cSer{
IDx: &attrValInt{Val: intPtr(k + opts.order)},
Order: &attrValInt{Val: intPtr(k + opts.order)},
Tx: &cTx{
StrRef: &cStrRef{
F: opts.Series[k].Name,
},
},
SpPr: f.drawChartSeriesSpPr(k, opts),
Marker: f.drawChartSeriesMarker(k, opts),
DPt: f.drawChartSeriesDPt(k, opts),
DLbls: f.drawChartSeriesDLbls(k, opts),
InvertIfNegative: &attrValBool{Val: boolPtr(false)},
Cat: f.drawChartSeriesCat(opts.Series[k], opts),
Smooth: &attrValBool{Val: boolPtr(opts.Series[k].Line.Smooth)},
Val: f.drawChartSeriesVal(opts.Series[k], opts),
XVal: f.drawChartSeriesXVal(opts.Series[k], opts),
YVal: f.drawChartSeriesYVal(opts.Series[k], opts),
BubbleSize: f.drawCharSeriesBubbleSize(opts.Series[k], opts),
Bubble3D: f.drawCharSeriesBubble3D(opts),
})
}
return &ser
}
// drawShapeFill provides a function to draw the a:solidFill element by given
// fill format sets.
func (f *File) drawShapeFill(fill Fill, spPr *cSpPr) *cSpPr {
if fill.Type == "pattern" && fill.Pattern == 1 {
if spPr == nil {
spPr = &cSpPr{}
}
if len(fill.Color) == 1 {
spPr.SolidFill = &aSolidFill{SrgbClr: &attrValString{Val: stringPtr(strings.TrimPrefix(fill.Color[0], "#"))}}
return spPr
}
spPr.SolidFill = nil
spPr.NoFill = stringPtr("")
}
return spPr
}
// drawChartSeriesSpPr provides a function to draw the c:spPr element by given
// format sets.
func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr {
spPr := &cSpPr{SolidFill: &aSolidFill{SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)}}}
spPr = f.drawShapeFill(opts.Series[i].Fill, spPr)
spPrScatter := &cSpPr{
Ln: &aLn{
W: 25400,
NoFill: &attrValString{},
},
}
spPrLine := &cSpPr{
Ln: &aLn{
W: f.ptToEMUs(opts.Series[i].Line.Width),
Cap: "rnd", // rnd, sq, flat
SolidFill: spPr.SolidFill,
},
}
if chartSeriesSpPr, ok := map[ChartType]*cSpPr{
Line: spPrLine, Scatter: spPrScatter,
}[opts.Type]; ok {
return chartSeriesSpPr
}
if spPr.SolidFill.SrgbClr != nil {
return spPr
}
return nil
}
// drawChartSeriesDPt provides a function to draw the c:dPt element by given
// data index and format sets.
func (f *File) drawChartSeriesDPt(i int, opts *Chart) []*cDPt {
dpt := []*cDPt{{
IDx: &attrValInt{Val: intPtr(i)},
Bubble3D: &attrValBool{Val: boolPtr(false)},
SpPr: &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)},
},
Ln: &aLn{
W: 25400,
Cap: "rnd",
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)},
},
},
Sp3D: &aSp3D{
ContourW: 25400,
ContourClr: &aContourClr{
SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)},
},
},
},
}}
chartSeriesDPt := map[ChartType][]*cDPt{Pie: dpt, Pie3D: dpt}
return chartSeriesDPt[opts.Type]
}
// drawChartSeriesCat provides a function to draw the c:cat element by given
// chart series and format sets.
func (f *File) drawChartSeriesCat(v ChartSeries, opts *Chart) *cCat {
cat := &cCat{
StrRef: &cStrRef{
F: v.Categories,
},
}
chartSeriesCat := map[ChartType]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil}
if _, ok := chartSeriesCat[opts.Type]; ok || v.Categories == "" {
return nil
}
return cat
}
// drawChartSeriesVal provides a function to draw the c:val element by given
// chart series and format sets.
func (f *File) drawChartSeriesVal(v ChartSeries, opts *Chart) *cVal {
val := &cVal{
NumRef: &cNumRef{
F: v.Values,
},
}
chartSeriesVal := map[ChartType]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil}
if _, ok := chartSeriesVal[opts.Type]; ok {
return nil
}
return val
}
// drawChartSeriesMarker provides a function to draw the c:marker element by
// given data index and format sets.
func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker {
defaultSymbol := map[ChartType]*attrValString{Scatter: {Val: stringPtr("circle")}}
marker := &cMarker{
Symbol: defaultSymbol[opts.Type],
Size: &attrValInt{Val: intPtr(5)},
}
if symbol := stringPtr(opts.Series[i].Marker.Symbol); *symbol != "" {
marker.Symbol = &attrValString{Val: symbol}
}
if size := intPtr(opts.Series[i].Marker.Size); *size != 0 {
marker.Size = &attrValInt{Val: size}
}
if i < 6 {
marker.SpPr = &cSpPr{
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "accent" + strconv.Itoa(i+1),
},
},
Ln: &aLn{
W: 9252,
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "accent" + strconv.Itoa(i+1),
},
},
},
}
}
marker.SpPr = f.drawShapeFill(opts.Series[i].Marker.Fill, marker.SpPr)
chartSeriesMarker := map[ChartType]*cMarker{Scatter: marker, Line: marker}
return chartSeriesMarker[opts.Type]
}
// drawChartSeriesXVal provides a function to draw the c:xVal element by given
// chart series and format sets.
func (f *File) drawChartSeriesXVal(v ChartSeries, opts *Chart) *cCat {
cat := &cCat{
StrRef: &cStrRef{
F: v.Categories,
},
}
chartSeriesXVal := map[ChartType]*cCat{Scatter: cat, Bubble: cat, Bubble3D: cat}
return chartSeriesXVal[opts.Type]
}
// drawChartSeriesYVal provides a function to draw the c:yVal element by given
// chart series and format sets.
func (f *File) drawChartSeriesYVal(v ChartSeries, opts *Chart) *cVal {
val := &cVal{
NumRef: &cNumRef{
F: v.Values,
},
}
chartSeriesYVal := map[ChartType]*cVal{Scatter: val, Bubble: val, Bubble3D: val}
return chartSeriesYVal[opts.Type]
}
// drawCharSeriesBubbleSize provides a function to draw the c:bubbleSize
// element by given chart series and format sets.
func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal {
if _, ok := map[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok {
return nil
}
fVal := v.Values
if v.Sizes != "" {
fVal = v.Sizes
}
return &cVal{
NumRef: &cNumRef{
F: fVal,
},
}
}
// drawCharSeriesBubble3D provides a function to draw the c:bubble3D element
// by given format sets.
func (f *File) drawCharSeriesBubble3D(opts *Chart) *attrValBool {
if _, ok := map[ChartType]bool{Bubble3D: true}[opts.Type]; !ok {
return nil
}
return &attrValBool{Val: boolPtr(true)}
}
// drawChartNumFmt provides a function to draw the c:numFmt element by given
// data labels format sets.
func (f *File) drawChartNumFmt(labels ChartNumFmt) *cNumFmt {
var numFmt *cNumFmt
if labels.CustomNumFmt != "" || labels.SourceLinked {
numFmt = &cNumFmt{
FormatCode: labels.CustomNumFmt,
SourceLinked: labels.SourceLinked,
}
}
return numFmt
}
// drawChartDLbls provides a function to draw the c:dLbls element by given
// format sets.
func (f *File) drawChartDLbls(opts *Chart) *cDLbls {
return &cDLbls{
NumFmt: f.drawChartNumFmt(opts.PlotArea.NumFmt),
ShowLegendKey: &attrValBool{Val: boolPtr(opts.Legend.ShowLegendKey)},
ShowVal: &attrValBool{Val: boolPtr(opts.PlotArea.ShowVal)},
ShowCatName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowCatName)},
ShowSerName: &attrValBool{Val: boolPtr(opts.PlotArea.ShowSerName)},
ShowBubbleSize: &attrValBool{Val: boolPtr(opts.PlotArea.ShowBubbleSize)},
ShowPercent: &attrValBool{Val: boolPtr(opts.PlotArea.ShowPercent)},
ShowLeaderLines: &attrValBool{Val: boolPtr(opts.PlotArea.ShowLeaderLines)},
}
}
// inSupportedChartDataLabelsPositionType provides a method to check if an
// element is present in an array, and return the index of its location,
// otherwise return -1.
func inSupportedChartDataLabelsPositionType(a []ChartDataLabelPositionType, x ChartDataLabelPositionType) int {
for idx, n := range a {
if x == n {
return idx
}
}
return -1
}
// drawChartSeriesDLbls provides a function to draw the c:dLbls element by
// given format sets.
func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls {
dLbls := f.drawChartDLbls(opts)
chartSeriesDLbls := map[ChartType]*cDLbls{
Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil,
}
if _, ok := chartSeriesDLbls[opts.Type]; ok {
return nil
}
if types, ok := supportedChartDataLabelsPosition[opts.Type]; ok && opts.Series[i].DataLabelPosition != ChartDataLabelsPositionUnset {
if inSupportedChartDataLabelsPositionType(types, opts.Series[i].DataLabelPosition) != -1 {
dLbls.DLblPos = &attrValString{Val: stringPtr(chartDataLabelsPositionTypes[opts.Series[i].DataLabelPosition])}
}
}
return dLbls
}
// drawPlotAreaCatAx provides a function to draw the c:catAx element.
func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs {
maxVal := &attrValFloat{Val: opts.XAxis.Maximum}
minVal := &attrValFloat{Val: opts.XAxis.Minimum}
if opts.XAxis.Maximum == nil {
maxVal = nil
}
if opts.XAxis.Minimum == nil {
minVal = nil
}
axs := []*cAxs{
{
AxID: &attrValInt{Val: intPtr(100000000)},
Scaling: &cScaling{
Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])},
Max: maxVal,
Min: minVal,
},
Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)},
AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
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),
CrossAx: &attrValInt{Val: intPtr(100000001)},
Crosses: &attrValString{Val: stringPtr("autoZero")},
Auto: &attrValBool{Val: boolPtr(true)},
LblAlgn: &attrValString{Val: stringPtr("ctr")},
LblOffset: &attrValInt{Val: intPtr(100)},
NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)},
},
}
if numFmt := f.drawChartNumFmt(opts.XAxis.NumFmt); numFmt != nil {
axs[0].NumFmt = numFmt
}
if opts.XAxis.MajorGridLines {
axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if opts.XAxis.MinorGridLines {
axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
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: maxVal,
Min: minVal,
},
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
}
// drawPlotAreaValAx provides a function to draw the c:valAx element.
func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs {
maxVal := &attrValFloat{Val: opts.YAxis.Maximum}
minVal := &attrValFloat{Val: opts.YAxis.Minimum}
if opts.YAxis.Maximum == nil {
maxVal = nil
}
if opts.YAxis.Minimum == nil {
minVal = nil
}
var logBase *attrValFloat
if opts.YAxis.LogBase >= 2 && opts.YAxis.LogBase <= 1000 {
logBase = &attrValFloat{Val: float64Ptr(opts.YAxis.LogBase)}
}
axs := []*cAxs{
{
AxID: &attrValInt{Val: intPtr(100000001)},
Scaling: &cScaling{
LogBase: logBase,
Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])},
Max: maxVal,
Min: minVal,
},
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],
},
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(100000000)},
Crosses: &attrValString{Val: stringPtr("autoZero")},
CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])},
},
}
if numFmt := f.drawChartNumFmt(opts.YAxis.NumFmt); numFmt != nil {
axs[0].NumFmt = numFmt
}
if opts.YAxis.MajorGridLines {
axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if opts.YAxis.MinorGridLines {
axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()}
}
if pos, ok := valTickLblPos[opts.Type]; ok {
axs[0].TickLblPos.Val = stringPtr(pos)
}
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: maxVal,
Min: minVal,
},
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
}
// drawPlotAreaSerAx provides a function to draw the c:serAx element.
func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs {
maxVal := &attrValFloat{Val: opts.YAxis.Maximum}
minVal := &attrValFloat{Val: opts.YAxis.Minimum}
if opts.YAxis.Maximum == nil {
maxVal = nil
}
if opts.YAxis.Minimum == nil {
minVal = nil
}
return []*cAxs{
{
AxID: &attrValInt{Val: intPtr(100000005)},
Scaling: &cScaling{
Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])},
Max: maxVal,
Min: minVal,
},
Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)},
AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])},
TickLblPos: &attrValString{Val: stringPtr("nextTo")},
SpPr: f.drawPlotAreaSpPr(),
TxPr: f.drawPlotAreaTxPr(nil),
CrossAx: &attrValInt{Val: intPtr(100000001)},
},
}
}
// drawChartFont provides a function to draw the a:rPr element.
func drawChartFont(fnt *Font, r *aRPr) {
if fnt == nil {
return
}
r.B = fnt.Bold
r.I = fnt.Italic
if idx := inStrSlice(supportedDrawingUnderlineTypes, fnt.Underline, true); idx != -1 {
r.U = supportedDrawingUnderlineTypes[idx]
}
if fnt.Color != "" {
if r.SolidFill == nil {
r.SolidFill = &aSolidFill{}
}
r.SolidFill.SchemeClr = nil
r.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(fnt.Color), "#", ""))}
}
if fnt.Family != "" {
r.Latin.Typeface = fnt.Family
}
if fnt.Size > 0 {
r.Sz = fnt.Size * 100
}
if fnt.Strike {
r.Strike = "sngStrike"
}
}
// 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}
drawChartFont(run.Font, &r.RPr)
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{
Ln: &aLn{
W: 9525,
Cap: "flat",
Cmpd: "sng",
Algn: "ctr",
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{Val: intPtr(15000)},
LumOff: &attrValInt{Val: intPtr(85000)},
},
},
},
}
}
// drawPlotAreaTxPr provides a function to draw the c:txPr element.
func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr {
cTxPr := &cTxPr{
BodyPr: aBodyPr{
Rot: -60000000,
SpcFirstLastPara: true,
VertOverflow: "ellipsis",
Vert: "horz",
Wrap: "square",
Anchor: "ctr",
AnchorCtr: true,
},
P: aP{
PPr: &aPPr{
DefRPr: aRPr{
Sz: 900,
B: false,
I: false,
U: "none",
Strike: "noStrike",
Kern: 1200,
Baseline: 0,
SolidFill: &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{Val: intPtr(15000)},
LumOff: &attrValInt{Val: intPtr(85000)},
},
},
Latin: &xlsxCTTextFont{Typeface: "+mn-lt"},
Ea: &aEa{Typeface: "+mn-ea"},
Cs: &aCs{Typeface: "+mn-cs"},
},
},
EndParaRPr: &aEndParaRPr{Lang: "en-US"},
},
}
if opts != nil {
drawChartFont(&opts.Font, &cTxPr.P.PPr.DefRPr)
}
return cTxPr
}
// drawChartLn provides a function to draw the a:ln element.
func (f *File) drawChartLn(opts *ChartLine) *aLn {
ln := &aLn{
W: f.ptToEMUs(opts.Width),
Cap: "flat",
Cmpd: "sng",
Algn: "ctr",
}
switch opts.Type {
case ChartLineSolid:
ln.SolidFill = &aSolidFill{
SchemeClr: &aSchemeClr{
Val: "tx1",
LumMod: &attrValInt{
Val: intPtr(15000),
},
LumOff: &attrValInt{
Val: intPtr(85000),
},
},
}
return ln
case ChartLineNone:
ln.NoFill = &attrValString{}
return ln
default:
return nil
}
}
// drawingParser provides a function to parse drawingXML. In order to solve
// the problem that the label structure is changed after serialization and
// deserialization, two different structures: decodeWsDr and encodeWsDr are
// defined.
func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) {
var (
err error
ok bool
)
_, ok = f.Drawings.Load(path)
if !ok {
content := xlsxWsDr{
NS: NameSpaceDrawingMLSpreadSheet.Value,
Xdr: NameSpaceDrawingMLSpreadSheet.Value,
A: NameSpaceDrawingML.Value,
}
if _, ok = f.Pkg.Load(path); ok { // Append Model
decodeWsDr := decodeWsDr{}
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))).
Decode(&decodeWsDr); err != nil && err != io.EOF {
return nil, 0, err
}
content.R = decodeWsDr.R
for _, v := range decodeWsDr.AlternateContent {
content.AlternateContent = append(content.AlternateContent, &xlsxAlternateContent{
Content: v.Content,
XMLNSMC: SourceRelationshipCompatibility.Value,
})
}
for _, v := range decodeWsDr.OneCellAnchor {
content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{
EditAs: v.EditAs,
GraphicFrame: v.Content,
})
}
for _, v := range decodeWsDr.TwoCellAnchor {
content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{
EditAs: v.EditAs,
GraphicFrame: v.Content,
})
}
}
f.Drawings.Store(path, &content)
}
var wsDr *xlsxWsDr
if drawing, ok := f.Drawings.Load(path); ok && drawing != nil {
wsDr = drawing.(*xlsxWsDr)
}
wsDr.mu.Lock()
defer wsDr.mu.Unlock()
return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2, nil
}
// addDrawingChart provides a function to add chart graphic frame by given
// sheet, drawingXML, cell, width, height, relationship index and format sets.
func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, opts *GraphicOptions) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
width = int(float64(width) * opts.ScaleX)
height = int(float64(height) * opts.ScaleY)
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
}
twoCellAnchor := xdrCellAnchor{}
twoCellAnchor.EditAs = opts.Positioning
from := xlsxFrom{}
from.Col = colStart
from.ColOff = opts.OffsetX * EMU
from.Row = rowStart
from.RowOff = opts.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
to.Row = rowEnd
to.RowOff = y2 * EMU
twoCellAnchor.From = &from
twoCellAnchor.To = &to
graphicFrame := xlsxGraphicFrame{
NvGraphicFramePr: xlsxNvGraphicFramePr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
Name: "Chart " + strconv.Itoa(cNvPrID),
},
},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
URI: NameSpaceDrawingMLChart.Value,
Chart: &xlsxChart{
C: NameSpaceDrawingMLChart.Value,
R: SourceRelationship.Value,
RID: "rId" + strconv.Itoa(rID),
},
},
},
}
graphic, _ := xml.Marshal(graphicFrame)
twoCellAnchor.GraphicFrame = string(graphic)
twoCellAnchor.ClientData = &xdrClientData{
FLocksWithSheet: *opts.Locked,
FPrintsWithSheet: *opts.PrintObject,
}
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
f.Drawings.Store(drawingXML, content)
return err
}
// addSheetDrawingChart provides a function to add chart graphic frame for
// chartsheet by given sheet, drawingXML, width, height, relationship index
// and format sets.
func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOptions) error {
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
}
absoluteAnchor := xdrCellAnchor{
EditAs: opts.Positioning,
Pos: &xlsxPoint2D{},
Ext: &aExt{},
}
graphicFrame := xlsxGraphicFrame{
NvGraphicFramePr: xlsxNvGraphicFramePr{
CNvPr: &xlsxCNvPr{
ID: cNvPrID,
Name: "Chart " + strconv.Itoa(cNvPrID),
},
},
Graphic: &xlsxGraphic{
GraphicData: &xlsxGraphicData{
URI: NameSpaceDrawingMLChart.Value,
Chart: &xlsxChart{
C: NameSpaceDrawingMLChart.Value,
R: SourceRelationship.Value,
RID: "rId" + strconv.Itoa(rID),
},
},
},
}
graphic, _ := xml.Marshal(graphicFrame)
absoluteAnchor.GraphicFrame = string(graphic)
absoluteAnchor.ClientData = &xdrClientData{
FLocksWithSheet: *opts.Locked,
FPrintsWithSheet: *opts.PrintObject,
}
content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor)
f.Drawings.Store(drawingXML, content)
return err
}
// deleteDrawing provides a function to delete the chart graphic frame and
// returns deleted embed relationships ID (for unique picture cell anchor) by
// given coordinates and graphic type.
func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (string, error) {
var (
err error
rID string
rIDs []string
wsDr *xlsxWsDr
deTwoCellAnchor *decodeCellAnchor
)
xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{
"Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil },
"Pic": func(anchor *xdrCellAnchor) bool { return anchor.Pic != nil },
}
decodeCellAnchorFuncs := map[string]func(anchor *decodeCellAnchor) bool{
"Chart": func(anchor *decodeCellAnchor) bool { return anchor.Pic == nil },
"Pic": func(anchor *decodeCellAnchor) bool { return anchor.Pic != nil },
}
onAnchorCell := func(c, r int) bool { return c == col && r == row }
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return rID, err
}
for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ {
if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) {
if onAnchorCell(wsDr.TwoCellAnchor[idx].From.Col, wsDr.TwoCellAnchor[idx].From.Row) {
rID, _ = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs)
wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...)
idx--
continue
}
_, rIDs = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs)
}
}
for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ {
deTwoCellAnchor = new(decodeCellAnchor)
if err = f.xmlNewDecoder(strings.NewReader("<decodeCellAnchor>" + wsDr.TwoCellAnchor[idx].GraphicFrame + "</decodeCellAnchor>")).
Decode(deTwoCellAnchor); err != nil && err != io.EOF {
return rID, err
}
if err = nil; deTwoCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deTwoCellAnchor) {
if onAnchorCell(deTwoCellAnchor.From.Col, deTwoCellAnchor.From.Row) {
rID, _ = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs)
wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...)
idx--
continue
}
_, rIDs = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs)
}
}
if inStrSlice(rIDs, rID, true) != -1 {
rID = ""
}
f.Drawings.Store(drawingXML, wsDr)
return rID, err
}
// extractEmbedRID returns embed relationship ID and all relationship ID lists
// for giving cell anchor.
func extractEmbedRID(pic *xlsxPic, decodePic *decodePic, rIDs []string) (string, []string) {
if pic != nil {
rIDs = append(rIDs, pic.BlipFill.Blip.Embed)
return pic.BlipFill.Blip.Embed, rIDs
}
if decodePic != nil {
rIDs = append(rIDs, decodePic.BlipFill.Blip.Embed)
return decodePic.BlipFill.Blip.Embed, rIDs
}
return "", rIDs
}
// deleteDrawingRels provides a function to delete relationships in
// xl/drawings/_rels/drawings%d.xml.rels by giving drawings relationships path
// and relationship ID.
func (f *File) deleteDrawingRels(rels, rID string) {
drawingRels, _ := f.relsReader(rels)
if drawingRels == nil {
drawingRels = &xlsxRelationships{}
}
drawingRels.mu.Lock()
defer drawingRels.mu.Unlock()
for k, v := range drawingRels.Relationships {
if v.ID == rID {
drawingRels.Relationships = append(drawingRels.Relationships[:k], drawingRels.Relationships[k+1:]...)
}
}
f.Relationships.Store(rels, drawingRels)
}
// 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)}}
}