Categories > TinyButStrong general >

Charts inside blocks

The forum is closed. Please use Stack Overflow for submitting new questions. Use tags: tinybutstrong , opentbs
By: walter
Date: 2015-03-05
Time: 00:19

Charts inside blocks


I really need to accomplish that. I am digging into the sources but until now I got no success.
To add graphs dynamically you need to add the rId reference to word/_rels/document.xml.rels, add the file to word/charts, add an Override to [Content_Types].xml and the <drawing> in word/document.xml.

My problem is the last step, needed to point to the graph, after the whole merging of the file. Or adapt the MergeBlock function for doing that for me.
Is there a way to get the final XML after all merging and search for a string and replace with my template? I think this could be enough for my issue, since I could merge ids for my graphs and then look for them, replacing with the appropriate XML.

Thanks in advance.
By: Skrol29
Date: 2015-03-05
Time: 16:48

Re: Charts inside blocks


Great effort !

> Or adapt the MergeBlock function for doing that for me.

You can use a custom "ondata" function that could build the rId value, and then use a TBS field to merge this is at the appropriate XML attribute using parameter "att".

> Is there a way to get the final XML after all merging

$xml = $TBS->Source;
$TBS->Source = $new_xml;
By: walter
Date: 2015-03-05
Time: 20:20

Re: Charts inside blocks

