{*********************************************}
{  TeeChart Storage functions                 }
{  Copyright (c) 1997-2020 by Steema Software }
{  All rights reserved                        }
{*********************************************}
unit TeeStore;
{$I TeeDefs.inc}

interface

uses
  {$IFNDEF FMX}
  {$IFNDEF LINUX}
  Windows,
  {$ENDIF}
  {$ENDIF}

  Classes, SysUtils,

  {$IFDEF D18}
  System.Generics.Collections,
  {$ENDIF}

  {$IFDEF FMX}
  System.UITypes, FMXTee.Procs, FMXTee.Engine, FMXTee.Chart
  {$ELSE}

  {$IFDEF D16}
  System.UITypes,
  {$ENDIF}

  TeeProcs, TeEngine, Chart
  {$ENDIF}
  ;

{ Read a Chart from a file (example: Chart1,'c:\demo.tee' ) }
Procedure LoadChartFromFile(const AChart:TCustomChart; Const AFileName:String);

{ Write a Chart to a file (example: Chart1,'c:\demo.tee' ) }
Procedure SaveChartToFile(const AChart:TCustomChart; Const AFileName:String;
                          IncludeData:Boolean=True;
                          TextFormat:Boolean=False);

Procedure SaveChartToXMLFile(const AChart:TCustomChart; Const AFileName:String;
                             IncludeData:Boolean=True; XMLHeader:Boolean=True);

{ The same using TStream components (good for BLOB fields, etc)  }
Procedure LoadChartFromStream(const AChart:TCustomChart; const AStream:TStream);

Procedure SaveChartToStream(const AChart:TCustomChart; const AStream:TStream;
                            IncludeData:Boolean=True;
                            TextFormat:Boolean=False);

Procedure SaveChartToXMLStream(const AChart:TCustomChart; const AStream:TStream;
                               IncludeData:Boolean=True; XMLHeader:Boolean=True);

{ (Advanced) Read charts and check for errors  }
{ return True if ok, False to stop loading }
type TProcTeeCheckError=function(const Message: string): Boolean of object;

Procedure LoadChartFromStreamCheck( const AChart:TCustomChart;
                                    const AStream:TStream;
                                    ACheckError:TProcTeeCheckError=nil;
                                    TryReadData:Boolean=False);

Procedure LoadChartFromFileCheck( const AChart:TCustomChart;
                                  const AName:String;
                                  ACheckError:TProcTeeCheckError);

// Convert a binary *.tee file to text format
Procedure ConvertTeeFileToText(Const InputFile,OutputFile:String);
Procedure ConvertTeeToText(const SInput,SOutput:TStream);

// Convert a text *.tee file to binary format
Procedure ConvertTeeFileToBinary(Const InputFile,OutputFile:String);
Procedure ConvertTeeToBinary(const SInput,SOutput:TStream);

type
  TSeriesData=class(TTeeExportData)
  private
    FChart         : TCustomChart;
    FIncludeColors : Boolean;
    FIncludeIndex  : Boolean;
    FIncludeHeader : Boolean;
    FIncludeLabels : Boolean;
    FSeries        : TChartSeries;
    FSeriesList    : TCustomSeriesList;
    FUseSeriesFormat : Boolean;
    FValueFormat   : String;

    Procedure Prepare; overload;
  protected
    IFormat : TeeFormatFlag;

    Procedure GuessSeriesFormat; overload; virtual;
    Procedure GuessSeriesFormat(ASeries:TChartSeries); overload; virtual;
    Function MaxSeriesCount:Integer;
    Function PointToString(Index:Integer):String; virtual;
    Procedure Prepare(const ASeries:TChartSeries); overload;
    function TotalColumns: Integer;
    function TotalRows:Integer;
    function TotalSeries:Integer;
  public
    // Use this array to decide which Series should be exported.
    // By default it is empty, that means all Series will be exported.

    Constructor Create(AChart:TCustomChart; ASeries:TChartSeries=nil); virtual;
    Destructor Destroy; override;

    function AsString:String; override;

    {$IFDEF D12}
    procedure SaveToStream(AStream:TStream); override;
    {$ENDIF}

    // Returns True if ASeries should be exported.
    function ValidSeries(const ASeries:TChartSeries):Boolean;

    function ValueToString(const ASeries:TChartSeries; const Value:Double):String;

    property Chart:TCustomChart read FChart write FChart;
    property Series:TChartSeries read FSeries write FSeries;
    property SeriesList:TCustomSeriesList read FSeriesList;
  published
    property IncludeColors:Boolean read FIncludeColors write FIncludeColors default False;
    property IncludeHeader:Boolean read FIncludeHeader write FIncludeHeader default False;
    property IncludeIndex:Boolean read FIncludeIndex write FIncludeIndex default False;
    property IncludeLabels:Boolean read FIncludeLabels write FIncludeLabels default True;

    // property IncludeSeriesTitle:Boolean read FIncludeSeriesTitle write FIncludeSeriesTitle default False;

    property UseSeriesFormat:Boolean read FUseSeriesFormat write FUseSeriesFormat default False;
    property ValueFormat:String read FValueFormat write FValueFormat;
  end;

  TSeriesDataClass=class of TSeriesData;

  TSeriesDataText=class(TSeriesData)
  private
    FTextDelimiter : Char;
    FTextQuotes    : String;
  protected
    procedure GuessSeriesFormat; override;
    Function PointToString(Index:Integer):String; override;
  public
    Constructor Create(AChart:TCustomChart;
                       ASeries:TChartSeries=nil); override;
    Function AsString:String; override;
  published
    property TextDelimiter:Char read FTextDelimiter write FTextDelimiter default TeeTabDelimiter;
    property TextQuotes:String read FTextQuotes write FTextQuotes;
  end;

  TSeriesDataXML=class(TSeriesData)
  private
    FEncoding : String;
    function IsEncodingStored:Boolean;
  public
    Constructor Create(AChart:TCustomChart; ASeries:TChartSeries=nil); override;
    Function AsString:String; override;
  published
    property Encoding:String read FEncoding write FEncoding stored IsEncodingStored;
    property IncludeHeader default True;
  end;

  TSeriesDataHTML=class(TSeriesData)
  protected
    Function PointToString(Index:Integer):String; override;
  public
    FirstSeriesLabels : Boolean;

    Function AsString:String; override;
  end;

  TSeriesDataXLS=class(TSeriesData)
  public
    Procedure SaveToStream(AStream:TStream); override;
  end;

  TJSONDateTimeFormat=(jdTimeStamp, jdJavaScript);

  TSeriesDataJSON=class(TSeriesData)
  private
    FDateFormat : TJSONDateTimeFormat;
  public
    Constructor Create(AChart:TCustomChart; ASeries:TChartSeries=nil); override;
    Function AsString:String; override;

    property DateTimeFormat:TJSONDateTimeFormat read FDateFormat write FDateFormat;
  end;

