1 /*
2 * Copyright (c) 2010 Sebastian Sauer <sebsauer@kdab.com>
3 * Copyright (c) 2010 Carlos Licea <carlos@kdab.com>
4 * Copyright (c) 2014 Inge Wallin <inge@lysator.liu.se>
5 *
6 * This library is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published
8 * by the Free Software Foundation; either version 2.1 of the License, or
9 * (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20
21 // Own
22 #include "KoOdfChartWriter.h"
23
24 // libstdc++
25 #include <algorithm> // For std:find()
26
27 // Calligra
28 #include <KoStore.h>
29 #include <KoXmlWriter.h>
30 #include <KoOdfWriteStore.h>
31 #include <KoStoreDevice.h>
32 #include <KoGenStyles.h>
33 #include <KoGenStyle.h>
34
35 #include "Odf2Debug.h"
36 #include <Charting.h>
37 #include "NumberFormatParser.h"
38
39
40 // Print the content of generated content.xml to the console for debugging purpose
41 //#define CONTENTXML_DEBUG
42
43 using namespace KoChart;
44
KoOdfChartWriter(KoChart::Chart * chart)45 KoOdfChartWriter::KoOdfChartWriter(KoChart::Chart* chart)
46 : m_x(0)
47 , m_y(0)
48 , m_width(0)
49 , m_height(0)
50 , m_end_x(0)
51 , m_end_y(0)
52 , m_chart(chart)
53 , sheetReplacement(true)
54 , paletteIsSet(false)
55 {
56 Q_ASSERT(m_chart);
57 m_drawLayer = false;
58 }
59
~KoOdfChartWriter()60 KoOdfChartWriter::~KoOdfChartWriter()
61 {
62 }
63
64
65 // Takes a Excel cellrange and translates it into a ODF cellrange
normalizeCellRange(QString range)66 QString KoOdfChartWriter::normalizeCellRange(QString range)
67 {
68 if (range.startsWith('[') && range.endsWith(']')) {
69 range.remove(0, 1).chop(1);
70 }
71 range.remove('$');
72
73 const bool isPoint = !range.contains( ':' );
74 QRegExp regEx(isPoint ? "(|.*\\.|.*\\!)([A-Z0-9]+)"
75 : "(|.*\\.|.*\\!)([A-Z]+[0-9]+)\\:(|.*\\.|.*\\!)([A-Z0-9]+)");
76 if (regEx.indexIn(range) >= 0) {
77 range.clear();
78 QString sheetName = regEx.cap(1);
79 if (sheetName.endsWith(QLatin1Char('.')) || sheetName.endsWith(QLatin1Char('!')))
80 sheetName.chop(1);
81 if (!sheetName.isEmpty())
82 range = sheetName + '.';
83 range += regEx.cap(2);
84 if (!isPoint)
85 range += ':' + regEx.cap(4);
86 }
87
88 return range;
89 }
90
tintColor(const QColor & color,qreal tintfactor)91 QColor KoOdfChartWriter::tintColor(const QColor & color, qreal tintfactor)
92 {
93 QColor retColor;
94 const qreal nonTindedPart = 1.0 - tintfactor;
95 qreal luminance = 0.0;
96 qreal sat = 0.0;
97 qreal hue = 0.0;
98 color.getHslF(&hue, &sat, &luminance);
99 luminance = luminance * tintfactor + nonTindedPart;
100 retColor.setHslF(hue, sat, luminance);
101 // const int tintedColor = 255 * nonTindedPart;
102 // retColor.setRed(tintedColor + tintfactor * color.red());
103 // retColor.setGreen(tintedColor + tintfactor * color.green());
104 // retColor.setBlue(tintedColor + tintfactor * color.blue());
105
106 return retColor;
107 }
108
calculateColorFromGradientStop(const KoChart::Gradient::GradientStop & grad)109 QColor KoOdfChartWriter::calculateColorFromGradientStop(const KoChart::Gradient::GradientStop& grad)
110 {
111 QColor color = grad.knownColorValue;
112
113 const int tintedColor = 255 * grad.tintVal / 100.0;
114 const qreal nonTindedPart = 1.0 - grad.tintVal / 100.0;
115 color.setRed(tintedColor + nonTindedPart * color.red());
116 color.setGreen(tintedColor + nonTindedPart * color.green());
117 color.setBlue(tintedColor + nonTindedPart * color.blue());
118
119 return color;
120 }
121
generateGradientStyle(KoGenStyles & mainStyles,const KoChart::Gradient * grad)122 QString KoOdfChartWriter::generateGradientStyle(KoGenStyles& mainStyles,
123 const KoChart::Gradient* grad)
124 {
125 KoGenStyle gradStyle(KoGenStyle::GradientStyle);
126 gradStyle.addAttribute("draw:style", "linear");
127
128 QColor startColor = calculateColorFromGradientStop(grad->gradientStops.first());
129 QColor endColor = calculateColorFromGradientStop(grad->gradientStops.last());
130
131 gradStyle.addAttribute("draw:start-color", startColor.name());
132 gradStyle.addAttribute("draw:end-color", endColor.name());
133 gradStyle.addAttribute("draw:angle", QString::number(grad->angle));
134
135 return mainStyles.insert(gradStyle, "ms_chart_gradient");
136 }
137
labelFontColor() const138 QColor KoOdfChartWriter::labelFontColor() const
139 {
140 return QColor();
141 }
142
genChartAreaStyle(KoGenStyle & style,KoGenStyles & styles,KoGenStyles & mainStyles)143 QString KoOdfChartWriter::genChartAreaStyle(KoGenStyle& style, KoGenStyles& styles,
144 KoGenStyles& mainStyles)
145 {
146 if (chart()->m_fillGradient) {
147 style.addProperty("draw:fill", "gradient", KoGenStyle::GraphicType);
148 style.addProperty("draw:fill-gradient-name",
149 generateGradientStyle(mainStyles, chart()->m_fillGradient),
150 KoGenStyle::GraphicType);
151 } else {
152 style.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
153
154 QColor color;
155 if (chart()->m_areaFormat
156 && chart()->m_areaFormat->m_fill
157 && chart()->m_areaFormat->m_foreground.isValid())
158 {
159 color = chart()->m_areaFormat->m_foreground;
160 }
161 else
162 color = QColor("#FFFFFF");
163 style.addProperty("draw:fill-color", color.name(), KoGenStyle::GraphicType);
164
165 if (color.alpha() < 255)
166 style.addProperty("draw:opacity",
167 QString("%1%").arg(chart()->m_areaFormat->m_foreground.alphaF()
168 * 100.0),
169 KoGenStyle::GraphicType);
170 }
171
172 return styles.insert(style, "ch");
173 }
174
175
genChartAreaStyle(KoGenStyles & styles,KoGenStyles & mainStyles)176 QString KoOdfChartWriter::genChartAreaStyle(KoGenStyles& styles, KoGenStyles& mainStyles)
177 {
178 KoGenStyle style(KoGenStyle::GraphicAutoStyle, "chart");
179
180 return genChartAreaStyle(style, styles, mainStyles);
181 }
182
183
genPlotAreaStyle(KoGenStyle & style,KoGenStyles & styles,KoGenStyles & mainStyles)184 QString KoOdfChartWriter::genPlotAreaStyle(KoGenStyle& style, KoGenStyles& styles,
185 KoGenStyles& mainStyles)
186 {
187 KoChart::AreaFormat *areaFormat = ((chart()->m_plotArea
188 && chart()->m_plotArea->m_areaFormat
189 && chart()->m_plotArea->m_areaFormat->m_fill)
190 ? chart()->m_plotArea->m_areaFormat
191 : chart()->m_areaFormat);
192 if (chart()->m_plotAreaFillGradient) {
193 style.addProperty("draw:fill", "gradient", KoGenStyle::GraphicType);
194 style.addProperty("draw:fill-gradient-name",
195 generateGradientStyle(mainStyles, chart()->m_plotAreaFillGradient),
196 KoGenStyle::GraphicType);
197 } else {
198 style.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
199
200 QColor color;
201 if (areaFormat && areaFormat->m_foreground.isValid())
202 color = areaFormat->m_foreground;
203 else
204 color = QColor(paletteIsSet ? "#C0C0C0" : "#FFFFFF");
205 style.addProperty("draw:fill-color", color.name(), KoGenStyle::GraphicType);
206
207 if (color.alpha() < 255)
208 style.addProperty("draw:opacity",
209 QString("%1%").arg(areaFormat->m_foreground.alphaF() * 100.0),
210 KoGenStyle::GraphicType);
211 }
212
213 return styles.insert(style, "ch");
214 }
215
216
addShapePropertyStyle(KoChart::Series * series,KoGenStyle & style,KoGenStyles &)217 void KoOdfChartWriter::addShapePropertyStyle(/*const*/ KoChart::Series* series, KoGenStyle& style,
218 KoGenStyles& /*mainStyles*/)
219 {
220 Q_ASSERT(series);
221 bool marker = false;
222 KoChart::ScatterImpl* impl = dynamic_cast< KoChart::ScatterImpl* >(m_chart->m_impl);
223
224 if (impl)
225 marker = (impl->style == KoChart::ScatterImpl::Marker
226 || impl->style == KoChart::ScatterImpl::LineMarker);
227
228 if (series->spPr->lineFill.valid) {
229 if (series->spPr->lineFill.type == KoChart::Fill::Solid) {
230 style.addProperty("draw:stroke", "solid", KoGenStyle::GraphicType);
231 style.addProperty("svg:stroke-color", series->spPr->lineFill.solidColor.name(),
232 KoGenStyle::GraphicType);
233 }
234 else if (series->spPr->lineFill.type == KoChart::Fill::None) {
235 style.addProperty("draw:stroke", "none", KoGenStyle::GraphicType);
236 }
237 }
238 else if ( (paletteIsSet && m_chart->m_impl->name() != "scatter")
239 || m_chart->m_showLines)
240 {
241 const int curSerNum = m_chart->m_series.indexOf(series);
242 style.addProperty("draw:stroke", "solid", KoGenStyle::GraphicType);
243 style.addProperty("svg:stroke-color", m_palette.at(24 + curSerNum).name(),
244 KoGenStyle::GraphicType);
245 }
246 else if (paletteIsSet && m_chart->m_impl->name() == "scatter")
247 style.addProperty("draw:stroke", "none", KoGenStyle::GraphicType);
248 if (series->spPr->areaFill.valid) {
249 if (series->spPr->areaFill.type == KoChart::Fill::Solid) {
250 style.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
251 style.addProperty("draw:fill-color", series->spPr->areaFill.solidColor.name(),
252 KoGenStyle::GraphicType);
253 }
254 else if (series->spPr->areaFill.type == KoChart::Fill::None)
255 style.addProperty("draw:fill", "none", KoGenStyle::GraphicType);
256 }
257 else if (paletteIsSet
258 && !(m_chart->m_markerType != KoChart::NoMarker || marker)
259 && series->m_markerType == KoChart::NoMarker)
260 {
261 const int curSerNum = m_chart->m_series.indexOf(series) % 8;
262 style.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
263 style.addProperty("draw:fill-color", m_palette.at(16 + curSerNum).name(),
264 KoGenStyle::GraphicType);
265 }
266 }
267
genPlotAreaStyle(KoGenStyles & styles,KoGenStyles & mainStyles)268 QString KoOdfChartWriter::genPlotAreaStyle(KoGenStyles& styles, KoGenStyles& mainStyles)
269 {
270 KoGenStyle style(KoGenStyle::ChartAutoStyle/*, "chart"*/);
271 return genPlotAreaStyle(style, styles, mainStyles);
272 }
273
replaceSheet(const QString & originalString,const QString & replacementSheet)274 QString KoOdfChartWriter::replaceSheet(const QString &originalString,
275 const QString &replacementSheet)
276 {
277 QStringList split = originalString.split(QLatin1Char('!'));
278 split[0] = replacementSheet;
279 return split.join(QString::fromLatin1("!"));
280 }
281
set2003ColorPalette(QList<QColor> palette)282 void KoOdfChartWriter::set2003ColorPalette(QList < QColor > palette)
283 {
284 m_palette = palette;
285 paletteIsSet = true;
286 }
287
markerType(KoChart::MarkerType type,int currentSeriesNumber)288 QString KoOdfChartWriter::markerType(KoChart::MarkerType type, int currentSeriesNumber)
289 {
290 QString markerName;
291 switch(type) {
292 case NoMarker:
293 break;
294 case AutoMarker: { // auto marker type
295 const int resNum = currentSeriesNumber % 3;
296 if (resNum == 0)
297 markerName = "square";
298 else if (resNum == 1)
299 markerName = "diamond";
300 else if (resNum == 2)
301 markerName = "circle";
302 } break;
303 case SquareMarker:
304 markerName = "square";
305 break;
306 case DiamondMarker:
307 markerName = "diamond";
308 break;
309 case StarMarker:
310 markerName = "star";
311 break;
312 case TriangleMarker:
313 markerName = "arrow-up";
314 break;
315 case DotMarker:
316 markerName = "dot";
317 break;
318 case PlusMarker:
319 markerName = "plus";
320 break;
321 case SymbolXMarker:
322 markerName = "x";
323 break;
324 case CircleMarker:
325 markerName = "circle";
326 break;
327 case DashMarker:
328 markerName = "horizontal-bar";
329 break;
330 }
331
332 return markerName;
333 }
334
335
336 // ----------------------------------------------------------------
337 // The actual saving code
338
339
saveIndex(KoXmlWriter * xmlWriter)340 bool KoOdfChartWriter::saveIndex(KoXmlWriter* xmlWriter)
341 {
342 if (!chart() || m_href.isEmpty())
343 return false;
344
345 // This because for presesentations the frame is done in read_graphicFrame
346 if (!m_drawLayer) {
347 xmlWriter->startElement("draw:frame");
348 // used in opendocumentpresentation for layers
349 //if (m_drawLayer)
350 // xmlWriter->addAttribute("draw:layer", "layout");
351
352 // used in opendocumentspreadsheet to reference cells
353 if (!m_endCellAddress.isEmpty()) {
354 xmlWriter->addAttribute("table:end-cell-address", m_endCellAddress);
355 xmlWriter->addAttributePt("table:end-x", m_end_x);
356 xmlWriter->addAttributePt("table:end-y", m_end_y);
357 }
358
359 xmlWriter->addAttributePt("svg:x", m_x);
360 xmlWriter->addAttributePt("svg:y", m_y);
361 if (m_width > 0)
362 xmlWriter->addAttributePt("svg:width", m_width);
363 if (m_height > 0)
364 xmlWriter->addAttributePt("svg:height", m_height);
365 }
366 //xmlWriter->addAttribute("draw:z-index", "0");
367 xmlWriter->startElement("draw:object");
368 //TODO don't show on e.g. presenter
369 if (!m_notifyOnUpdateOfRanges.isEmpty())
370 xmlWriter->addAttribute("draw:notify-on-update-of-ranges", m_notifyOnUpdateOfRanges);
371
372 xmlWriter->addAttribute("xlink:href", "./" + m_href);
373 xmlWriter->addAttribute("xlink:type", "simple");
374 xmlWriter->addAttribute("xlink:show", "embed");
375 xmlWriter->addAttribute("xlink:actuate", "onLoad");
376
377 xmlWriter->endElement(); // draw:object
378 if (!m_drawLayer) {
379 xmlWriter->endElement(); // draw:frame
380 }
381 return true;
382 }
383
saveContent(KoStore * store,KoXmlWriter * manifestWriter)384 bool KoOdfChartWriter::saveContent(KoStore* store, KoXmlWriter* manifestWriter)
385 {
386 if (!chart() || !chart()->m_impl || m_href.isEmpty())
387 return false;
388
389 KoGenStyles styles;
390 KoGenStyles mainStyles;
391
392 store->pushDirectory();
393 store->enterDirectory(m_href);
394
395 KoOdfWriteStore s(store);
396 KoXmlWriter* bodyWriter = s.bodyWriter();
397 KoXmlWriter* contentWriter = s.contentWriter();
398 Q_ASSERT(bodyWriter && contentWriter);
399
400 bodyWriter->startElement("office:body");
401 bodyWriter->startElement("office:chart");
402
403 //<chart:chart chart:class="chart:circle"
404 // svg:width="8cm" svg:height="7cm"
405 // chart:style-name="ch1">
406 bodyWriter->startElement("chart:chart");
407
408 if (!chart()->m_impl->name().isEmpty()) {
409 bodyWriter->addAttribute("chart:class", "chart:" + chart()->m_impl->name());
410 }
411
412 if (m_width > 0) {
413 bodyWriter->addAttributePt("svg:width", m_width);
414 }
415 if (m_height > 0) {
416 bodyWriter->addAttributePt("svg:height", m_height);
417 }
418
419 bodyWriter->addAttribute("chart:style-name", genChartAreaStyle(styles, mainStyles));
420
421 // <chart:title svg:x="5.618cm" svg:y="0.14cm" chart:style-name="ch2">
422 // <text:p>PIE CHART</text:p>
423 // </chart:title>
424 if (!chart()->m_title.isEmpty()) {
425 bodyWriter->startElement("chart:title");
426
427 /* TODO we can't determine this because by default we need to center the title,
428 in order to center it we need to know the textbox size, and to do that we need
429 the used font metrics.
430
431 Also, for now, the default implementation of KChart centers
432 the title, so we get close to the expected behavior. We ignore any offset though.
433
434 Nonetheless, the formula should be something like this:
435 const int widht = m_width/2 - textWidth/2 + sprcToPt(t->m_x1, vertical);
436 const int height = m_height/2 - textHeight/2 + sprcToPt(t->m_y1, horizontal);
437 bodyWriter->addAttributePt("svg:x", width);
438 bodyWriter->addAttributePt("svg:y", height);
439 */
440
441 // NOTE: Don't load width or height, the record MUST be ignored and
442 // determined by the application
443 // see [MS-XLS] p. 362
444
445 bodyWriter->startElement("text:p");
446 bodyWriter->addTextNode(chart()->m_title);
447 bodyWriter->endElement(); // text:p
448 bodyWriter->endElement(); // chart:title
449 }
450
451 // Legend
452 if (chart()->m_legend) {
453 bodyWriter->startElement("chart:legend");
454 bodyWriter->addAttribute("chart:legend-position", "end");
455
456 KoGenStyle legendstyle(KoGenStyle::ChartAutoStyle, "chart");
457
458 QColor labelColor = labelFontColor();
459 if (labelColor.isValid())
460 legendstyle.addProperty("fo:font-color", labelColor.name(), KoGenStyle::TextType);
461
462 bodyWriter->addAttribute("chart:style-name", styles.insert(legendstyle, "lg"));
463
464 bodyWriter->endElement(); // chart:legend
465 }
466
467 // <chart:plot-area chart:style-name="ch3"
468 // table:cell-range-address="Sheet1.C2:Sheet1.E2"
469 // svg:x="0.16cm" svg:y="0.14cm">
470 bodyWriter->startElement("chart:plot-area");
471
472 if (chart()->m_is3d) {
473 //bodyWriter->addAttribute("dr3d:transform", "matrix (0.893670830886674 0.102940425033731 -0.436755898547686 -0.437131441492021 0.419523087196176 -0.795560483036015 0.101333848646097 0.901888933407692 0.419914042293545 0cm 0cm 0cm)");
474 //bodyWriter->addAttribute("dr3d:vrp", "(12684.722548717 7388.35827488833 17691.2795565958)");
475 //bodyWriter->addAttribute("dr3d:vpn", "(0.416199821709347 0.173649045905254 0.892537795986984)");
476 //bodyWriter->addAttribute("dr3d:vup", "(-0.0733876362771618 0.984807599917971 -0.157379306090273)");
477 //bodyWriter->addAttribute("dr3d:projection", "parallel");
478 //bodyWriter->addAttribute("dr3d:distance", "4.2cm");
479 //bodyWriter->addAttribute("dr3d:focal-length", "8cm");
480 //bodyWriter->addAttribute("dr3d:shadow-slant", "0");
481 //bodyWriter->addAttribute("dr3d:shade-mode", "flat");
482 //bodyWriter->addAttribute("dr3d:ambient-color", "#b3b3b3");
483 //bodyWriter->addAttribute("dr3d:lighting-mode", "true");
484 }
485
486 KoGenStyle chartstyle(KoGenStyle::ChartAutoStyle, "chart");
487 //chartstyle.addProperty("chart:connect-bars", "false");
488 //chartstyle.addProperty("chart:include-hidden-cells", "false");
489 chartstyle.addProperty("chart:auto-position", "true");
490 chartstyle.addProperty("chart:auto-size", "true");
491 chartstyle.addProperty("chart:angle-offset", chart()->m_angleOffset);
492
493 //chartstyle.addProperty("chart:series-source", "rows");
494 //chartstyle.addProperty("chart:sort-by-x-values", "false");
495 //chartstyle.addProperty("chart:right-angled-axes", "true");
496 if (chart()->m_is3d) {
497 chartstyle.addProperty("chart:three-dimensional", "true");
498 }
499 //chartstyle.addProperty("chart:angle-offset", "90");
500 //chartstyle.addProperty("chart:series-source", "rows");
501 //chartstyle.addProperty("chart:right-angled-axes", "false");
502 if (chart()->m_transpose) {
503 chartstyle.addProperty("chart:vertical", "true");
504 }
505 if (chart()->m_stacked) {
506 chartstyle.addProperty("chart:stacked", "true");
507 }
508 if (chart()->m_f100) {
509 chartstyle.addProperty("chart:percentage", "true");
510 }
511 bodyWriter->addAttribute("chart:style-name", genPlotAreaStyle(chartstyle, styles, mainStyles));
512
513 QString verticalCellRangeAddress = chart()->m_verticalCellRangeAddress;
514 // FIXME microsoft treats the regions from this area in a different order, so don't use it or x and y values will be switched
515 // if (!chart()->m_cellRangeAddress.isEmpty()) {
516 // if (sheetReplacement)
517 // bodyWriter->addAttribute("table:cell-range-address", replaceSheet(normalizeCellRange(m_cellRangeAddress), QString::fromLatin1("local"))); //"Sheet1.C2:Sheet1.E5");
518 // else
519 // bodyWriter->addAttribute("table:cell-range-address", normalizeCellRange(m_cellRangeAddress)); //"Sheet1.C2:Sheet1.E5");
520 // }
521
522 /*FIXME
523 if (verticalCellRangeAddress.isEmpty()) {
524 // only add the chart:data-source-has-labels if no chart:categories with a table:cell-range-address was defined within an axis.
525 bodyWriter->addAttribute("chart:data-source-has-labels", "both");
526 }
527 */
528
529 //bodyWriter->addAttribute("svg:x", "0.16cm"); //FIXME
530 //bodyWriter->addAttribute("svg:y", "0.14cm"); //FIXME
531 //bodyWriter->addAttribute("svg:width", "6.712cm"); //FIXME
532 //bodyWriter->addAttribute("svg:height", "6.58cm"); //FIXME
533
534 const bool definesCategories = chart()->m_impl->name() != "scatter"; // scatter charts are using domains
535 int countXAxis = 0;
536 int countYAxis = 0;
537 foreach (KoChart::Axis* axis, chart()->m_axes) {
538 //TODO handle series-axis
539 if (axis->m_type == KoChart::Axis::SeriesAxis) continue;
540
541 bodyWriter->startElement("chart:axis");
542
543 KoGenStyle axisstyle(KoGenStyle::ChartAutoStyle, "chart");
544
545 if (axis->m_reversed)
546 axisstyle.addProperty("chart:reverse-direction", "true", KoGenStyle::ChartType);
547
548 //FIXME this hits an infinite-looping bug in kdchart it seems... maybe fixed with a newer version
549 // if (axis->m_logarithmic)
550 // axisstyle.addProperty("chart:logarithmic", "true", KoGenStyle::ChartType);
551
552 if (!axis->m_autoMinimum)
553 axisstyle.addProperty("chart:minimum", QString::number(axis->m_minimum, 'f'),
554 KoGenStyle::ChartType);
555 if (!axis->m_autoMaximum)
556 axisstyle.addProperty("chart:maximum", QString::number(axis->m_maximum, 'f'),
557 KoGenStyle::ChartType);
558
559 axisstyle.addProperty("fo:font-size", QString("%0pt").arg(chart()->m_textSize),
560 KoGenStyle::TextType);
561
562 QColor labelColor = labelFontColor();
563 if (labelColor.isValid())
564 axisstyle.addProperty("fo:font-color", labelColor.name(), KoGenStyle::TextType);
565
566 if (!axis->m_numberFormat.isEmpty()) {
567 const KoGenStyle style = NumberFormatParser::parse(axis->m_numberFormat, &styles);
568 axisstyle.addAttribute("style:data-style-name", styles.insert(style, "ds"));
569 }
570
571 bodyWriter->addAttribute("chart:style-name", styles.insert(axisstyle, "ch"));
572
573 switch(axis->m_type) {
574 case KoChart::Axis::VerticalValueAxis:
575 bodyWriter->addAttribute("chart:dimension", "y");
576 bodyWriter->addAttribute("chart:name", QString("y%1").arg(++countYAxis));
577 break;
578 case KoChart::Axis::HorizontalValueAxis:
579 bodyWriter->addAttribute("chart:dimension", "x");
580 bodyWriter->addAttribute("chart:name", QString("x%1").arg(++countXAxis));
581 if (countXAxis == 1 && definesCategories && !verticalCellRangeAddress.isEmpty()) {
582 bodyWriter->startElement("chart:categories");
583 if (sheetReplacement)
584 verticalCellRangeAddress
585 = normalizeCellRange(replaceSheet(verticalCellRangeAddress,
586 QString::fromLatin1("local")));
587 else
588 verticalCellRangeAddress = normalizeCellRange(verticalCellRangeAddress);
589 bodyWriter->addAttribute("table:cell-range-address", verticalCellRangeAddress); //"Sheet1.C2:Sheet1.E2");
590 bodyWriter->endElement();
591 }
592 break;
593 default: break;
594 }
595
596 if (axis->m_majorGridlines.m_format.m_style != KoChart::LineFormat::None) {
597 bodyWriter->startElement("chart:grid");
598 bodyWriter->addAttribute("chart:class", "major");
599 bodyWriter->endElement(); // chart:grid
600 }
601
602 if (axis->m_minorGridlines.m_format.m_style != KoChart::LineFormat::None) {
603 bodyWriter->startElement("chart:grid");
604 bodyWriter->addAttribute("chart:class", "minor");
605 bodyWriter->endElement(); // chart:grid
606 }
607 bodyWriter->endElement(); // chart:axis
608 }
609
610 // Add at least one x-axis.
611 if (countXAxis == 0) {
612 bodyWriter->startElement("chart:axis");
613 bodyWriter->addAttribute("chart:dimension", "x");
614 bodyWriter->addAttribute("chart:name", "primary-x");
615
616 if (definesCategories && !verticalCellRangeAddress.isEmpty()) {
617 bodyWriter->startElement("chart:categories");
618 if (sheetReplacement)
619 verticalCellRangeAddress
620 = normalizeCellRange(replaceSheet(verticalCellRangeAddress,
621 QString::fromLatin1("local")));
622 else
623 verticalCellRangeAddress = normalizeCellRange(verticalCellRangeAddress);
624
625 bodyWriter->addAttribute("table:cell-range-address", verticalCellRangeAddress);
626 bodyWriter->endElement();
627 }
628
629 bodyWriter->endElement(); // chart:axis
630 }
631
632 // Add at least one y-axis.
633 if (countYAxis == 0) {
634 bodyWriter->startElement("chart:axis");
635 bodyWriter->addAttribute("chart:dimension", "y");
636 bodyWriter->addAttribute("chart:name", "primary-y");
637 bodyWriter->endElement(); // chart:axis
638 }
639
640 //<chart:axis chart:dimension="x" chart:name="primary-x" chart:style-name="ch4"/>
641 //<chart:axis chart:dimension="y" chart:name="primary-y" chart:style-name="ch5"><chart:grid chart:style-name="ch6" chart:class="major"/></chart:axis>
642
643 // NOTE: The XLS format specifies that if an explodeFactor that is > 100
644 // is found, we should find the biggest and make it 100, then scale
645 // all the other factors accordingly.
646 // see 2.4.195 PieFormat
647 int maxExplode = 100;
648 foreach (KoChart::Series* series, chart()->m_series) {
649 foreach (KoChart::Format* f, series->m_datasetFormat) {
650 if (KoChart::PieFormat* pieformat = dynamic_cast<KoChart::PieFormat*>(f)) {
651 if (pieformat->m_pcExplode > 0) {
652 maxExplode = qMax(maxExplode, pieformat->m_pcExplode);
653 }
654 }
655 }
656 }
657
658 // Area diagrams are special in that Excel displays the areas in another
659 // order than OpenOffice.org and Calligra Sheets. To make sure the same areas are
660 // visible we do the same as OpenOffice.org does and reverse the order.
661 if (chart()->m_impl->name() == "area") {
662 for (int i = chart()->m_series.count() - 1; i >= 0; --i) {
663 chart()->m_series.append(chart()->m_series.takeAt(i));
664 }
665 }
666
667 // Save the series.
668 if (!saveSeries(styles, mainStyles, bodyWriter, maxExplode))
669 return false;
670
671 bodyWriter->startElement("chart:wall");
672 bodyWriter->endElement(); // chart:wall
673
674 bodyWriter->startElement("chart:floor");
675 bodyWriter->endElement(); // chart:floor
676
677 bodyWriter->endElement(); // chart:plot-area
678
679 writeInternalTable(bodyWriter);
680
681 bodyWriter->endElement(); // chart:chart
682 bodyWriter->endElement(); // office:chart
683 bodyWriter->endElement(); // office:body
684
685 #ifdef CONTENTXML_DEBUG
686 debugOdf2 << bodyWriter->toString();
687 #endif
688
689 styles.saveOdfStyles(KoGenStyles::DocumentAutomaticStyles, contentWriter);
690 s.closeContentWriter();
691
692 if (store->open("styles.xml")) {
693 KoStoreDevice dev(store);
694 KoXmlWriter* stylesWriter = new KoXmlWriter(&dev);
695
696 stylesWriter->startDocument("office:document-styles");
697 stylesWriter->startElement("office:document-styles");
698 stylesWriter->addAttribute("xmlns:office", "urn:oasis:names:tc:opendocument:xmlns:office:1.0");
699 stylesWriter->addAttribute("xmlns:style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0");
700 stylesWriter->addAttribute("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0");
701 stylesWriter->addAttribute("xmlns:table", "urn:oasis:names:tc:opendocument:xmlns:table:1.0");
702 stylesWriter->addAttribute("xmlns:draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0");
703 stylesWriter->addAttribute("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0");
704 stylesWriter->addAttribute("xmlns:svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0");
705 stylesWriter->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
706 stylesWriter->addAttribute("xmlns:chart", "urn:oasis:names:tc:opendocument:xmlns:chart:1.0");
707 stylesWriter->addAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
708 stylesWriter->addAttribute("xmlns:meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0");
709 stylesWriter->addAttribute("xmlns:number", "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0");
710 stylesWriter->addAttribute("xmlns:dr3d", "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0");
711 stylesWriter->addAttribute("xmlns:math", "http://www.w3.org/1998/Math/MathML");
712 stylesWriter->addAttribute("xmlns:of", "urn:oasis:names:tc:opendocument:xmlns:of:1.2");
713 stylesWriter->addAttribute("office:version", "1.2");
714 mainStyles.saveOdfStyles(KoGenStyles::MasterStyles, stylesWriter);
715 mainStyles.saveOdfStyles(KoGenStyles::DocumentStyles, stylesWriter); // office:style
716 mainStyles.saveOdfStyles(KoGenStyles::DocumentAutomaticStyles, stylesWriter); // office:automatic-styles
717 stylesWriter->endElement(); // office:document-styles
718 stylesWriter->endDocument();
719
720 delete stylesWriter;
721 store->close();
722 }
723
724 manifestWriter->addManifestEntry(m_href + QLatin1Char('/'), "application/vnd.oasis.opendocument.chart");
725 manifestWriter->addManifestEntry(QString("%1/styles.xml").arg(m_href), "text/xml");
726 manifestWriter->addManifestEntry(QString("%1/content.xml").arg(m_href), "text/xml");
727
728 store->popDirectory();
729 return true;
730 }
731
732 // FIXME: We should probably create a KoOdfChartWriterContext out of these
733 // parameters when we add more similar saving functions later.
saveSeries(KoGenStyles & styles,KoGenStyles & mainStyles,KoXmlWriter * bodyWriter,int maxExplode)734 bool KoOdfChartWriter::saveSeries(KoGenStyles &styles, KoGenStyles &mainStyles,
735 KoXmlWriter* bodyWriter, int maxExplode)
736 {
737 int curSerNum = 0;
738 bool lines = true;
739 bool marker = false;
740 Q_FOREACH (KoChart::Series* series, chart()->m_series) {
741 lines = true;
742 if (chart()->m_impl->name() == "scatter" && !paletteIsSet) {
743 KoChart::ScatterImpl* impl = static_cast< KoChart::ScatterImpl* >(chart()->m_impl);
744 lines = (impl->style == KoChart::ScatterImpl::Line
745 || impl->style == KoChart::ScatterImpl::LineMarker);
746 marker = (impl->style == KoChart::ScatterImpl::Marker
747 || impl->style == KoChart::ScatterImpl::LineMarker);
748 }
749 const bool noLineFill = ((series->spPr != 0)
750 && series->spPr->lineFill.type == KoChart::Fill::None);
751 lines = lines && !noLineFill;
752 lines = lines || m_chart->m_showLines;
753
754 // <chart:series chart:style-name="ch7"
755 // chart:values-cell-range-address="Sheet1.C2:Sheet1.E2"
756 // chart:class="chart:circle">
757 bodyWriter->startElement("chart:series");
758 KoGenStyle seriesstyle(KoGenStyle::GraphicAutoStyle, "chart");
759 if (series->spPr)
760 addShapePropertyStyle(series, seriesstyle, mainStyles);
761 else if (lines && paletteIsSet) {
762 lines = false;
763 seriesstyle.addProperty("draw:stroke", "solid", KoGenStyle::GraphicType);
764 seriesstyle.addProperty("svg:stroke-color", m_palette.at(24 + curSerNum).name(),
765 KoGenStyle::GraphicType);
766 }
767
768 if (paletteIsSet
769 && m_chart->m_impl->name() != "ring"
770 && m_chart->m_impl->name() != "circle")
771 {
772 if (series->m_markerType == KoChart::NoMarker
773 && m_chart->m_markerType == KoChart::NoMarker
774 && !marker)
775 {
776 seriesstyle.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
777 seriesstyle.addProperty("draw:fill-color", m_palette.at(16 + curSerNum).name(),
778 KoGenStyle::GraphicType);
779 }
780 }
781
782 if (series->m_markerType != KoChart::NoMarker) {
783 QString markerName = markerType(series->m_markerType, curSerNum);
784 if (!markerName.isEmpty()) {
785 seriesstyle.addProperty("chart:symbol-type", "named-symbol", KoGenStyle::ChartType);
786 seriesstyle.addProperty("chart:symbol-name", markerName, KoGenStyle::ChartType);
787 }
788 }
789 else if (m_chart->m_markerType != KoChart::NoMarker || marker) {
790 QString markerName = markerType(m_chart->m_markerType == KoChart::NoMarker
791 ? KoChart::AutoMarker
792 : m_chart->m_markerType, curSerNum);
793 if (!markerName.isEmpty()) {
794 seriesstyle.addProperty("chart:symbol-type", "named-symbol", KoGenStyle::ChartType);
795 seriesstyle.addProperty("chart:symbol-name", markerName, KoGenStyle::ChartType);
796 }
797 }
798
799 if (chart()->m_impl->name() != "circle" && chart()->m_impl->name() != "ring")
800 addDataThemeToStyle(seriesstyle, curSerNum, chart()->m_series.count(), lines);
801 //seriesstyle.addProperty("draw:stroke", "solid");
802 //seriesstyle.addProperty("draw:fill-color", "#ff0000");
803
804 foreach (KoChart::Format* f, series->m_datasetFormat) {
805 if (KoChart::PieFormat* pieformat = dynamic_cast<KoChart::PieFormat*>(f)) {
806 if (pieformat->m_pcExplode > 0) {
807 // Note that 100.0/maxExplode will yield 1.0 most of the
808 // time, that's why do that division first
809 const int pcExplode = (int)((float)pieformat->m_pcExplode * (100.0 / (float)maxExplode));
810 seriesstyle.addProperty("chart:pie-offset", pcExplode, KoGenStyle::ChartType);
811 }
812 }
813 }
814
815 if (series->m_showDataLabelValues && series->m_showDataLabelPercent) {
816 seriesstyle.addProperty("chart:data-label-number", "value-and-percentage",
817 KoGenStyle::ChartType);
818 } else if (series->m_showDataLabelValues) {
819 seriesstyle.addProperty("chart:data-label-number", "value", KoGenStyle::ChartType);
820 } else if (series->m_showDataLabelPercent) {
821 seriesstyle.addProperty("chart:data-label-number", "percentage", KoGenStyle::ChartType);
822 }
823
824 if (series->m_showDataLabelCategory) {
825 seriesstyle.addProperty("chart:data-label-text", "true", KoGenStyle::ChartType);
826 }
827 //seriesstyle.addProperty("chart:data-label-symbol", "true", KoGenStyle::ChartType);
828
829 if (!series->m_numberFormat.isEmpty()) {
830 const KoGenStyle style = NumberFormatParser::parse(series->m_numberFormat, &styles);
831 seriesstyle.addAttribute("style:data-style-name", styles.insert(style, "ds"));
832 }
833
834 bodyWriter->addAttribute("chart:style-name", styles.insert(seriesstyle, "ch"));
835
836 // ODF does not support custom labels so we depend on the
837 // SeriesLegendOrTrendlineName being defined and to point to a valid
838 // cell to be able to display custom labels.
839 if (series->m_datasetValue.contains(KoChart::Value::SeriesLegendOrTrendlineName)) {
840 KoChart::Value* v = series->m_datasetValue[KoChart::Value::SeriesLegendOrTrendlineName];
841 if (!v->m_formula.isEmpty()) {
842 bodyWriter->addAttribute("chart:label-cell-address",
843 (v->m_type == KoChart::Value::CellRange
844 ? normalizeCellRange(v->m_formula)
845 : v->m_formula));
846 }
847 }
848
849 if (!series->m_labelCell.isEmpty()) {
850 QString labelAddress = series->m_labelCell;
851 if (sheetReplacement)
852 labelAddress = normalizeCellRange(replaceSheet(labelAddress,
853 QString::fromLatin1("local")));
854 else
855 labelAddress = normalizeCellRange(labelAddress);
856 bodyWriter->addAttribute("chart:label-cell-address", labelAddress);
857 }
858
859 QString valuesCellRangeAddress;
860 if (sheetReplacement)
861 valuesCellRangeAddress
862 = normalizeCellRange(replaceSheet(series->m_valuesCellRangeAddress,
863 QString::fromLatin1("local")));
864 else
865 valuesCellRangeAddress = normalizeCellRange(series->m_valuesCellRangeAddress);
866
867 if (!valuesCellRangeAddress.isEmpty()) {
868 // "Sheet1.C2:Sheet1.E2";
869 bodyWriter->addAttribute("chart:values-cell-range-address", valuesCellRangeAddress);
870 }
871 else if (!series->m_domainValuesCellRangeAddress.isEmpty()) {
872 // "Sheet1.C2:Sheet1.E2";
873 bodyWriter->addAttribute("chart:values-cell-range-address",
874 series->m_domainValuesCellRangeAddress.last());
875 }
876
877 bodyWriter->addAttribute("chart:class", "chart:" + chart()->m_impl->name());
878
879 // if (chart()->m_impl->name() == "scatter") {
880 // bodyWriter->startElement("chart:domain");
881 // bodyWriter->addAttribute("table:cell-range-address", verticalCellRangeAddress); //"Sheet1.C2:Sheet1.E5");
882 // bodyWriter->endElement();
883 // } else if (chart()->m_impl->name() == "bubble") {
884
885 QString domainRange;
886 Q_FOREACH (const QString& curRange, series->m_domainValuesCellRangeAddress) {
887 bodyWriter->startElement("chart:domain");
888 if (sheetReplacement)
889 domainRange = normalizeCellRange(replaceSheet(curRange, QString::fromLatin1("local")));
890 else
891 domainRange = normalizeCellRange(curRange);
892 if (!domainRange.isEmpty())
893 bodyWriter->addAttribute("table:cell-range-address", domainRange);
894 bodyWriter->endElement();
895 }
896 // if (series->m_domainValuesCellRangeAddress.count() == 1){
897 // bodyWriter->startElement("chart:domain");
898 // bodyWriter->addAttribute("table:cell-range-address", series->m_domainValuesCellRangeAddress.last()); //"Sheet1.C2:Sheet1.E5");
899 // bodyWriter->endElement();
900 // }
901 // if (series->m_domainValuesCellRangeAddress.isEmpty()){
902 // bodyWriter->startElement("chart:domain");
903 // bodyWriter->addAttribute("table:cell-range-address", series->m_valuesCellRangeAddress); //"Sheet1.C2:Sheet1.E5");
904 // bodyWriter->endElement();
905 // bodyWriter->startElement("chart:domain");
906 // bodyWriter->addAttribute("table:cell-range-address", series->m_valuesCellRangeAddress); //"Sheet1.C2:Sheet1.E5");
907 // bodyWriter->endElement();
908 // }
909 // }
910
911 for (int j = 0; j < series->m_countYValues; ++j) {
912 bodyWriter->startElement("chart:data-point");
913 KoGenStyle gs(KoGenStyle::GraphicAutoStyle, "chart");
914
915 if (chart()->m_impl->name() == "circle" || chart()->m_impl->name() == "ring") {
916 QColor fillColor;
917 if (j < series->m_dataPoints.count()) {
918 KoChart::DataPoint *dataPoint = series->m_dataPoints[j];
919 if (dataPoint->m_areaFormat) {
920 fillColor = dataPoint->m_areaFormat->m_foreground;
921 }
922 }
923
924 if (fillColor.isValid()) {
925 gs.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
926 gs.addProperty("draw:fill-color", fillColor.name(), KoGenStyle::GraphicType);
927 }
928 else if (series->m_markerType == KoChart::NoMarker
929 && m_chart->m_markerType == KoChart::NoMarker
930 && !marker)
931 {
932 if (paletteIsSet) {
933 gs.addProperty("draw:fill", "solid", KoGenStyle::GraphicType);
934 gs.addProperty("draw:fill-color", m_palette.at(16 + j).name(),
935 KoGenStyle::GraphicType);
936 }
937 else {
938 addDataThemeToStyle(gs, j, series->m_countYValues, lines);
939 }
940 }
941 }/*
942 else
943 {
944 addSeriesThemeToStyle(gs, curSerNum, chart()->m_series.count());
945 }*/
946
947 //gs.addProperty("chart:solid-type", "cuboid", KoGenStyle::ChartType);
948 //gs.addProperty("draw:fill-color",j==0?"#004586":j==1?"#ff420e":"#ffd320",
949 // KoGenStyle::GraphicType);
950 bodyWriter->addAttribute("chart:style-name", styles.insert(gs, "ch"));
951
952 Q_FOREACH (KoChart::Text* t, series->m_texts) {
953 bodyWriter->startElement("chart:data-label");
954 bodyWriter->startElement("text:p");
955 bodyWriter->addTextNode(t->m_text);
956 bodyWriter->endElement();
957 bodyWriter->endElement();
958 }
959
960 bodyWriter->endElement();
961 }
962
963 ++curSerNum;
964 bodyWriter->endElement(); // chart:series
965 }
966
967 return true;
968 }
969
970
971 // ----------------------------------------------------------------
972 // Some helper functions
973
974
975 // Calculate fade factor as suggested in msoo xml reference page 4161
calculateFade(int index,int maxIndex)976 qreal KoOdfChartWriter::calculateFade(int index, int maxIndex)
977 {
978 return -70.0 + 140.0 * ((double) index / ((double) maxIndex + 1.0));
979 }
980
shadeColor(const QColor & col,qreal factor)981 QColor KoOdfChartWriter::shadeColor(const QColor& col, qreal factor)
982 {
983 QColor result = col;
984 qreal luminance = 0.0;
985 qreal hue = 0.0;
986 qreal sat = 0.0;
987 result.getHslF(&hue, &sat, &luminance);
988 luminance *= factor;
989 result.setHslF(hue, sat, luminance);
990 return result;
991 }
992
addDataThemeToStyle(KoGenStyle & style,int dataNumber,int maxNumData,bool strokes)993 void KoOdfChartWriter::addDataThemeToStyle(KoGenStyle& style, int dataNumber, int maxNumData,
994 bool strokes)
995 {
996 // FIXME: This is only relevant to themes, so remove this function after
997 // we are done with saveContent().
998 Q_UNUSED(style);
999 Q_UNUSED(dataNumber);
1000 Q_UNUSED(maxNumData);
1001 Q_UNUSED(strokes);
1002 }
1003
1004
sprcToPt(int sprc,Orientation orientation)1005 float KoOdfChartWriter::sprcToPt(int sprc, Orientation orientation )
1006 {
1007 if (orientation & vertical)
1008 return (float)sprc * ( (float)m_width / 4000.0);
1009
1010 return (float)sprc * ( (float)m_height / 4000.0);
1011 }
1012
writeInternalTable(KoXmlWriter * bodyWriter)1013 void KoOdfChartWriter::writeInternalTable(KoXmlWriter* bodyWriter)
1014 {
1015 Q_ASSERT( bodyWriter );
1016 bodyWriter->startElement("table:table");
1017 bodyWriter->addAttribute( "table:name", "local" );
1018
1019 bodyWriter->startElement( "table:table-header-columns" );
1020 bodyWriter->startElement( "table:table-column" );
1021 bodyWriter->endElement();
1022 bodyWriter->endElement();
1023
1024 bodyWriter->startElement( "table:table-columns" );
1025 bodyWriter->startElement( "table:table-column" );
1026 bodyWriter->endElement();
1027 bodyWriter->endElement();
1028
1029 bodyWriter->startElement( "table:table-rows" );
1030
1031 const int rowCount = chart()->m_internalTable.maxRow();
1032 for (int r = 1; r <= rowCount; ++r) {
1033 bodyWriter->startElement("table:table-row");
1034 const int columnCount = chart()->m_internalTable.maxCellsInRow(r);
1035 for (int c = 1; c <= columnCount; ++c) {
1036 bodyWriter->startElement("table:table-cell");
1037 if (Cell* cell = chart()->m_internalTable.cell(c, r, false)) {
1038 //debugOdf2 << "cell->m_value " << cell->m_value;
1039 if (!cell->m_value.isEmpty()) {
1040 if (!cell->m_valueType.isEmpty()) {
1041 bodyWriter->addAttribute("office:value-type", cell->m_valueType);
1042 if (cell->m_valueType == "string") {
1043 bodyWriter->addAttribute("office:string-value", cell->m_value);
1044 } else if (cell->m_valueType == "boolean") {
1045 bodyWriter->addAttribute("office:boolean-value", cell->m_value);
1046 } else if (cell->m_valueType == "date") {
1047 bodyWriter->addAttribute("office:date-value", cell->m_value);
1048 } else if (cell->m_valueType == "time") {
1049 bodyWriter->addAttribute("office:time-value", cell->m_value);
1050 } else { // float, percentage and currency including fraction and scientific
1051 bodyWriter->addAttribute("office:value", cell->m_value);
1052 }
1053 }
1054
1055 bodyWriter->startElement("text:p");
1056 bodyWriter->addTextNode( cell->m_value );
1057 bodyWriter->endElement(); // text:p
1058 }
1059 }
1060 bodyWriter->endElement(); // table:table-cell
1061 }
1062 bodyWriter->endElement(); // table:table-row
1063 }
1064 bodyWriter->endElement(); // table:table-rows
1065 bodyWriter->endElement(); // table:table
1066 }
1067
setSheetReplacement(bool val)1068 void KoOdfChartWriter::setSheetReplacement( bool val )
1069 {
1070 sheetReplacement = val;
1071 }
1072