I solved my problem. It is not a very dynamic solution, but can be very handy in some cases.
Using it goes like this: You add the charts at the moment you call the MergeBlock commands. Something like this
                'title' => $fNames[$key],
                'series' => array(
                        'title' => $fNames[$key],
                        'x' => $x,
                        'y' => $y

Besides that, I select the place the chart will be inserted merging a simple block, like this
                'show' => '1',
                'caption' => 'Lorem Ips',
                'graph' => 'chr1',

Note that the chr1 is exactly what is going to be placed on the final file. Then starts the awesome!
I extended the classes (TBS and OpenTBS) and overloaded some methods:
class CustomTbs extends clsTinyButStrong
    public $charts = array();
    public $lastChart = 0;

    public function AddChart($props){
        $this->charts['chr'.$this->lastChart] = $props;

class CustomOpenTbs extends clsOpenTBS

    function chartDocumentCall($id) {
        $n = substr($id, 3);
        return '<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0" wp14:anchorId="29E87212" wp14:editId="01E1C7C3"><wp:extent cx="5486400" cy="3200400"/><wp:effectExtent l="0" t="0" r="25400" b="25400"/><wp:docPr id="' . $n . '" name="Chart ' . $n . '"/><wp:cNvGraphicFramePr/><a:graphic xmlns:a=""><a:graphicData uri=""><c:chart xmlns:c="" xmlns:r="" r:id="' . $id . '"/></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>';

    function chartTemplate($chart){

        $s = '';
        $i = 0;
        foreach ($chart['series'] as $serie) {
            $s .= $this->chartSeries($i, $serie['title'], $serie['x'], $serie['y']);

        return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<c:chartSpace xmlns:c="" xmlns:a="" xmlns:r=""><c:date1904 val="0"/><c:lang val="en-US"/><c:roundedCorners val="0"/><mc:AlternateContent xmlns:mc=""><mc:Choice Requires="c14" xmlns:c14=""><c14:style val="118"/></mc:Choice><mc:Fallback><c:style val="18"/></mc:Fallback></mc:AlternateContent><c:chart><c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr/></a:pPr><a:r><a:rPr lang="en-US"/><a:t>'.$chart['title'].'</a:t></a:r></a:p></c:rich></c:tx><c:layout/><c:overlay val="0"/></c:title><c:autoTitleDeleted val="0"/><c:plotArea><c:layout/><c:scatterChart><c:scatterStyle val="lineMarker"/><c:varyColors val="0"/>'.$s.'<c:dLbls><c:showLegendKey val="0"/><c:showVal val="0"/><c:showCatName val="0"/><c:showSerName val="0"/><c:showPercent val="0"/><c:showBubbleSize val="0"/></c:dLbls><c:axId val="2123714840"/><c:axId val="2125151656"/></c:scatterChart><c:valAx><c:axId val="2123714840"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="b"/><c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:crossAx val="2125151656"/><c:crosses val="autoZero"/><c:crossBetween val="midCat"/></c:valAx><c:valAx><c:axId val="2125151656"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/><c:axPos val="l"/><c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:crossAx val="2123714840"/><c:crosses val="autoZero"/><c:crossBetween val="midCat"/></c:valAx></c:plotArea><c:legend><c:legendPos val="b"/><c:layout/><c:overlay val="0"/></c:legend><c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/><c:showDLblsOverMax val="0"/></c:chart><c:spPr><a:ln><a:noFill/></a:ln></c:spPr></c:chartSpace>';

    function InsertCharts() {
        $TBS =& $this->TBS;
        foreach ($TBS->charts as $rid=>$chart) {
            $this->OpenXML_Rels_AddNewChartRid('word/document.xml', 'charts/', $rid);

    function chartSeries($id, $title, $xValues, $yValues) {
        $nx = count($xValues);
        $ny = count($yValues);

        $x = '';
        $i = 0;
        foreach ($xValues as $v) {
            $x .= '<c:pt idx="'.$i.'"><c:v>'.$v.'</c:v></c:pt>';

        $y = '';
        $i = 0;
        foreach ($yValues as $v) {
            $y .= '<c:pt idx="'.$i.'"><c:v>'.$v.'</c:v></c:pt>';

        return '<c:ser><c:idx val="'.$id.'"/><c:order val="'.$id.'"/><c:tx><c:v>'.$title.'</c:v></c:tx><c:spPr><a:ln w="47625"><a:noFill/></a:ln></c:spPr><c:marker><c:symbol val="circle"/><c:size val="4"/><c:spPr><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:ln><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:ln></c:spPr></c:marker><c:trendline><c:trendlineType val="linear"/><c:dispRSqr val="0"/><c:dispEq val="0"/></c:trendline><c:xVal><c:numLit><c:formatCode>General</c:formatCode><c:ptCount val="'.$nx.'"/>'.$x.'</c:numLit></c:xVal><c:yVal><c:numLit><c:formatCode>General</c:formatCode><c:ptCount val="'.$ny.'"/>'.$y.'</c:numLit></c:yVal><c:smooth val="0"/></c:ser>';

    function BeforeShow(&$Render, $File='') {

        $TBS =& $this->TBS;


        if ($this->OpenXmlRid!==false) $this->OpenXML_Rels_CommitNewChartRids($TBS); //Moved for merging the chart call in document.xml

        if ($this->ArchFile==='') {
            return $this->RaiseError('Command Show() cannot be processed because no archive is opened.');

        if ($TBS->_Mode!=0) return; // If we are in subtemplate mode, the we use the TBS default process

        if ($this->TbsSystemCredits) {
            $this->Misc_EditCredits("OpenTBS " . $this->Version, true, true);
        $this->TbsStorePark(); // Save the current loaded subfile if any

        $TBS->Plugin(-4); // deactivate other plugins

        $Debug = (($Render & OPENTBS_DEBUG_XML)==OPENTBS_DEBUG_XML);
        if ($Debug) $this->DebugLst = array();


        switch ($this->ExtEquiv) {
            case 'ods':  $this->OpenDoc_SheetSlides_DeleteAndDisplay(true); break;
            case 'odp':  $this->OpenDoc_SheetSlides_DeleteAndDisplay(false); break;
            case 'xlsx': $this->MsExcel_SheetDeleteAndDisplay(); break;
            case 'pptx': $this->MsPowerpoint_SlideDelete(); break;
        $explicitRef = ($TBS->OtbsMsExcelExplicitRef && ($this->ExtEquiv==='xlsx'));
        // Merges all modified subfiles
        $idx_lst = array_keys($this->TbsStoreLst);
        foreach ($idx_lst as $idx) {
            $TBS->Source = $this->TbsStoreLst[$idx]['src'];
            $onshow = $this->TbsStoreLst[$idx]['onshow'];
            unset($this->TbsStoreLst[$idx]); // save memory space
            $TBS->OtbsCurrFile = $this->TbsGetFileName($idx); // usefull for TbsPicAdd()
            $this->TbsCurrIdx = $idx; // usefull for debug mode
            if ($TbsShow && $onshow) $TBS->Show(TBS_NOTHING);
            if ($this->ExtEquiv == 'docx') {
            if ($explicitRef && (!isset($this->MsExcel_KeepRelative[$idx])) ) {
            if ($Debug) $this->DebugLst[$this->TbsGetFileName($idx)] = $TBS->Source;
            $this->FileReplace($idx, $TBS->Source, TBSZIP_STRING, $TBS->OtbsAutoUncompress);
        $TBS->Plugin(-10); // reactivate other plugins
        $this->TbsCurrIdx = false;
        if ($this->OpenXmlCTypes!==false) $this->OpenXML_CTypesCommit($Debug);    // Commit special OpenXML features if any
        if ($this->OpenDocManif!==false)  $this->OpenDoc_ManifestCommit($Debug);  // Commit special OpenDocument features if any
        //if ($this->OpenXmlRid!==false) $this->OpenXML_Rels_CommitNewRids($Debug); // Must be done also after the loop because some Rid can be added with [onshow]
        //if ($this->OpenXmlRid!==false) $this->OpenXML_Rels_CommitNewChartRids($Debug); // Must be done also after the loop because some Rid can be added with [onshow]
        if ($TBS->OtbsGarbageCollector) {
            if ($this->ExtType=='openxml') $this->OpenMXL_GarbageCollector();

        if ( ($TBS->ErrCount>0) && (!$TBS->NoErr) && (!$Debug)) {
            $TBS->meth_Misc_Alert('Show() Method', 'The output is cancelled by the OpenTBS plugin because at least one error has occured.');

        if ($Debug) {
            // Do the debug even if other options are used
            $this->TbsDebug_Merge(true, false);
        } elseif (($Render & TBS_OUTPUT)==TBS_OUTPUT) { // notice that TBS_OUTPUT = OPENTBS_DOWNLOAD
            // download
            $ContentType = (isset($this->ExtInfo['ctype'])) ? $this->ExtInfo['ctype'] : '';
            $this->Flush($Render, $File, $ContentType); // $Render is used because it can contain options OPENTBS_DOWNLOAD and OPENTBS_NOHEADER.
            $Render = $Render - TBS_OUTPUT; //prevent TBS from an extra output.
        } elseif(($Render & OPENTBS_FILE)==OPENTBS_FILE) {
            // to file
            $this->Flush(TBSZIP_FILE, $File);
        } elseif(($Render & OPENTBS_STRING)==OPENTBS_STRING) {
            // to string
            $TBS->Source = $this->OutputSrc;
            $this->OutputSrc = '';

        if (($Render & TBS_EXIT)==TBS_EXIT) {

        return false; // cancel the default Show() process


    function OpenXML_Rels_AddNewChartRid($DocPath, $TargetDir, $rid) {

        $o = $this->OpenXML_Rels_GetObj($DocPath, $TargetDir);

        $Target = $TargetDir.$rid.'.xml';

        if (isset($o->RidLst[$Target])) return $o->RidLst[$Target];

        // Add the Rid in the information
        //$NewRid = 'chr'.(1+count($o->RidNew));
        $NewRid = $rid;
        $o->RidLst[$Target] = $NewRid;
        $o->RidNew[$Target] = $NewRid;

        //$this->IdxToCheck[$o->ParentIdx] = $o->FicIdx;

        return $NewRid;

    function OpenXML_Rels_CommitNewChartRids (&$TBS, $Debug=false) {

        foreach ($this->OpenXmlRid as $doc => $o) {

            if (count($o->RidNew)>0) {

                // search position for insertion
                $p = strpos($o->FicTxt, '</Relationships>');
                if ($p===false) return $this->RaiseError("(OpenXML) closing tag </Relationships> not found in subfile ".$o->FicPath);

                // build the string to insert
                $x = '';
                $r = '';
                foreach ($o->RidNew as $target=>$rid) {
                    $x .= '<Relationship Id="'.$rid.'" Type="" Target="'.$target.'"/>';

                    //insert pointer to graph in document
                    $TBS->Source = str_replace('<w:r><w:t>' . $rid . '</w:t></w:r>', $this->chartDocumentCall($rid), $TBS->Source);

                    //inser file in charts/
                    $this->FileAdd('word/' . $target, $this->chartTemplate($TBS->charts[$rid]));

                    $r .= '<Override PartName="/word/' . $target . '" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>';

                //insert content types
                $file = '[Content_Types].xml';
                $idx = $this->FileGetIdx($file);
                $Txt = $this->FileRead($idx, true);
                $posBeg = 0;
                $loc = clsTbsXmlLoc::FindStartTag($Txt, 'Types', $posBeg);

                $Txt = substr_replace($Txt, $r, $loc->PosEnd - 7, 0);

                $this->FileReplace($idx, $Txt);

                // insert in references
                $o->FicTxt = substr_replace($o->FicTxt, $x, $p, 0);

                // save
                if ($o->FicType==1) {
                    $this->FileAdd($o->FicPath, $o->FicTxt);
                } else {
                    $this->FileReplace($o->FicIdx, $o->FicTxt);

                // debug mode
                if ($Debug) $this->DebugLst[$o->FicPath] = $o->FicTxt;
                $this->OpenXmlRid[$doc]->RidNew = array(); // Erase the Rid done because there can be another commit


The magic happens on OpenXML_Rels_CommitNewChartRids, when the merged chart ids are replaced by the proper xml tags, the files are created in word/charts and de references updated. The ContentTypes gets updated there too.

Hope it helps!
By: Skrol29
Date: 2015-03-07
Time: 23:20

Re: Charts inside blocks

Hi Walter,

This is very clever.
This could be a nice feature for a future version.