// Makes sure AFileName contains the ".tee" extension
Function TeeCheckExtension(Const AFileName:String):String;

implementation

Uses {$IFDEF FMX}
     FMX.Types,
     FMX.Platform,
     {$ELSE}
     Graphics, Clipbrd,
     {$ENDIF}

     Math,

     {$IFDEF FMX}
     FMXTee.Canvas, FMXTee.Constants
     {$ELSE}
     TeCanvas, TeeConst
     {$ENDIF}
     ;

Const MagicTeeFile  =$3060;

      {$IFDEF FMX}
      VersionTeeFile=$0230; { RAD XE2 and up }
      {$ELSE}

      {$IFDEF D7}
      VersionTeeFile=$0157; { Delphi 7 }
      {$ELSE}
      {$IFDEF D6}
      VersionTeeFile=$0150; { Delphi 6 }
      {$ELSE}
      VersionTeeFile=$0140; { Delphi / C++ Builder 5 }
      {$ENDIF}
      {$ENDIF}

      {$ENDIF}

type
  TTeeFileHeader=Packed Record
    Magic   : Word; { secure checking }
    Version : Word; { Chart file version }
  end;

  TSeriesAccess=class(TChartSeries);

// compatibility with previous versions saved *.tee files
Procedure ReadChartData(const AStream:TStream; const AChart:TCustomChart);
Var t : Integer;
begin { read each Series data }
  for t:=0 to AChart.SeriesCount-1 do
      TSeriesAccess(AChart[t]).ReadData(AStream);
end;

{ Special reader to skip Delphi 3 or 4 new properties when
  reading Charts in Delphi 1.0 or 2.0 }
type
  TChartReader=class(TReader)
  protected
    procedure FindMethodEvent(Reader: TReader; const MethodName: string;
                              var Address: Pointer;
                              var Error: Boolean);
    function Error(const Message: string): Boolean; override;
  public
    CheckError:TProcTeeCheckError;
  end;

function TChartReader.Error(const Message: string): Boolean;
begin
  if Assigned(CheckError) then result:=CheckError(Message)
                          else result:=True;
end;

procedure TChartReader.FindMethodEvent(Reader: TReader; const MethodName: string;
                                       var Address: Pointer;
                                       var Error: Boolean);
begin
  Error:=False;
end;

Procedure ReadHeader(const Stream:TStream; out Header: TTeeFileHeader);
begin
  Stream.Read(Header,SizeOf(Header));
end;

{$IFDEF IOS}
{$DEFINE HEADERBYTES}
{$ENDIF}

{$IFDEF ANDROID}
{$DEFINE HEADERBYTES}
{$ENDIF}

{$IFDEF LINUX}
{$DEFINE HEADERBYTES}
{$ENDIF}

function TeeFileIsValid(const Stream:TStream):Boolean;
var Pos    : Integer;
    Header : TTeeFileHeader;
begin
  Pos := Stream.Position;
  ReadHeader(Stream,Header);

  { check is a valid Tee file }
  result:=Header.Magic=MagicTeeFile;

  if not result then
     result:=  {$IFDEF D12}

               {$IFDEF HEADERBYTES}
               Byte(Header.Magic) in [Ord('o'),Ord('O')]
               {$ELSE}
               AnsiChar(Header.Magic) in ['o','O']
               {$ENDIF}

               {$ELSE}
               Char(Header.Magic) in ['o','O']
               {$ENDIF}
             ;

  Stream.Position := Pos;
end;

function TeeFileIsBinary(const Stream:TStream):Boolean;
var Pos : Integer;
    Header  : TTeeFileHeader;
begin
  Pos := Stream.Position;
  ReadHeader(Stream,Header);
  { check is a valid Tee file }
  result:=Header.Magic=MagicTeeFile;
  Stream.Position := Pos;
end;

Procedure TeeWriteHeader(const AStream:TStream);
var Header  : TTeeFileHeader;
begin
  { write file header, with "magic" number and version }
  Header.Magic:=MagicTeeFile;
  Header.Version:=VersionTeeFile;
  AStream.Write(Header,SizeOf(Header));
end;

Procedure ConvertTeeToBinary(const SInput,SOutput:TStream);
begin
  if TeeFileIsValid(SInput) then
  begin
    if not TeeFileIsBinary(SInput) then
    begin
      TeeWriteHeader(SOutput);
      ObjectTextToBinary(SInput,SOutput);
    end
    else
       SOutput.CopyFrom(SInput,SInput.Size);
  end
  else Raise ChartException.Create(TeeMsg_WrongTeeFileFormat);
end;

Procedure ConvertTeeToText(const SInput,SOutput:TStream);
var Header  : TTeeFileHeader;
begin
  if TeeFileIsValid(SInput) then
  begin
    if TeeFileIsBinary(SInput) then
    begin
      ReadHeader(SInput,Header);
      ObjectBinaryToText(SInput,SOutput);
    end
    else SOutput.CopyFrom(SInput,SInput.Size);
  end
  else Raise ChartException.Create(TeeMsg_WrongTeeFileFormat);
end;

{ Reads Series and Points from a Stream into a Chart }
Procedure LoadChartFromStreamCheck( const AChart:TCustomChart;
                                    const AStream:TStream;
                                    ACheckError:TProcTeeCheckError=nil;
                                    TryReadData:Boolean=False);
var Reader  : TChartReader;
    tmpName : TComponentName;
    DestStream : TStream;
    Header  : TTeeFileHeader;
begin
  if AStream.Size=0 then
     raise ChartException.Create(TeeMsg_EmptyTeeFile);

  if TeeFileIsValid(AStream) then
  begin
    { disable auto-repaint }
    AChart.AutoRepaint:=False;

    { remove all child Series and Tools }
    AChart.ClearChart;

    if TeeFileIsBinary(AStream) then
       DestStream:=AStream
    else
    begin
      DestStream:=TMemoryStream.Create;
      ConvertTeeToBinary(AStream,DestStream);
      DestStream.Position:=0;
    end;

    ReadHeader(DestStream,Header);

    { read the Chart, Series and Tools properties }
    Reader:=TChartReader.Create(DestStream, 4096);
    try
      Reader.OnFindMethod:=Reader.FindMethodEvent;

      Reader.CheckError:=ACheckError;

      tmpName:=AChart.Name;
      Reader.ReadRootComponent(AChart);

      if tmpName<>'' then
         AChart.Name:=tmpName;
    finally
      Reader.Free;
    end;

    if Assigned(AChart) then
    begin
      // compatibility with previous versions saved *.tee files
      // read the Series points
      if TryReadData and (DestStream.Size>DestStream.Position) then
         ReadChartData(DestStream,AChart);

      (*
      { Change each Series ownership: }
      { Doing this breaks TeeFunction objects as DataSource !! }
      if AChart.Owner<>nil then
      for t:=0 to AChart.SeriesCount-1 do
      begin
        AChart[t].Owner.RemoveComponent(AChart[t]);
        AChart.Owner.InsertComponent(AChart[t]);
      end;
      *)

      { restore AutoRepaint }
      AChart.AutoRepaint:=True;
      AChart.Invalidate;
    end
    else raise ChartException.Create(TeeMsg_InvalidTeeFile);

    if DestStream<>AStream then
       DestStream.Free;
  end
  else { is the Stream.Position maybe not zero ? }
    raise ChartException.Create(TeeMsg_WrongTeeFileFormat);
end;

Procedure LoadChartFromStream(const AChart:TCustomChart; const AStream:TStream);
begin
  LoadChartFromStreamCheck(AChart,AStream);
end;

Function TeeCheckExtension(Const AFileName:String):String;
begin  // Append ".tee" to filename if the extension is missing
  if ExtractFileExt(AFileName)='' then
     result:=AFileName+'.'+TeeMsg_TeeExtension
  else
     result:=AFileName;
end;

Procedure LoadChartFromFileCheck( const AChart:TCustomChart;
                                  Const AName:String;
                                  ACheckError:TProcTeeCheckError );
Var tmp : TFileStream;
begin
  tmp:=TFileStream.Create(TeeCheckExtension(AName),fmOpenRead or fmShareDenyWrite);
  try
    LoadChartFromStreamCheck(AChart,tmp,ACheckError);
  finally
    tmp.Free;
  end;
end;

Procedure LoadChartFromFile(const AChart:TCustomChart; Const AFileName:String);
begin
  LoadChartFromFileCheck(AChart,AFileName,nil);
end;

Procedure ConvertTeeStream(Const InputStream,OutputStream:TStream; ToText:Boolean);
begin
end;

Procedure ConvertTeeFile(Const InputFile,OutputFile:String; ToText:Boolean);
var SInput  : TFileStream;
    SOutput : TFileStream;
begin
  SInput:=TFileStream.Create(InputFile,fmOpenRead);
  try
    SOutput:=TFileStream.Create(OutputFile,fmCreate);
    try
      if ToText then ConvertTeeToText(SInput,SOutput)
                else ConvertTeeToBinary(SInput,SOutput);
    finally
      SOutput.Free;
    end;
  finally
    SInput.Free;
  end;
end;

{ Create a text file from a binary *.tee file }
Procedure ConvertTeeFileToText(Const InputFile,OutputFile:String);
begin
  ConvertTeeFile(InputFile,OutputFile,True);
end;

{ Create a binary file from a text *.tee file }
Procedure ConvertTeeFileToBinary(Const InputFile,OutputFile:String);
begin
  ConvertTeeFile(InputFile,OutputFile,False);
end;

{
Procedure WriteChartData(AStream:TStream; AChart:TCustomChart);
Var t : Integer;
begin
  for t:=0 to AChart.SeriesCount-1 do
      TSeriesAccess(AChart[t]).WriteData(AStream);
end;
}

type
  TWriterAccess=class(TWriter);

{$IFDEF FPC}
const
  FilerSignature: array[1..4] of AnsiChar = AnsiString('TPF0');
{$ENDIF}

Procedure SaveChartToStream(const AChart:TCustomChart; const AStream:TStream;
                            IncludeData:Boolean=True;
                            TextFormat:Boolean=False);

  {$IFDEF FPC}
  procedure WriteSignature(const AWriter:TWriter);
  begin
    AWriter.Write(FilerSignature, SizeOf(FilerSignature));
  end;
  {$ENDIF}

var OldName  : TComponentName;
    Writer   : TWriter;
    tmpOwner : TComponent;
    DestStream : TStream;
    t          : Integer;
    tmpSeries  : TChartSeries;
begin
  if TextFormat then
     DestStream:=TMemoryStream.Create
  else
     DestStream:=AStream;

  try
    TeeWriteHeader(DestStream);

    { write the Chart, Series and Tools properties }

    if not (csDesigning in AChart.ComponentState) then
    begin
      OldName:=AChart.Name;
      AChart.Name:='';
    end;

    for t:=0 to AChart.SeriesCount-1 do
    begin
      tmpSeries:=AChart[t];

      if IncludeData then
          TSeriesAccess(tmpSeries).ForceSaveData:=True
      else
          TSeriesAccess(tmpSeries).DontSaveData:=True;
    end;

    try
      Writer := TWriter.Create(DestStream, 4096);
      try
        tmpOwner:=AChart.Owner;
        if not Assigned(tmpOwner) then
           tmpOwner:=AChart;

        TWriterAccess(Writer).SetRoot(tmpOwner);

        {$IFDEF FPC}
        WriteSignature(Writer);
        {$ELSE}
        Writer.WriteSignature;
        {$ENDIF}

        Writer.WriteComponent(AChart);
      finally
        Writer.Free;
      end;

    finally
      for t:=0 to AChart.SeriesCount-1 do
      begin
        tmpSeries:=AChart[t];
        TSeriesAccess(tmpSeries).ForceSaveData:=False;
        TSeriesAccess(tmpSeries).DontSaveData:=False;
      end;
    end;

    if not (csDesigning in AChart.ComponentState) then
       AChart.Name:=OldName;

    if TextFormat then
    begin
      DestStream.Position:=0;
      ConvertTeeToText(DestStream,AStream);
    end;

  finally
    if TextFormat then
       DestStream.Free;
  end;
end;

Procedure SaveChartToXMLStream(const AChart:TCustomChart; const AStream:TStream;
                               IncludeData:Boolean=True;
                               XMLHeader:Boolean=True);
begin
  SaveChartToStream(AChart,AStream,IncludeData,True);
  AStream.Position:=0;
  ConvertTextToXML(AStream,XMLHeader);
end;

Procedure SaveChartToFile(const AChart:TCustomChart; Const AFileName:String;
                          IncludeData:Boolean=True;
                          TextFormat:Boolean=False);
Var tmp : TFileStream;
begin
  tmp:=TFileStream.Create(TeeCheckExtension(AFileName),fmCreate);
  try
    SaveChartToStream(AChart,tmp,IncludeData,TextFormat);
  finally
    tmp.Free;
  end;
end;

Procedure SaveChartToXMLFile(const AChart:TCustomChart; Const AFileName:String;
                             IncludeData:Boolean=True;
                             XMLHeader:Boolean=True);
Var tmp : TFileStream;
begin
  tmp:=TFileStream.Create(TeeCheckExtension(AFileName),fmCreate);
  try
    SaveChartToXMLStream(AChart,tmp,IncludeData,XMLHeader);
  finally
    tmp.Free;
  end;
end;

Function ColorToHex(const Color:TColor):String;
var tmp : Byte;
begin
  tmp:=TTeeCanvas.ColorAlpha(Color);

  with RGBValue(Color) do
  if tmp<>0 then
     result:=Format('#%.2x%.2x%.2x%.2x',[tmp,Red,Green,Blue])
  else
     result:=Format('#%.2x%.2x%.2x',[Red,Green,Blue]);
end;

{ TSeriesData }

Constructor TSeriesData.Create(AChart:TCustomChart; ASeries:TChartSeries=nil);
begin
  inherited Create;

  FSeriesList:=TCustomSeriesList.Create;

  FChart:=AChart;
  FSeries:=ASeries;

  FIncludeLabels:=True;
end;

Destructor TSeriesData.Destroy;
begin
  FSeriesList.Free;
  inherited;
end;

// Returns True if ASeries should be exported.
//
// When SeriesList array is empty, result is True.
// Otherwise ASeries is searched inside the SeriesList array.

function TSeriesData.ValidSeries(const ASeries:TChartSeries):Boolean;
var t : Integer;
begin
  result:=(SeriesList.Count=0) or (ASeries=FSeries);

  if not result then
    for t:=0 to SeriesList.Count-1 do
    if SeriesList[t]=ASeries then
    begin
      result:=True;
      break;
    end;
end;

Procedure TSeriesData.GuessSeriesFormat(ASeries:TChartSeries);
var tmp : TChartSeries;
    t   : Integer;
begin
  tmp:=ASeries;

  if not Assigned(tmp) then
  begin
    if Assigned(FChart) and (FChart.SeriesCount > 0) then
    begin
      for t:=0 to FChart.SeriesCount-1 do
          if ValidSeries(FChart[t]) then
             if FChart[t].Count<>0 then
             begin
               tmp:=FChart[t];
               break;
             end;
    end;
  end;

  if Assigned(tmp) then
     IFormat:=SeriesGuessContents(tmp);
end;

Procedure TSeriesData.GuessSeriesFormat;
begin
  GuessSeriesFormat(FSeries)
end;

Procedure TSeriesData.Prepare;
begin
  Prepare(FSeries);
end;

// Returns the number of Series that should be exported.
function TSeriesData.TotalSeries:Integer;
var t : Integer;
begin
  if Assigned(FSeries) then
     result:=1
  else
  if SeriesList.Count=0 then
     result:=FChart.SeriesCount
  else
  begin
    result:=0;

    for t:=0 to FChart.SeriesCount-1 do
        if ValidSeries(FChart[t]) then
           Inc(result);
  end;
end;

function TSeriesData.TotalColumns: Integer;
var tmp : Integer;
    Row : Integer;
begin
  result:=0;

  if IncludeIndex then Inc(result);

  tmp:=TotalSeries;

  if tfLabel in IFormat then Inc(result,tmp);

  if tfColor in IFormat then Inc(result,tmp);

  if tfNoMandatory in IFormat then Inc(result,tmp);

  if Assigned(FSeries) then
     Inc(result,FSeries.ValuesList.Count-1)
  else
  for Row:=0 to FChart.SeriesCount-1 do
     if ValidSeries(FChart[Row]) then
        Inc(result,FChart[Row].ValuesList.Count-1);
end;

function TSeriesData.TotalRows: Integer;
begin
  result:=MaxSeriesCount; { rows }

  if IncludeHeader then
     Inc(result);
end;

Procedure TSeriesData.Prepare(const ASeries:TChartSeries);
begin
  GuessSeriesFormat(ASeries);

  if not IncludeLabels then
     Exclude(IFormat,tfLabel);

  // Add or remove colors
  if IncludeColors then
     Include(IFormat,tfColor)
  else
     Exclude(IFormat,tfColor);
end;

Function TSeriesData.AsString:String;
Var tmp : Integer;
    t   : Integer;
begin
  Prepare;
  result:='';

  tmp:=MaxSeriesCount;

  for t:=0 to tmp-1 do
      result:=result+PointToString(t)+TeeTextLineSeparator;
end;

Function TSeriesData.ValueToString(const ASeries:TChartSeries; const Value:Double):String;
var tmp : String;
begin
  if Assigned(ASeries) and UseSeriesFormat then
     tmp:=ASeries.ValueFormat
  else
     tmp:=ValueFormat;

  if tmp='' then
     result:=FloatToStr(Value)
  else
     result:=FormatFloat(tmp,Value);
end;

Function TSeriesData.MaxSeriesCount:Integer;
var t : Integer;
begin
  if Assigned(FSeries) then
     result:=FSeries.Count
  else
  begin
    result:=0;

    if Assigned(FChart) then
    for t:=0 to FChart.SeriesCount-1 do
        if ValidSeries(FChart[t]) then
           result:=Math.Max(result,FChart[t].Count);
  end;
end;

function TSeriesData.PointToString(Index: Integer): String;
begin
  result:='';
end;

{$IFDEF D12}
Procedure TSeriesData.SaveToStream(AStream:TStream);
var Bytes    : TBytes;
    Preamble : TBytes;
begin
  if Assigned(FileEncoding) then
     Bytes:=FileEncoding.GetBytes(AsString)
  else
     Bytes:=TEncoding.Default.GetBytes(AsString);

  if Assigned(FileEncoding) then
     Preamble:=FileEncoding.GetPreamble
  else
     Preamble:=TEncoding.Default.GetPreamble;

  if Length(Preamble) > 0 then
     AStream.Write(Preamble[0],Length(Preamble));

  AStream.Write(Bytes[0],Length(Bytes));
end;
{$ENDIF}

{ TSeriesDataText }

Constructor TSeriesDataText.Create(AChart:TCustomChart; ASeries: TChartSeries=nil);
begin
  inherited;
  FTextDelimiter:=TeeTabDelimiter;
  FTextQuotes:='';
end;

function TSeriesDataText.AsString: String;

  Function Header:String;

    Function HeaderSeries(const ASeries:TChartSeries):String;

      Procedure AddToResult(const S:String);
      begin
        if result='' then result:=TextQuotes+S+TextQuotes
                     else result:=result+TextDelimiter+TextQuotes+S+TextQuotes;
      end;

    var t : Integer;
    begin
      result:='';
      With ASeries do
      begin
        if tfNoMandatory in IFormat then
           result:=TextQuotes+NotMandatoryValueList.Name+TextQuotes;

        if ValuesList.Count=2 then
           AddToResult(SeriesTitleOrName(ASeries))
        else
        begin
          AddToResult(MandatoryValueList.Name);
          for t:=2 to ValuesList.Count-1 do
              AddToResult(ValuesList[t].Name);
        end;
      end;
    end;

  var t : Integer;
      tmpFirst : Boolean;
  begin
    if IncludeIndex then result:=TextQuotes+TeeMsg_Index+TextQuotes
                    else result:='';

    if tfLabel in IFormat then
    begin
      if result<>'' then result:=result+TextDelimiter;
      result:=result+TextQuotes+TeeMsg_Text+TextQuotes;
    end;

    if tfColor in IFormat then
    begin
      if result<>'' then result:=result+TextDelimiter;
      result:=result+TextQuotes+TeeMsg_Colors+TextQuotes;
    end;

    if result<>'' then result:=result+TextDelimiter;

    if Assigned(Series) then
       result:=result+HeaderSeries(Series)
    else
    if Chart.SeriesCount>0 then
    begin
      t:=0;

      tmpFirst:=True;

      while t<Chart.SeriesCount do
      begin
        if ValidSeries(Chart[t]) then
           if tmpFirst then
           begin
             result:=result+HeaderSeries(Chart[t]);
             tmpFirst:=False;
           end
           else
              result:=result+TextDelimiter+HeaderSeries(Chart[t]);

        Inc(t);
      end;
    end;
  end;

Begin
  Prepare;

  if IncludeHeader then result:=Header+TeeTextLineSeparator
                   else result:='';

  result:=result+inherited AsString+TeeTextLineSeparator;
end;

procedure TSeriesDataText.GuessSeriesFormat;
var t : Integer;
    tmp : TeeFormatFlag;
begin
  inherited;

  if (not Assigned(Series)) and IncludeLabels and (not (tfLabel in IFormat)) then
  for t:=0 to Chart.SeriesCount-1 do
  if ValidSeries(Chart[t]) then
  begin
    tmp:=SeriesGuessContents(Chart[t]);

    if tfLabel in tmp then
    begin
      Include(IFormat, tfLabel);
      break;
    end;
  end;
end;

function TSeriesDataText.PointToString(Index: Integer): String;
var
  SomethingAdded : Boolean;

  Procedure Add(Const St:String);
  begin
    if not SomethingAdded then // TV52012444
       result:=St
    else
       result:=result+TextDelimiter+St;

    SomethingAdded:=True;
  end;

var
  tmpNum : Integer;

  Procedure DoSeries(const ASeries:TChartSeries);

    Function ValueToStr(const ValueList:TChartValueList; Index:Integer):String;
    begin
      if ASeries.Count>Index then
         if ValueList.DateTime then
            result:=TextQuotes+DateTimeToStr(ValueList.Value[Index])+TextQuotes
         else
            result:=ValueToString(ASeries,ValueList.Value[Index])
      else
         result:='';
    end;

    Function FirstSeriesLabel(Index:Integer):String;
    var t : Integer;
    begin
      result:='';

      if Assigned(Series) then
      begin
        if Series.Count>Index then result:=Series.Labels[Index];
      end
      else
      for t:=0 to Chart.SeriesCount-1 do
      if ValidSeries(Chart[t]) then
        if Chart[t].Count>Index then
        begin
          result:=Chart[t].Labels[Index];
          if result<>'' then break;
        end;

      if result<>'' then
         result:=TextQuotes+result+TextQuotes;
    end;

  var tt : Integer;
  begin
    { current number of exported Series }
    Inc(tmpNum);

    { the point Label text, if exists, and only for first Series }
    if (tmpNum=1) and (tfLabel in IFormat) then
       Add(FirstSeriesLabel(Index));

    { the point Color,  if exists, and only for first Series }
    if (tmpNum=1) and (tfColor in IFormat) then
       if ASeries.Count>Index then Add(ColorToHex(ASeries.ValueColor[Index]))
                              else Add(ColorToHex(clTeeColor));

    { the "X" point value, if exists }
    if tfNoMandatory in IFormat then
       Add(ValueToStr(ASeries.NotMandatoryValueList,Index));

    { the "Y" point value }
    Add(ValueToStr(ASeries.MandatoryValueList,Index));

    { write the rest of values (always) }
    for tt:=2 to ASeries.ValuesList.Count-1 do
        result:=result+TextDelimiter+ValueToStr(ASeries.ValuesList[tt],Index);
  end;

var t : Integer;
begin
  result:='';
  SomethingAdded:=False;

  { Point number? }
  if IncludeIndex then
     Add(TeeStr(Index));

  { Export Series data }
  tmpNum:=0;

  if Assigned(Series) then
     DoSeries(Series)
  else
  for t:=0 to Chart.SeriesCount-1 do
      if ValidSeries(Chart[t]) then
         DoSeries(Chart[t]);
end;

{ TSeriesDataXML }
Constructor TSeriesDataXML.Create(AChart: TCustomChart;
  ASeries: TChartSeries);
begin
  inherited;
  FIncludeHeader:=True;
  FEncoding:=TeeDefaultXMLEncoding;
end;

function TSeriesDataXML.IsEncodingStored:Boolean;
begin
  result:=(Encoding<>'') and (Encoding<>TeeDefaultXMLEncoding);
end;

Function TSeriesDataXML.AsString:String;

  Function SeriesPoints(const ASeries:TChartSeries):String;

    Function GetPointString(Index:Integer):String;

      Procedure AddResult(Const St:String);
      begin
        if result='' then result:=St
                     else result:=result+St;
      end;

      Function Get(const AList:TChartValueList):String;
      begin
        result:=' '+AList.Name+'="'+ValueToString(ASeries,AList.Value[Index])+'"';
      end;

    var tt : Integer;
    begin
      if IncludeIndex then result:='index="'+TeeStr(Index)+'"'
                      else result:='';

      With ASeries do
      begin
        { the point Label text, if exists }
        if tfLabel in IFormat then
           result:=result+' text="'+Labels[Index]+'"';

        { the point Color, if exists }
        if tfColor in IFormat then
           result:=result+' color="'+ColorToHex(ValueColor[Index])+'"';

        { the "X" point value, if exists }
        if tfNoMandatory in IFormat then
           AddResult(Get(NotMandatoryValueList));

        if not IsNull(Index) then // TV52014076
        begin
          { the "Y" point value }
          AddResult(Get(MandatoryValueList));

          { write the rest of values (always) }
          for tt:=2 to ValuesList.Count-1 do
              result:=result+Get(ValuesList[tt]);
        end;
      end;
    end;

  var t : Integer;
  begin
    result:='';
    if ASeries.Count>0 then
       for t:=0 to ASeries.Count-1 do
           result:=result+'<point '+GetPointString(t)+'/>'+TeeTextLineSeparator;
  end;

  Function XMLSeries(const ASeries:TChartSeries):String;
  var
    tmp: string;
    Index: Integer;
  begin
    tmp := GetGallerySeriesName(ASeries);
    Index := Pos('&', tmp);

    if Index > 0 then  // '&' --> '&amp;'
       Insert('amp;', tmp, Index + 1);

    result:=
          '<series title="'+SeriesTitleOrName(ASeries)+'" type="'+
          tmp+'" color="'+
          ColorToHex(ASeries.Color)+'">'+TeeTextLineSeparator+
          '<points count="'+TeeStr(ASeries.Count)+'">'+TeeTextLineSeparator+
          SeriesPoints(ASeries)+
          '</points>'+TeeTextLineSeparator+
          '</series>'+TeeTextLineSeparator+TeeTextLineSeparator;
  end;

var t : Integer;
begin
  Prepare;
  
  if IncludeHeader then
     result:=TeeXMLHeader(Encoding)+TeeTextLineSeparator
  else
     result:='';

  if Assigned(FSeries) then
     result:=result+XMLSeries(FSeries)
  else
  begin
    result:=result+'<chart>'+TeeTextLineSeparator;

    for t:=0 to FChart.SeriesCount-1 do
    if ValidSeries(FChart[t]) then
    begin
      Prepare(FChart[t]); //ME: TV52012679. Prepare for each Serie to catch appropriate format for each one
      result:=result+XMLSeries(FChart[t]);
    end;

    result:=result+'</chart>';
  end;
end;

{ TSeriesDataHTML }
function TSeriesDataHTML.AsString: String;

  Function Header:String;

    Function HeaderSeries(const ASeries:TChartSeries):String;

      Procedure AddCol(Const ColTitle:String);
      begin
        result:=result+'<th>'+ColTitle+'</th>'
      end;

    var t : Integer;
    begin
      result:='';

      if tfLabel in IFormat then
         if (not FirstSeriesLabels) or (ASeries.SeriesIndex=0) then
             AddCol(TeeMsg_Text);

      if tfColor in IFormat then
         AddCol(TeeMsg_Colors);

      with ASeries do
      begin
        if tfNoMandatory in IFormat then
           AddCol(NotMandatoryValueList.Name);

        if ValuesList.Count=2 then
           AddCol(SeriesTitleOrName(ASeries))
        else
        begin
          AddCol(MandatoryValueList.Name);

          for t:=2 to ValuesList.Count-1 do
              AddCol(ValuesList[t].Name);
        end;
      end;
    end;

  var t : Integer;
  begin
    result:='<tr>';
    if IncludeIndex then
       result:=result+'<th>'+TeeMsg_Index+'</th>';

    if Assigned(FSeries) then
       result:=result+HeaderSeries(FSeries)
    else
    if Assigned(FChart) then
    for t:=0 to FChart.SeriesCount-1 do
        if ValidSeries(FChart[t]) then
           result:=result+HeaderSeries(FChart[t]);

    result:=result+'</tr>';
  end;

begin
  Prepare;

  result:='<table border="1">'+TeeTextLineSeparator;

  if IncludeHeader then
     result:=result+Header+TeeTextLineSeparator;

  result:=result+inherited AsString+'</table>';
end;

Function TSeriesDataHTML.PointToString(Index:Integer):String;

  Function GetPointString:String;

    Function GetPointStringSeries(const ASeries:TChartSeries):String;

      Function CellDouble(Const Value:Double):String;
      begin
        result:='<td>'+ValueToString(ASeries,Value)+'</td>';
      end;

    const
      EmptyCell='<td></td>';

    var tt : Integer;
    begin
      result:='';

      With ASeries do
      if (Count-1)<Index then
      begin
        if tfLabel in IFormat then result:=result+EmptyCell;
        if tfColor in IFormat then result:=result+EmptyCell;
        if tfNoMandatory in IFormat then result:=result+EmptyCell;

        result:=result+EmptyCell;

        for tt:=2 to ValuesList.Count-1 do
            result:=result+EmptyCell;
      end
      else
      begin
        { the point Label text, if exists }
        if tfLabel in IFormat then
           if (not FirstSeriesLabels) or (ASeries.SeriesIndex=0) then
              result:=result+'<td>'+Labels[Index]+'</td>';

        { the point Label text, if exists }
        if tfColor in IFormat then
           result:=result+'<td>'+ColorToHex(ValueColor[Index])+'</td>';

        { the "X" point value, if exists }
        if tfNoMandatory in IFormat then
           result:=result+CellDouble(NotMandatoryValueList.Value[Index]);

        { the "Y" point value }
        result:=result+CellDouble(MandatoryValueList.Value[Index]);

        { write the rest of values (always) }
        for tt:=2 to ValuesList.Count-1 do
            result:=result+CellDouble(ValuesList[tt].Value[Index]);
      end;
    end;

  var t : Integer;
  begin
    if IncludeIndex then result:='<td>'+TeeStr(Index)+'</td>'
                    else result:='';

    if Assigned(FSeries) then
       result:=result+GetPointStringSeries(FSeries)
    else
    for t:=0 to FChart.SeriesCount-1 do
        if ValidSeries(FChart[t]) then
           result:=result+GetPointStringSeries(FChart[t])
  end;

begin
  result:='<tr>'+GetPointString+'</tr>';
end;

{ TSeriesDataXLS }

Procedure TSeriesDataXLS.SaveToStream(AStream:TStream);
var Buf : Array[0..4] of Word;
    Row : Integer;
    Col : Integer;

  Procedure WriteBuf(Value,Size:Word);
  begin
    Buf[0]:=Value;
    Buf[1]:=Size;
    AStream.Write(Buf,2*SizeOf(Word));
  end;

  Procedure WriteParams(Value,Size:Word);
  Const Attr:Array[0..2] of Byte=(0,0,0);
  begin
    WriteBuf(Value,Size+2*SizeOf(Word)+SizeOf(Attr));

    if IncludeHeader then WriteBuf(Row+1,Col)
                     else WriteBuf(Row,Col);

    AStream.Write(Attr,SizeOf(Attr));
  end;

  procedure WriteDouble(Const Value:Double);
  begin
    WriteParams(3,SizeOf(Double));
    AStream.WriteBuffer(Value,SizeOf(Double));
  end;

  {$IFDEF D12}
  {$DEFINE D12Win32}
  {$ENDIF}

  {$IFDEF D12Win32}

  procedure WriteText(Const Value:String);
  {$IFNDEF HEADERBYTES}
  var tmpAnsi : ShortString;
  {$ENDIF}
  begin
    {$IFDEF HEADERBYTES}
    WriteParams(4,Length(Value)+1);
    AStream.Write(Value,Length(Value)+1);
    {$ELSE}
    tmpAnsi:=ShortString(Value);
    WriteParams(4,Length(tmpAnsi)+1);
    AStream.Write(tmpAnsi,Length(tmpAnsi)+1);
    {$ENDIF}
  end;

  {$ELSE}

  procedure WriteText(Const Value:ShortString);
  begin
    WriteParams(4,Length(Value)+1);
    AStream.Write(Value,Length(Value)+1)
  end;

  {$ENDIF}

  procedure WriteNull;
  begin
    WriteParams(1,0);
  end;

  Procedure WriteHeaderSeries(const ASeries:TChartSeries);
  var tt : Integer;
  begin
    if tfLabel in IFormat then
    begin
      WriteText(TeeMsg_Text);
      Inc(Col);
    end;

    if tfColor in IFormat then
    begin
      WriteText(TeeMsg_Colors);
      Inc(Col);
    end;

    if tfNoMandatory in IFormat then
    begin
      WriteText(ASeries.NotMandatoryValueList.Name);
      Inc(Col);
    end;

    for tt:=1 to ASeries.ValuesList.Count-1 do
    begin
      WriteText(ASeries.ValuesList[tt].Name);
      Inc(Col);
    end;
  end;

  Procedure WriteSeries(const ASeries:TChartSeries);

    procedure WriteValue(const AList:TChartValueList);
    var tmp : String;
    begin
      if UseSeriesFormat then
         tmp:=ASeries.ValueFormat
      else
         tmp:=ValueFormat;

      if tmp='' then
         WriteDouble(AList.Value[Row])
      else
         // Trick, use format string to "round" value:
         WriteDouble(StrToFloat(ValueToString(ASeries,AList.Value[Row])));
    end;

  var tt : Integer;
  begin
    if (ASeries.Count-1)<Row then
    begin
      if tfLabel in IFormat then
      begin
        WriteText('');
        Inc(Col);
      end;

      if tfColor in IFormat then
      begin
        WriteText('');
        Inc(Col);
      end;

      if tfNoMandatory in IFormat then
      begin
        WriteNull;
        Inc(Col);
      end;

      for tt:=1 to ASeries.ValuesList.Count-1 do
      begin
        WriteNull;
        Inc(Col);
      end;
    end
    else
    begin
      if tfLabel in IFormat then
      begin
        WriteText(ASeries.Labels[Row]);
        Inc(Col);
      end;

      if tfColor in IFormat then
      begin
        WriteText(ColorToHex(ASeries.ValueColor[Row]));
        Inc(Col);
      end;

      if tfNoMandatory in IFormat then
      begin
        WriteValue(ASeries.NotMandatoryValueList);
        Inc(Col);
      end;

      if ASeries.IsNull(Row) then
      for tt:=1 to ASeries.ValuesList.Count-1 do
      begin
        WriteNull;
        Inc(Col);
      end
      else
      begin
        WriteValue(ASeries.MandatoryValueList);

        Inc(Col);

        for tt:=2 to ASeries.ValuesList.Count-1 do
        begin
          WriteValue(ASeries.ValuesList[tt]);
          Inc(Col);
        end;
      end;
    end;
  end;

Const //BeginExcel : Array[0..5] of Word=($809,8,$500,$10,0,0);  // $ 500 support later Excel 2010 on Japanese m/c
      BeginExcel : Array[0..1] of Word=(0,$09);
      EndExcel   : Array[0..1] of Word=($A,0);

var tt  : Integer;
begin
  Prepare;

  WriteBuf($09, 4);
  AStream.Write(BeginExcel, SizeOf(BeginExcel));

  if IncludeHeader then
  begin
    Row:=-1;
    Col:=0;

    if IncludeIndex then
    begin
      WriteText(TeeMsg_Index);
      Inc(Col);
    end;

    if Assigned(FSeries) then
       WriteHeaderSeries(FSeries)
    else
    for tt:=0 to FChart.SeriesCount-1 do
       if ValidSeries(FChart[tt]) then
          WriteHeaderSeries(FChart[tt]);
  end;

  for Row:=0 to MaxSeriesCount-1 do
  begin
    Col:=0;

    if IncludeIndex then
    begin
      WriteDouble(Row);
      Inc(Col);
    end;

    if Assigned(FSeries) then
       WriteSeries(FSeries)
    else
    for tt:=0 to FChart.SeriesCount-1 do
       if ValidSeries(FChart[tt]) then
          WriteSeries(FChart[tt]);
  end;

  AStream.WriteBuffer(EndExcel,SizeOf(EndExcel)); { end }
end;

function JSONColor(const AColor:TColor):String;
begin
  result:=Format('%s%.6x', ['#', AColor]);
end;

{ TSeriesDataJSON }

Constructor TSeriesDataJSON.Create(AChart:TCustomChart; ASeries:TChartSeries=nil);
begin
  inherited;
end;

const
  CRLF=TeeTextLineSeparator;

Function TSeriesDataJSON.AsString:String;

  function SeriesPoints(const ASeries:TChartSeries; const Tabs:String):String;

    function SeriesPoint(Index:Integer):String;

      function ValueToStr(const AList:TChartValueList; AIndex:Integer):String;
      const
        Jan1st_1970:TDateTime=25569.0;
      begin
        if AList.DateTime and (FDateFormat=jdJavaScript) then
           result:=IntToStr(Round((AList.Value[Index]-Jan1st_1970)*86400000))
        else
           result:=FloatToStr(AList.Value[AIndex]);
      end;

    begin
      // { "value":123, "x": 21 },

      result:='{ "value":'+ValueToStr(ASeries.MandatoryValueList,Index);

      if tfNoMandatory in IFormat then
         result:=result+', "x":'+ValueToStr(ASeries.NotMandatoryValueList,Index);

      if tfColor in IFormat then
         result:=result+', "color":"'+JSONColor(ASeries.ValueColor[Index])+'"';

      if tfLabel in IFormat then
         result:=result+', "name":"'+ASeries.XLabel[Index]+'"';

      result:=result+' }';
    end;

  var t : Integer;
  begin
    Prepare(ASeries);

    result:='';

    for t:=0 to ASeries.Count-1 do
    begin
      result:=result+Tabs+' '+SeriesPoint(t);

      if t<ASeries.Count-1 then
         result:=result+',';

      result:=result+CRLF;
    end;
  end;

  procedure AddSeries(const ASeries:TChartSeries; const Tabs:String);
  var tmp : String;
  begin
    result:=result+Tabs+'{'+CRLF;
    result:=result+Tabs+' "series": {'+CRLF;

    tmp:=SeriesTitleOrName(ASeries);

    if tmp<>'' then
       result:=result+Tabs+' "name":"'+tmp+'",'+CRLF;

    result:=result+Tabs+' "color":"'+JSONColor(ASeries.Color)+'",'+CRLF;

    result:=result+Tabs+' "point": ['+CRLF;

    result:=result+SeriesPoints(ASeries,Tabs+'  ');

    result:=result+Tabs+' ]'+CRLF;

    result:=result+Tabs+' }'+CRLF;
    result:=result+Tabs+'}';
  end;

var t : Integer;
    tmpFirst : Boolean;
begin
  result:='{ "chart": ['+CRLF;

  if Assigned(Series) then
  begin
    AddSeries(Series,'  ');
    result:=result+','+CRLF;
  end
  else
  begin
    tmpFirst:=True;

    for t:=0 to Chart.SeriesCount-1 do
    if ValidSeries(Chart[t]) then
    begin
       if tmpFirst then
          tmpFirst:=False
       else
          result:=result+',';

       AddSeries(Chart[t],'  ');

       result:=result+CRLF;
    end;
  end;

  result:=result+' ]'+CRLF;
  result:=result+'}'+CRLF;
end;

{$IFNDEF FMX}
{$IFNDEF TEEOCX}
type
  TChartChart=class(TChart) end;
  TODBCChart=class(TChartChart) end;

initialization
  RegisterClasses([TChartChart,TODBCChart]);
finalization
  UnRegisterClasses([TChartChart,TODBCChart]);
{$ENDIF}
{$ENDIF}
end.

