{********************************************}
{ TeeChart Data Import Component             }
{ Copyright (c) 2013-2020 by Steema Software }
{ All Rights Reserved                        }
{********************************************}
unit TeeImport;
{$I TeeDefs.inc}

// This unit contains a component TDataImport, to automatically fill a Chart
// from a variety of sources.

// Supported data formats:

// CSV (comma separated text, or other separators like tab/space etc)
// XML
// JSON
// Excel
// Dataset (TDataSource, TClientDataset, etc, etc)
// RTTI (TList<>, TCollection, TValue)
// Graphic picture

// Data origin can be an object, a component, a control like a TMemo, a stream,
// a file path, an url, etc.

// Based on analyzed content, the Chart is filled with series loaded with data.
// Field values, text labels, etc are automatically selected.

(*
  Example:

  DataImport1.Chart := Chart1;
  DataImport1.Import( MyData ); <--- MyData can be any object, component, stream, etc.
*)


interface

{$IFDEF D19}

 {$IFNDEF TEENOREST} // Define TEENOREST to NOT use the REST components
 {$DEFINE HASREST}
 {$ENDIF}

 // Warning the SOAPRTL lib in XE6 for Android platform is missing.
 // Hotfix for XE6 includes libs: http://cc.embarcadero.com/Item/29847
 {$IFDEF ANDROID}
  {$IFDEF D20}
   {$IFNDEF D21}
     {$UNDEF HASREST}
   {$ENDIF}
  {$ENDIF}
 {$ENDIF}

{$ENDIF}

{$IFDEF D14}
 {$DEFINE HASJSON}
{$ELSE}
{$IFDEF D7}
 {$IFDEF HASLKJSON}
  {$DEFINE HASJSON}
 {$ENDIF}
{$ENDIF}
{$ENDIF}

{$IFDEF D14}
 {$DEFINE HASRTTI}
{$ENDIF}

{$IFDEF MSWINDOWS}
 {$IFNDEF FPC} // Excel not available in Lazarus and XE2 iOS (FMI)
  {$DEFINE HASEXCEL}
 {$ENDIF}
{$ENDIF}

{$IFDEF D6}
 {$DEFINE HASXML}

 {$IFNDEF FPC}
 {$DEFINE HASINDY}
 {$ENDIF}

{$ENDIF}

{$IFDEF D16}
{$DEFINE HASZIP}
{$ENDIF}

{$IFNDEF FMX}
{$IFNDEF LCL} // Lazarus
{$IFNDEF NOUSE_BDE}
{$DEFINE HASBDE}
{$ENDIF}
{$ENDIF}
{$ENDIF}

uses
  {$IFNDEF D6}
  Windows, // <-- TRect
  {$ENDIF}

  Classes, SysUtils,
  {$IFDEF D6}
  Variants,
  {$ENDIF}

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

  {$IFDEF FMX}

  FMX.Types,

  FMXTee.Engine, FMXTee.Chart, FMXTee.Store, FMXTee.Series.World,
  FMXTee.Series.Candle,

  {$IFDEF D19}
  FMX.Graphics,
  {$ENDIF}

  {$IFDEF NEXTGEN}
  {$IFDEF POSIX}
  Posix.Unistd, // <-- inline DeleteFile
  {$ENDIF}
  {$ENDIF}

  FMX.Grid,

  {$ELSE}
  Graphics, Grids, TeEngine, Chart, TeeStore, TeeWorldSeries,
  CandleCh,

  {$IFNDEF HASINDY}
  TeeURL,
  {$ENDIF}

  {$ENDIF}

  // REST

  {$IFDEF HASREST}
  Rest.Client,
  {$ENDIF}

  // RTTI

  {$IFDEF HASRTTI}
  RTTI, Generics.Collections,
  {$ENDIF}

  // ZIP
  {$IFDEF HASZIP}
  System.Zip,
  {$ENDIF}

  // DBF & DB
  {$IFDEF HASBDE}
  DBTables,
  {$ENDIF}

  // JSON

  {$IFDEF HASJSON}
   {$IFDEF D20}
   System.JSON,
   {$ELSE}
   {$IFDEF D14}
   DBXJSON,
   {$ELSE}
   uLkJSON,
   {$ENDIF}
   {$ENDIF}
  {$ENDIF}
             
  // XML
  {$IFDEF HASXML}
    {$IFDEF FPC}
    DOM, XMLRead,
    {$ELSE}
    XMLIntf, XMLDoc,
    {$ENDIF}
  {$ENDIF}

  {$IFDEF D6}
  Types,
  {$ENDIF}

  // Database
  DB;

type
  {$IFDEF HASLKJSON}
  TJSONValue=TlkJSONbase;
  TJSONArray=TlkJSONlist;
  TJSONObject=TlkJSONobject;
  TJSONNumber=TlkJSONnumber;
  TJSONBoolean=TlkJSONnumber;
  {$ENDIF}

  EDataImport=class(Exception);

  TDataImport=class;

  TDataImportProgress=procedure(Sender:TDataImport; Current,Total:Cardinal; var Cancel:Boolean) of object;

  TDataImportStyle=(isAutomatic, is2D, isXY, isGrid, isXYZ, isFinancial, isGeographic);

  TKeepSetting=(ksSeries, ksTitle, ksAxesTitle, ksLegendTitle, ksView3D);
  TKeepSettings=set of TKeepSetting;

  TDataFormat=class(TCustomChart)
  private
    {$IFDEF D7}
    ISettings : TFormatSettings;
    {$ENDIF}

    IGeoMap : TWorldMap;

    FirstRow,
    LastRow : Cardinal;

    AutoLoaded,
    DecimalDetected : Boolean;

    function CloneSeries(const AOwner:TComponent):TChartSeriesList;
    function GuessValue(const AText:String; const AIndex:Integer; const ATitle:String):Boolean;
    procedure NewSeriesIndex(const AIndex:Integer; const ATitle:String);
    procedure TitleFromHeaders(const AStrings:TStrings);
    procedure TrySetTextField(const AIndex:Integer; const ATitle:String);
  protected
    procedure Analyze; virtual;

    procedure BeginLoop; virtual;
    procedure EndLoop; virtual;

    procedure ChangedOption(Sender:TObject); virtual;

    function GetText(const ARow:Cardinal):String; virtual;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; virtual;
    function NextIteration:Boolean; virtual;
    function Options:TStrings; virtual;
    procedure PrepareOption(const AComponent:TComponent; const AIndex:Integer); virtual;
    procedure SetTitles(const AChart:TCustomChart; const AKeep:TKeepSettings);
    function ValidRow(const ARow:Cardinal):Boolean; virtual;
  public
    ActualStyle : TDataImportStyle;

    TextField : Integer;
    TextFieldName : String;

    Headers : Array of String;
    HeaderLines : Word;

    SeriesTitles : Array of String;
    SeriesIndex : Array of Word; // of Array of Word <-- Multiple ValueLists

    DataQuantity : Cardinal;

    Constructor Create; reintroduce;

    procedure CreateSeries(const AOwner:TComponent);
  end;

  TDataImportStreamFormat=(ifCSV, ifXML, ifJSON);

  {$IFDEF D16}
  [ComponentPlatformsAttribute(TeeAllComponentPlatformIDs)]
  {$ENDIF}
  TDataImport=class(TCustomChartLink)
  private
    FKeep  : TKeepSettings;
    FOnProgress : TDataImportProgress;
    FStyle : TDataImportStyle;

    ICurrent,
    ITotal : Cardinal;
    ICancel : Boolean;

    function FindGeoEntity(const ASeries:TWorldSeries; const AEntity:String):Integer;

    procedure Finish(const AFormat:TDataFormat; const AChart:TCustomChart; const ASeries:TChartSeriesList);
    procedure FinishLocalFormat(const AFormat:TDataFormat);
    function InitFormat(const AQuantity: Cardinal):TDataFormat;

    procedure SetGeoEntity(const ASeries:TWorldSeries; const AEntity:String; const AValue:TChartValue);

    procedure SynchronizeProgress;

    procedure TryFreeSeries(const AChart:TCustomChart; Force:Boolean=False);
    procedure TryProgress(const Current,Total:Cardinal);
  protected
    IFormat : TDataFormat;

    procedure LocalImport(const ADataset:TDataset; DoFree:Boolean); overload;
    procedure LocalImport(const AStrings:TStrings; DoFree:Boolean); overload;
  public
    AnalyzeMode : Boolean;

    Destructor Destroy; override;

    // Returns True when AComponent is supported (ie: it can be imported)
    class function CanImport(const AComponent: TComponent):Boolean;

    class procedure GroupSimilarSeries(const AList:TCustomSeriesList);

    // Import from a TDataFormat abstract class, this allows custom formats:
    procedure Import(const AFormat:TDataFormat); overload;

    procedure Import(const AObject:TObject); overload;
    procedure Import(const AComponent:TComponent); overload;
    procedure Import(const AStrings:TStrings); overload;
    procedure Import(const AStream:TStream; const AFormat:TDataImportStreamFormat); overload;
    procedure Import(const AStream:TStringStream); overload;
    procedure Import(const ADataset:TDataset); overload;
    procedure Import(const AText:String); overload;
    procedure Import(const AGraphic:{$IFDEF FMX}TBitmap{$ELSE}TGraphic{$ENDIF}); overload;

    procedure Import(const Data:Array of Double); overload;
    procedure Import(const Data:Array of Integer); overload;
    procedure Import(const Data:Array of Int64); overload;

    procedure Import(const AChart:TCustomChart); overload;
    procedure Import(const ASeries:TChartSeries); overload;
    procedure Import(const AGrid:TStringGrid; const AColumns:Array of {$IFDEF FMX}TColumn{$ELSE}Integer{$ENDIF}); overload;

    {$IFDEF HASXML}
    procedure Import(const AXML:TXMLDocument); overload;

    {$IFNDEF FPC}
    procedure Import(const AXML:IXMLDocument); overload;
    {$ENDIF}

    procedure ImportXML(const AStrings:TStrings); overload;
    procedure ImportXML(const AXML:String); overload;
    procedure ImportHTML(const AHTML:String; const ATableID:String=''); overload;
    {$ENDIF}

    {$IFDEF HASRTTI}
    procedure Import(const AData:TValue); overload;

    // Pending:
    // procedure Import(const AList:TList<T>) ... generics

    procedure Import(const ACollection:TCollection); overload;
    {$ENDIF}

    {$IFDEF HASJSON}
    procedure ImportJSON(const AStrings:TStrings; const Root:String=''; const Field:String=''); overload;
    procedure ImportJSON(const AText:String; const Root:String=''; const Field:String=''); overload;
    procedure Import(const AJSON:TJSONValue; const Root:String=''; const Field:String=''); overload;
    {$ENDIF}

    {$IFDEF HASREST}
    procedure Import(const AResponse:TRESTResponse); overload;
    {$ENDIF}

    {$IFDEF HASEXCEL}
    procedure ImportExcelFile(const APath:String; const AWorkSheet:String='';
                              const ARange:String='');
    {$ENDIF}

    procedure ImportFromFile(const APath:String); overload;
    procedure ImportFromURL(const AURL:String); overload;

    // Import data with a DataFormat settings into AChart.
    // (Just add already-analyzed data to AChart).
    procedure ImportToChart(const AFormat:TDataFormat; const AChart:TCustomChart);
  published
    property Chart;
    property KeepSettings:TKeepSettings read FKeep write FKeep default [];
    property Style:TDataImportStyle read FStyle write FStyle default isAutomatic;

    property OnProgress:TDataImportProgress read FOnProgress write FOnProgress;
  end;

implementation

uses
  Math,

  {$IFDEF FMX}
  System.UITypes,
  FMXTee.Procs, FMXTee.Series, FMXTee.Series.Surface, FMXTee.Tools,
  FMXTee.Dataset, FMXTee.Series.Map, FMXTee.Tools.ScrollPager,
  FMX.ListBox, FMX.Memo, FMX.Edit, FMX.Controls, FMX.Objects,

  {$IFDEF D18}
  FMX.StdCtrls,
  {$ENDIF}

  {$ELSE}

  TeeProcs, Series, TeeSurfa, TeeTools, TeeData, TeeMapSeries, TeeExport,
  TeeScrollPagerTool,

  Controls, StdCtrls, ExtCtrls,
  {$ENDIF}

  {$IFDEF HASEXCEL}
  ActiveX, ComObj,
  {$ENDIF}

  {$IFDEF NEXTGEN}
  System.Character,
  {$ENDIF}

  // URL
  IniFiles,

  {$IFDEF HASINDY}
  IdHTTP, IdFTP, IdURI, IdSSLOpenSSL,
  {$ENDIF}

  {$IFDEF D14}
  {$IFNDEF D21}
  TypInfo, // <-- tkInteger, etc
  {$ENDIF}
  {$ENDIF}

  // ClientDataset
  {$IFDEF FPC}
  BufDataset
  {$ELSE}
  DBClient
  {$ENDIF}
  ;


const
  TeeMsg_Longitude='Longitude';
  TeeMsg_Latitude='Latitude';

  {$IFDEF HASEXCEL}
  TeeMsg_ExcelNotInstalled='Cannot start Microsoft Excel. Is it correctly installed?';
  {$ENDIF}

function TextFromFile(const APath:TFileName):String;
{$IFNDEF D16}
var S : TStrings;
{$ENDIF}
begin
  {$IFDEF D16}
  result:=TFile.ReadAllText(APath);
  {$ELSE}
  S:=TStringList.Create;
  try
    S.LoadFromFile(APath);
    result:=S.Text;
  finally
    S.Free;
  end;
  {$ENDIF}
end;

// Removes quotes (" and ') from S string
function RemoveQuotes(const S:String):String;
var L : Integer;
begin
  result:=S;

  if (Copy(result,1,1)='"') or (Copy(result,1,1)='''') then
     Delete(result,1,1);

  L:=Length(result);

  if (Copy(result,L,1)='"') or (Copy(result,L,1)='''') then
     Delete(result,L,1);
end;

// Internal use, to detect "Zero Based Strings"
var
  ZBS : Boolean=False; // Zero-based strings
  FirstStringChar : Integer=1;

{ TDataImport }

Destructor TDataImport.Destroy;
begin
  IFormat.Free;
  inherited;
end;

procedure TDataImport.Import(const AGraphic:{$IFDEF FMX}TBitmap{$ELSE}TGraphic{$ENDIF});
var R : TRectangleTool;
    W,H : Integer;
begin
  if Assigned(Chart) then
  begin
    R:=TRectangleTool.Create(Chart.Owner);

    R.Shape.Transparency:=10;
    R.Shape.Picture.Graphic:=AGraphic;

    //R.Name:=CorrectName(RemoveFileExtension(ExtractFileName(APath)));

    W:=R.Shape.Picture.Width;
    H:=R.Shape.Picture.Height;

    if H>0 then
       R.Width:=Round(R.Height*(W/H));

    Chart.Tools.Add(R);
  end;
end;

type
  {$IFDEF FMX}
  TColumnAccess=class(TColumn);
  {$ELSE}
  TControlAccess=class(TControl);
  {$ENDIF}

procedure TDataImport.TryFreeSeries(const AChart:TCustomChart; Force:Boolean=False);
begin
  if Force or (not (ksSeries in FKeep)) then
  begin
    AChart.FreeAllSeries;
    AChart.SeriesGroups.Clear;
  end;
end;

procedure TDataImport.Import(const AChart:TCustomChart);
var M : TMemoryStream;

    tmpAlign : {$IFDEF FMX}TAlignLayout{$ELSE}TAlign{$ENDIF};
    tmpR : {$IFDEF FMX}TRectF{$ELSE}TRect{$ENDIF};
begin
  M:=TMemoryStream.Create;
  try
    SaveChartToStream(AChart,M);
    M.Position:=0;

    TryFreeSeries(Chart,True);

    tmpAlign:=Chart.Align;

    tmpR:=Chart.{$IFDEF FMX}{$IFDEF D22}BoundsRect{$ELSE}ParentedRect{$ENDIF}{$ELSE}BoundsRect{$ENDIF};

    try
      LoadChartFromStream(Chart,M);
    finally
      Chart.Align:=tmpAlign;

      {$IFDEF FMX}
      Chart.Position.X:=tmpR.Left;
      Chart.Position.Y:=tmpR.Top;
      {$ELSE}
      Chart.Left:=tmpR.Left;
      Chart.Top:=tmpR.Top;
      {$ENDIF}

      Chart.Width:=tmpR.Right-tmpR.Left;
      Chart.Height:=tmpR.Bottom-tmpR.Top;
    end;

  finally
    M.Free;
  end;
end;

procedure TDataImport.Import(const ASeries:TChartSeries);
begin
  if Assigned(ASeries) then
  begin
    TryFreeSeries(Chart);
    CloneChartSeries(ASeries).ParentChart:=Chart;
  end
  else
    raise EDataImport.Create('Cannot import. Series parameter is nil.');
end;

procedure TDataImport.SynchronizeProgress;
begin
  FOnProgress(Self,ICurrent,ITotal,ICancel);
end;

const
  ProgressStep=100;

procedure TDataImport.TryProgress(const Current,Total:Cardinal);
begin
  ICancel:=False;
  ICurrent:=Current;
  ITotal:=Total;

  {$IFDEF D7}
  TThread.Synchronize(nil,SynchronizeProgress);
  {$ELSE}
  SynchronizeProgress;
  {$ENDIF}

  if ICancel then
     Abort;
end;

type
  TGridFormat=class(TDataFormat)
  private
    FGrid : TStringGrid;

    FColumns : Array of {$IFDEF FMX}TColumn{$ELSE}Integer{$ENDIF};
  protected
    procedure Analyze; override;

    function GetText(const ARow:Cardinal):String; override;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; override;
    function ValidRow(const ARow:Cardinal):Boolean; override;
  public
    Constructor Create(const AGrid:TStringGrid; const AColumns:Array of {$IFDEF FMX}TColumn{$ELSE}Integer{$ENDIF});
  end;

Constructor TGridFormat.Create(const AGrid:TStringGrid; const AColumns:Array of {$IFDEF FMX}TColumn{$ELSE}Integer{$ENDIF});
var t, L : Integer;
begin
  inherited Create;
  FGrid:=AGrid;

  L:=Length(AColumns);
  SetLength(FColumns,L);

  for t:=0 to L-1 do
      FColumns[t]:=AColumns[Low(AColumns)+t];
end;

function TGridFormat.GetText(const ARow:Cardinal):String;
begin
  if TextField=-1 then
     result:=''
  else
     result:=FGrid.Cells[TextField,ARow];
end;

function TGridFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
begin
  result:=TryStrToFloat(FGrid.Cells[SeriesIndex[ACol],ARow],Value {$IFDEF D7},ISettings{$ENDIF});
end;

function TGridFormat.ValidRow(const ARow:Cardinal):Boolean;
var col : Integer;
begin
  for col:=0 to FGrid.{$IFDEF FMX}ColumnCount{$ELSE}ColCount{$ENDIF}-1 do
      if FGrid.Cells[col,ARow]<>'' then
      begin
        result:=True;
        Exit;
      end;

  result:=False;
end;

procedure TGridFormat.Analyze;

  function CountOfFilledRows(Start:Integer):Cardinal;
  var t : Integer;
  begin
    result:=0;

    for t:=Start to FGrid.RowCount-1 do
        if ValidRow(t) then
           Inc(result);
  end;

  procedure GuessColumn(const ACol:Integer);
  var tmp : String;
  begin
    if FirstRow=0 then
       tmp:=''
    else
       tmp:=FGrid.Cells[ACol,FirstRow-1];

    GuessValue(FGrid.Cells[ACol,FirstRow],ACol,tmp);
  end;

var col,
    NumCols : Integer;
begin
  inherited;

  FirstRow:={$IFDEF FMX}1{$ELSE}FGrid.FixedRows{$ENDIF};

  DataQuantity:=CountOfFilledRows(FirstRow);

  if FGrid.RowCount>0 then
     LastRow:=FGrid.RowCount-1;

  NumCols:=FGrid.{$IFDEF FMX}ColumnCount{$ELSE}ColCount{$ENDIF};

  if DataQuantity>0 then
     if Length(FColumns)=0 then
        for col:=0 to NumCols-1 do
            GuessColumn(col)
     else
        for col:=Low(FColumns) to High(FColumns) do
            if FColumns[col]{$IFDEF FMX}.Index{$ENDIF}<NumCols then
               GuessColumn(FColumns[col]{$IFDEF FMX}.Index{$ENDIF});

  if (NumCols>0) and (FGrid.RowCount>0) then
     Title.Caption:=FGrid.Cells[0,0];
end;

procedure TDataImport.Import(const AFormat:TDataFormat);

  procedure GuessXYZ;

    function IsField(Index:Integer; const Search:String):Boolean;
    begin
      result:=SameText(AFormat.SeriesTitles[Index],Search);
    end;

  begin
    if Length(AFormat.SeriesIndex)=3 then
       if (FStyle=isXYZ) or
                    ( (FStyle=isAutomatic) and
                      (IsField(0,'X') and IsField(1,'Y') and IsField(2,'Z'))
                    ) then
          AFormat.ActualStyle:=isXYZ;
  end;

begin
  AFormat.Title.Caption:='';
  AFormat.Analyze;

  if AFormat.ActualStyle<>isGrid then
     GuessXYZ;

  AFormat.CreateSeries(nil);

  if not AnalyzeMode then
     ImportToChart(AFormat,FChart);
end;

procedure TDataImport.ImportToChart(const AFormat:TDataFormat; const AChart:TCustomChart);

  procedure Loop(const ASeries:TChartSeriesList);
  var t,
      tmp : Integer;

      row : Cardinal;

      X,Y,Z : TChartValue;

      tmpXYZ : TCustom3DSeries;

      Text : String;
      Value : TChartValue;
      Total : Cardinal;
  begin
    Total:=AFormat.LastRow-AFormat.FirstRow+1;

    if Assigned(FOnProgress) then
       TryProgress(0,Total);

    AFormat.BeginLoop;

    try
      if (AFormat.ActualStyle=isGrid) or (AFormat.ActualStyle=isXYZ) then
         tmpXYZ:=(ASeries[0] as TCustom3DSeries)
      else
         tmpXYZ:=nil;

      for row:=AFormat.FirstRow to AFormat.LastRow do
      begin
        if Assigned(FOnProgress) then
        begin
          ICurrent:=row-AFormat.FirstRow;

          if (ICurrent mod ProgressStep)=0 then
             TryProgress(ICurrent,Total);
        end;

        if AFormat.ValidRow(row) then
        begin
          Text:=AFormat.GetText(row);

          if AFormat.ActualStyle=isGrid then
          begin
            for t:=Low(AFormat.SeriesIndex) to High(AFormat.SeriesIndex) do
                if AFormat.GetValue(t,row,Value) then
                   tmpXYZ.AddXYZ(t,Value,row)
                else
                   tmpXYZ.AddXYZ(t,0,row);
          end
          else
          for t:=0 to ASeries.Count-1 do
          if ASeries[t].Visible then
          begin
            if AFormat.ActualStyle=isXYZ then
            begin
              if AFormat.GetValue(t,row,X) and
                 AFormat.GetValue(t+1,row,Y) and
                 AFormat.GetValue(t+2,row,Z) then
                   tmpXYZ.AddXYZ(X,Y,Z);
            end
            else
            begin
              if AFormat.GetValue(t,row,Value) then
                 if AFormat.ActualStyle=isGeographic then
                    SetGeoEntity(TWorldSeries(ASeries[t]),Text,Value)
                 else
                    ASeries[t].Add(Value,Text)
              else
              if AFormat.ActualStyle=isGeographic then
              begin
                tmp:=FindGeoEntity(TWorldSeries(ASeries[t]),Text);

                if tmp=-1 then
                   ASeries[t].AddNull(Text)
                else
                   ASeries[t].SetNull(tmp,True)
              end
              else
                 ASeries[t].AddNull(Text);
            end;
          end;
        end;

        if not AFormat.NextIteration then
           break;
      end;

      if Assigned(FOnProgress) then
         TryProgress(Total,Total);

    finally
      AFormat.EndLoop;

      if Assigned(FOnProgress) then
         TryProgress(0,Total);
    end;
  end;

var tmp : TChartSeriesList;
begin
  TryFreeSeries(AChart);

  if (AFormat.DataQuantity>0) and (Length(AFormat.SeriesIndex)>0) then
  begin
    tmp:=AFormat.CloneSeries(AChart.Owner);
    try
      try
        if not AFormat.AutoLoaded then
           Loop(tmp);

        Finish(AFormat,AChart,tmp);
      except
        on Exception do
        begin
          AFormat.FreeAllSeries;
          raise;
        end;
      end;
    finally
      tmp.Free;
    end;
  end
  else
  if AFormat.HeaderLines>0 then
     // No data. We can just use text to display at Chart title
     AFormat.TitleFromHeaders(AChart.Title.Text);
end;

procedure TDataImport.FinishLocalFormat(const AFormat:TDataFormat);
begin
  if AnalyzeMode then
  begin
    IFormat.Free;
    IFormat:=AFormat;
  end
  else
     AFormat.{$IFDEF AUTOREFCOUNT}DisposeOf{$ELSE}Free{$ENDIF};
end;

procedure TDataImport.Import(const AGrid:TStringGrid; const AColumns:Array of {$IFDEF FMX}TColumn{$ELSE}Integer{$ENDIF});
var tmp : TGridFormat;
begin
  tmp:=TGridFormat.Create(AGrid,AColumns);
  try
    Import(tmp);
  finally
    FinishLocalFormat(tmp);
  end;
end;

// Returns True when AComponent is supported (ie: it can be imported)
class function TDataImport.CanImport(const AComponent: TComponent):Boolean;
begin
  result:=(AComponent is TChartSeries) or
          (AComponent is TCustomChart) or
          (AComponent is TCustomListBox) or
          (AComponent is {$IFDEF FMX}TMemo{$ELSE}TCustomMemo{$ENDIF}) or
          (AComponent is TCustomEdit) or

          {$IFDEF FMX}
          (AComponent is TCustomComboBox) or
          {$ELSE}
          {$IFDEF LCL}
          (AComponent is TCustomComboBox) or
          {$ELSE}
          (AComponent is {$IFNDEF D6}TCustomComboBox{$ELSE}TCustomCombo{$ENDIF}) or
          {$ENDIF}
          {$ENDIF}

          (AComponent is TImage) or
          (AComponent is TStringGrid) or
          {$IFDEF FMX}
          (AComponent is TImageControl) or
          (
            (AComponent is TColumn) and
            {$IFDEF D24}
            (TColumnAccess(AComponent).Model.Owner<>nil) and
            (TColumnAccess(AComponent).Model.Owner is TStringGrid)
            {$ELSE}
            (TColumnAccess(AComponent).Grid<>nil) and
            (TColumnAccess(AComponent).Grid is TStringGrid)
            {$ENDIF}
          ) or
          (AComponent is TTextControl)
          {$ELSE}
          (AComponent is TControl)
          {$ENDIF}
          ;
end;

procedure TDataImport.Import(const AComponent: TComponent);

  procedure ErrorCannotImport;
  begin
    Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from Component class: '+AComponent.ClassName);
  end;

{$IFDEF FMX}
var Grid : TCustomGrid;
{$ENDIF}
begin
  if not Assigned(Chart) then
     Raise EDataImport.Create('Cannot import. Destination Chart property is not assigned');

  if AComponent is TChartSeries then
     Import(TChartSeries(AComponent))
  else
  if AComponent is TCustomChart then
     Import(TCustomChart(AComponent))
  else
  if AComponent is TCustomListBox then
     Import(TCustomListBox(AComponent).Items)
  else
  if AComponent is {$IFDEF FMX}TMemo{$ELSE}TCustomMemo{$ENDIF} then
     Import({$IFDEF FMX}TMemo{$ELSE}TCustomMemo{$ENDIF}(AComponent).Lines)
  else
  if AComponent is TCustomEdit then
     Import(TCustomEdit(AComponent).Text)
  else

  {$IFDEF FMX}
  if AComponent is TCustomComboBox then
     Import(TCustomComboBox(AComponent).Items)
  else
  {$ELSE}
  {$IFDEF LCL}
  if AComponent is TCustomComboBox then
     Import(TCustomComboBox(AComponent).Items)
  else
  {$ELSE}
  if AComponent is {$IFNDEF D6}TCustomComboBox{$ELSE}TCustomCombo{$ENDIF} then
     Import({$IFNDEF D6}TCustomComboBox{$ELSE}TCustomCombo{$ENDIF}(AComponent).Items)
  else
  {$ENDIF}
  {$ENDIF}

  if AComponent is TImage then
     Import(TImage(AComponent).{$IFDEF FMX}Bitmap{$ELSE}Picture.Graphic{$ENDIF})
  else
  if AComponent is TStringGrid then
     Import(TStringGrid(AComponent),[])
  else
  {$IFDEF FMX}
  if AComponent is TImageControl then
     Import(TImageControl(AComponent).Bitmap)
  else
  if AComponent is TColumn then
  begin
    {$IFDEF D24}
    Grid:=TColumnAccess(AComponent).Model.Owner as TCustomGrid;
    {$ELSE}
    Grid:=TColumnAccess(AComponent).Grid;
    {$ENDIF}

    if Grid is TStringGrid then
       Import(TStringGrid(Grid),[TColumn(AComponent)])
    else
       ErrorCannotImport;
  end
  else
  if AComponent is TTextControl then
     Import(TTextControl(AComponent).Text)
  else
  {$ELSE}
  if AComponent is TControl then
     Import(TControlAccess(AComponent).Text)
  else
  {$ENDIF}
     {$IFDEF HASRTTI}
     Import(TValue.From(AComponent));
     {$ELSE}

     // TListView

     ErrorCannotImport;
     {$ENDIF}
end;

procedure TDataImport.Import(const AObject: TObject);
begin
  if AObject is TDataSource then
     Import((AObject as TDataSource).DataSet)
  else
  if AObject is TDataSet then
     Import(AObject as TDataSet)
  else
  if AObject is TStrings then
     Import(AObject as TStrings)
  else
  if AObject is TComponent then
     Import(AObject as TComponent)
  else
     {$IFDEF HASRTTI}
     Import(TValue.From(AObject));
     {$ELSE}
     Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from Object class: '+AObject.ClassName);
     {$ENDIF}
end;

type
  TDataType=(dtNone, dtText, dtNumber, dtDateTime, dtBoolean, dtPicture, dtBlob);

  TURLStyle=(usNone, usHttp, usHttps, usFtp, usUnknown);

function URLStyle(const APath:String):TURLStyle;
var S : String;
begin
  S:=UpperCase(Trim(APath));

  if Copy(S,1,7)='HTTP://' then
     result:=usHttp
  else
  if Copy(S,1,8)='HTTPS://' then
     result:=usHttps
  else
  if Copy(S,1,6)='FTP://' then
     result:=usFtp
  else
  if Pos('://',S)=0 then
     result:=usNone
  else
     result:=usUnknown;
end;

{$IFDEF HASRTTI}
type
  TRTTIFormat=class(TDataFormat)
  private
    FContext : TRttiContext;
    FData    : TValue;

    FDatas   : TRttiType;

    IMoveNext : TRttiMethod;
    IEnumObject : TObject;
    ICurrent : TRttiProperty;

    function GetRTTIValue(const RecNo:Cardinal; const AField: String;
                          out AValue: Variant): Boolean;
    function MoveNextSuccess(const AMethod:TRttiMethod; const AValue:TObject):Boolean;
  protected
    procedure Analyze; override;
    function GetText(const ARow:Cardinal):String; override;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; override;
    function NextIteration:Boolean; override;
  public
    Constructor Create(const AData:TValue);
    Destructor Destroy; override;
  end;

Constructor TRTTIFormat.Create(const AData:TValue);
begin
  inherited Create;

  FData:=AData;
  FContext:=TRttiContext.Create;
  IEnumObject:=nil;
  IMoveNext:=nil;
end;

Destructor TRTTIFormat.Destroy;
begin
  FContext.Free;
  inherited;
end;

function TRTTIFormat.GetText(const ARow:Cardinal):String;
var tmp : Variant;
begin
  if (TextFieldName<>'') and GetRTTIValue(ARow,TextFieldName,tmp) then
     result:=tmp
  else
     result:='';
end;

function TRTTIFormat.GetRTTIValue(const RecNo:Cardinal; const AField: String;
                                  out AValue: Variant): Boolean;

var tmpField : TRttiField;

  function GetFromTValue(const AValue:TValue): Boolean;
  var tmp : TRttiType;
  begin
    result:=False;

    if AValue.IsObject then
       tmp:=FContext.GetType(AValue.AsObject.ClassInfo)
    else
       tmp:=FContext.GetType(AValue.TypeInfo);

    if Assigned(tmp) then
    begin
      if tmp.IsOrdinal or (tmp is TRttiFloatType) then
         result:=True
      else
      begin
        tmpField:=tmp.GetField(AField);
        result:=True;
      end;
    end;
  end;

  function GetFromArray(const AValue:TValue; out tmpValue:TValue):Boolean;
  begin
    tmpValue:=AValue.GetArrayElement(RecNo);
    result:=GetFromTValue(tmpValue);
  end;

var p : TRttiProperty;
    tmpList : TValue;
    t : TRttiType;
    tmpValue : TValue;
    tmpData : Pointer;
begin
  result:=not FData.IsEmpty;

  if result then
  begin
    result:=False;

    tmpField:=nil;

    if FData.IsArray then
       result:=GetFromArray(FData,tmpValue)
    else
    if FData.IsObject then
    begin
       t:=FContext.GetType(FData.TypeInfo);
       p:=t.GetProperty('List');

       if Assigned(p) then
       begin
         tmpList:=p.GetValue(FData.AsObject);

         if tmpList.IsArray then
            result:=GetFromArray(tmpList,tmpValue);
       end
       else
       if Assigned(IEnumObject) then
       begin
         tmpValue:=ICurrent.GetValue(IEnumObject);
         result:=GetFromTValue(tmpValue);
       end;
    end;

    if result then
       if Assigned(tmpField) then
       begin
         if tmpValue.IsObject then
            tmpData:=tmpValue.AsObject
         else
            tmpData:=tmpValue.GetReferenceToRawData;

         result:=Assigned(tmpData);

         if result then
            AValue:=tmpField.GetValue(tmpData).AsVariant
       end
       else
          AValue:=FData.GetArrayElement(RecNo).AsVariant;
  end;
end;

function TRTTIFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
var tmp : Variant;
begin
  result:=GetRTTIValue(ARow,Headers[ACol],tmp);

  if result then
     Value:=tmp;
end;

function TRTTIFormat.MoveNextSuccess(const AMethod:TRttiMethod; const AValue:TObject):Boolean;
var moveResult : TValue;
begin
  result:=False;

  if Assigned(AMethod) then
  begin
    moveResult:=AMethod.Invoke(AValue,[]);

    if not moveResult.IsEmpty then
       if moveResult.AsType<Boolean> then
          result:=True;
  end;
end;

function TRTTIFormat.NextIteration:Boolean;
begin
  result:=(not Assigned(IMoveNext)) or
           MoveNextSuccess(IMoveNext,IEnumObject);
end;

procedure TRTTIFormat.Analyze;

  function CalcCount:Cardinal;
  var p : TRttiProperty;
      tmpDim : Integer;
  begin
    result:=0;

    if FDatas is TRttiArrayType then
    begin
      tmpDim:=TRttiArrayType(FDatas).DimensionCount;

      if tmpDim>0 then
         result:=TRttiArrayType(FDatas).TotalElementCount div tmpDim;
    end
    else
    if FDatas is TRttiDynamicArrayType then
    begin
      if FData.IsArray then
         result:=FData.GetArrayLength;
    end
    else
    {$IFNDEF D18} // TList is deprecated in XE4 and up
    if FData.IsObject and (FData.AsObject is TList) then
       result:=TList(FData.AsObject).Count
    else
    {$ENDIF}
    begin
      p:=FDatas.GetProperty('Count');

      if Assigned(p) then
         result:=p.GetValue(FData.AsObject).AsOrdinal;
    end;
  end;

  procedure DoAddField(const AName:String; const AType:TDataType);
  var L : Integer;
  begin
    if AType=dtText then
       TrySetTextField(0,AName)
    else
    if AType=dtNumber then
    begin
      L:=SeriesCount;

      NewSeriesIndex(L,AName);

      SetLength(Headers,L+1);
      Headers[L]:=AName;
    end;
  end;

  function GuessDataType:TRttiType;
  var tmp : TObject;
  begin
    result:=nil;

    if not FData.IsEmpty then
    begin
      if FData.IsObject then
      begin
        tmp:=FData.AsObject;

        if tmp is TRttiType then
           result:=TRttiType(tmp)
        else
           result:=FContext.GetType(tmp.ClassInfo);
      end
      else
        result:=FContext.GetType(FData.TypeInfo);
    end;
  end;

  function AutoTypeOf(const AType:TRttiType):TDataType;
  var tmp : String;
  begin
    tmp:=AType.ToString;

    if (tmp='TDateTime') or (tmp='TDate') or (tmp='TTime') then
      result:=dtDateTime
    else
    if tmp='Boolean' then
      result:=dtBoolean
    else
    case AType.TypeKind of
      tkInteger,
      tkFloat,
      tkInt64 : result:=dtNumber;

      tkChar,
      tkString,
      tkWChar,
      tkLString,
      tkWString,
      tkUString : result:=dtText;
    else
      result:=dtNone;
    end;
  end;

  procedure GuessTitle;
  var tmp : String;
  begin
    if FDatas is TRttiArrayType then
       tmp:=TRttiArrayType(FDatas).ElementType.Name
    else
    if FDatas is TRttiDynamicArrayType then
       tmp:=TRttiDynamicArrayType(FDatas).ElementType.Name
    else
       tmp:=FDatas.ToString;

    Title.Caption:=tmp;
  end;

var f : TRttiField;
    p : TRttiProperty;
    t : TRttiType;
    m : TRttiMethod;
    v : TValue;
    tmpEnumerator : TValue;
    tmpCurrentValue : TValue;
begin
  inherited;

  FDatas:=GuessDataType;

  if Assigned(FDatas) then
  begin
    DataQuantity:=CalcCount;

    if FDatas is TRttiArrayType then
       t:=TRttiArrayType(FDatas).ElementType
    else
    if FDatas is TRttiDynamicArrayType then
       t:=TRttiDynamicArrayType(FDatas).ElementType
    else
    begin
      t:=FDatas;
      if FData.IsObject then
      begin
        p:=FDatas.GetProperty('List');

        if Assigned(p) then
        begin
          t:=FContext.GetType(p.GetValue(FData.AsObject).TypeInfo);

          if t is TRttiArrayType then
             t:=TRttiArrayType(p).ElementType
          else
             t:=TRttiDynamicArrayType(t).ElementType;
        end
        else
        begin
          p:=FDatas.GetProperty('Count');

          if Assigned(p) then
          begin
            m:=FDatas.GetMethod('GetEnumerator');

            if Assigned(m) then
            begin
              v:=m.Invoke(FData,[]);

              if not v.IsEmpty then
              begin
                tmpEnumerator:=v;

                t:=FContext.GetType(v.TypeInfo);
                p:=t.GetProperty('Current');

                if Assigned(p) then
                begin
                  m:=t.GetMethod('MoveNext');

                  if MoveNextSuccess(m,v.AsObject) then
                  begin
                    tmpCurrentValue:=p.GetValue(v.AsObject);

                    if not tmpCurrentValue.IsEmpty then
                    begin
                      t:=FContext.GetType(tmpCurrentValue.TypeInfo);

                      ICurrent:=p;
                      IMoveNext:=m;
                      IEnumObject:=tmpEnumerator.AsObject;
                    end;
                  end;
                end;
              end;
            end;
          end;
        end;
      end;
    end;

    if t is TRttiStringType then
       DoAddField(t.Name,dtText)
    else
    if (t is TRttiFloatType) or (t is TRttiOrdinalType) or
       (t is TRttiInt64Type) then
       DoAddField(t.Name,dtNumber)
    else
    if t is TRttiInstanceType then
       for f in t.GetDeclaredFields do
           DoAddField(f.Name,AutoTypeOf(f.FieldType))
    else
    if t is TRttiRecordType then
       for f in t.GetFields do
           DoAddField(f.Name,AutoTypeOf(f.FieldType));

    FirstRow:=0;

    if DataQuantity>0 then
       LastRow:=DataQuantity-1;

    GuessTitle;
  end;
end;

procedure TDataImport.Import(const AData:TValue);
var tmp : TRTTIFormat;
begin
  tmp:=TRTTIFormat.Create(AData);
  try
    Import(tmp);
  finally
    FinishLocalFormat(tmp);
  end;
end;
{$ENDIF}

{$IFDEF HASREST}
procedure TDataImport.Import(const AResponse:TRESTResponse);
begin
  // ImportJSON ?
  Import(AResponse.Content);
end;
{$ENDIF}

{$IFDEF HASJSON}
type
  TJSONFormat=class(TDataFormat)
  private
    JSON : TJSONValue;

    RowObj,
    Obj : TJSONObject;

    FreeJSON : Boolean;

    Field,
    Root : String;

    function ArrayValue(const AArray:TJSONArray; const Index:Integer):TJSONValue;
    function GetItem(const Obj:TJSONObject; const Index:Integer; out IsNull:Boolean; out ATitle:String):String;
    function GetRow(const JSON:TJSONValue; const Index:Integer):TJSONObject;
    function ObjectValue(const AObj:TJSONObject; const Field:String):TJSONValue;
  protected
    procedure Analyze; override;
    function GetText(const ARow:Cardinal):String; override;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; override;
    function ValidRow(const ARow:Cardinal):Boolean; override;
  public
    Constructor Create(const AJSON:TJSONValue; const ARoot,AField:String; const DoFreeJSON:Boolean);
    Destructor Destroy; override;
  end;

Constructor TJSONFormat.Create(const AJSON:TJSONValue; const ARoot,AField:String; const DoFreeJSON:Boolean);
begin
  inherited Create;

  JSON:=AJSON;
  Field:=AField;
  Root:=ARoot;

  FreeJSON:=DoFreeJSON;
end;

Destructor TJSONFormat.Destroy;
begin
  if FreeJSON then
     JSON.Free;
     
  inherited;
end;

function TJSONFormat.ObjectValue(const AObj:TJSONObject; const Field:String):TJSONValue;
{$IFDEF D14}
{$IFNDEF D20}
var t : Integer;
{$ENDIF}
{$ENDIF}
begin
  {$IFDEF D14}
  {$IFDEF D20}
  result:=AObj.Values[Field];
  {$ELSE}

  for t:=0 to AObj.Size-1 do
      if CompareText(AObj.Get(t).JsonString.Value,Field)=0 then
      begin
        result:=AObj.Get(t).JsonValue;
        Exit;
      end;

  result:=nil;

  {$ENDIF}
  {$ELSE}
  result:=AObj.Field[Field];
  {$ENDIF}
end;

procedure TJSONFormat.Analyze;

  function GuessJSON(out JSON:TJSONValue):TJSONObject;

    function GuessQuantity(const AArray:TJSONArray):TJSONValue;
    begin
      DataQuantity:=AArray.{$IFDEF D14}{$IFNDEF D20}Size{$ELSE}Count{$ENDIF}{$ELSE}Count{$ENDIF};

      if DataQuantity>0 then
         result:=ArrayValue(AArray,0)
      else
         result:=nil;
    end;

    procedure GuessSeries(const JSON:TJSONValue; const Obj:TJSONObject);
    var tmp : TJSONObject;
        t   : Integer;
        tmpTitle,
        S   : String;
        IsNull : Boolean;
    begin
      tmp:=GetRow(JSON,0);

      if Assigned(tmp) then
      begin
        for t:=0 to tmp.{$IFDEF D14}{$IFNDEF D20}Size{$ELSE}Count{$ENDIF}{$ELSE}Count{$ENDIF}-1 do
        begin
          S:=GetItem(Obj,t,IsNull,tmpTitle);

          if not IsNull then
             GuessValue(S,t,Obj.{$IFDEF D14}{$IFDEF D20}Pairs[t]{$ELSE}Get(t){$ENDIF}.JsonString.Value{$ELSE}NameOf[t]{$ENDIF});
        end;
      end;
    end;

  var tmp,
      tmpArray : TJSONValue;
      Obj : TJSONObject;
      t   : Integer;
  begin
    result:=nil;

    if Root<>'' then
    begin
      {$IFDEF D14}
      {$IFDEF D20}

      if JSON.TryGetValue<TJSONValue>(Root, tmp) then

      {$ELSE}

      if JSON is TJSONObject then
         tmp:=ObjectValue(TJSONObject(JSON),Root)
      else
         tmp:=nil;

      if Assigned(tmp) then

      {$ENDIF}
      {$ELSE}
      tmp:=JSON.Field[Root];

      if Assigned(tmp) then
      {$ENDIF}
      begin
        {$IFDEF D14}
        tmp.Owned:=False;
        {$ENDIF}

        //JSON.Free;
        JSON:=tmp;
      end;
    end;

    if JSON is TJSONArray then
       tmp:=GuessQuantity(TJSONArray(JSON))
    else
    begin
      // Pending: Investigate JSON pairs, try to find a list/array inside them.
      // Choose the bigger list (the one with more elements) as "Root"

      DataQuantity:=1;

      tmp:=JSON;

      if tmp is TJSONObject then
      begin
        Obj:=TJSONObject(tmp);

        for t:=0 to Obj.{$IFDEF D14}{$IFNDEF D20}Size{$ELSE}Count{$ENDIF}{$ELSE}Count{$ENDIF}-1 do
        begin
          tmpArray:=Obj.{$IFDEF D14}{$IFDEF D20}Pairs[t]{$ELSE}Get(t){$ENDIF}.JsonValue{$ELSE}FieldByIndex[t]{$ENDIF};

          if tmpArray is TJSONArray then
          begin
            tmp:=GuessQuantity(TJSONArray(tmpArray));

            if Assigned(tmp) then
            begin
              JSON:=tmpArray;
              break;
            end;
          end;
        end;
      end;
    end;

    if DataQuantity>0 then
       if tmp is TJSONObject then
       begin
         if Field<>'' then
            tmp:=ObjectValue(TJSONObject(tmp),Field);

         if tmp is TJSONObject then
         begin
           result:=TJSONObject(tmp);
           GuessSeries(tmp,result);
         end;
       end;
  end;

begin
  inherited;

  Obj:=GuessJSON(JSON);
  FirstRow:=0;

  if DataQuantity>0 then
     LastRow:=DataQuantity-1;
end;

function TJSONFormat.ArrayValue(const AArray:TJSONArray; const Index:Integer):TJSONValue;
begin
  result:=AArray.{$IFDEF D14}{$IFDEF D20}Items[Index]{$ELSE}Get(Index){$ENDIF}{$ELSE}Child[Index]{$ENDIF};
end;

function TJSONFormat.GetRow(const JSON:TJSONValue; const Index:Integer):TJSONObject;
var tmp : TJSONValue;
begin
  result:=nil;

  if JSON is TJSONArray then
     tmp:=ArrayValue(TJSONArray(JSON),Index)
  else
     tmp:=JSON;

  if tmp is TJSONObject then
  begin
    if Field<>'' then
       tmp:=ObjectValue(TJSONObject(tmp),Field);

    if tmp is TJSONObject then
       result:=TJSONObject(tmp);
  end;
end;

function TJSONFormat.ValidRow(const ARow:Cardinal):Boolean;
begin
  RowObj:=GetRow(JSON,ARow);
  result:=True; // Assigned(Obj);
end;

function TJSONFormat.GetItem(const Obj:TJSONObject; const Index:Integer; out IsNull:Boolean; out ATitle:String):String;
{$IFDEF D14}
var Value : TJSONValue;
    Pair : TJSONPair;
{$ELSE}
var Value : Variant;
    tmp : TJSONValue;
{$ENDIF}
begin
  {$IFDEF D14}
  Pair:=Obj.{$IFDEF D20}Pairs[Index]{$ELSE}Get(Index){$ENDIF};

  if Assigned(Pair) then
  begin
    ATitle:=Pair.JsonString.Value;
    Value:=Pair.JSONValue;
    IsNull:=Value.Null;

    if IsNull then
       result:=''
    else
       result:=Value.Value;
  end
  else
  begin
    IsNull:=True;
    ATitle:='';
    result:='';
  end;

  {$ELSE}
  tmp:=Obj.FieldByIndex[Index];

  ATitle:=tmp.SelfTypeName;
  Value:=tmp.Value;
  IsNull:=VarIsNull(Value);

  if IsNull then
     result:=''
  else
     result:=Value;
  {$ENDIF}
end;

function TJSONFormat.GetText(const ARow:Cardinal):String;
var IsNull : Boolean;
    tmp : String;
begin
  if TextField=-1 then
     result:=''
  else
     result:=GetItem(RowObj,TextField,IsNull,tmp);
end;

function TJSONFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
var S,
    tmp : String;
    IsNull : Boolean;
begin
  S:=GetItem(RowObj,SeriesIndex[ACol],IsNull,tmp);
  result:=(not IsNull) and TryStrToFloat(S,Value {$IFDEF D7},ISettings{$ENDIF});
end;

procedure TDataImport.ImportJSON(const AStrings:TStrings; const Root:String='';
                                 const Field:String='');
begin
  ImportJSON(AStrings.Text,Root,Field);
end;

procedure TDataImport.ImportJSON(const AText:String; const Root:String='';
                                 const Field:String='');

  {$IFDEF D14}
  {$IFNDEF D15}
  function CleanJSON(const AText:String):String;
  var t,
      Offset : Integer;
      C : Char;
  begin
    result:='';

    if ZBS then
       Offset:=-1
    else
       Offset:=0;

    for t:=FirstStringChar to Length(AText)-Offset do
    begin
      C:=AText[t];

      if (C<>#13) and (C<>#10) and (C<>' ') then
         result:=result+C;
    end;
  end;
  {$ENDIF}
  {$ENDIF}

var JSON : TJSONValue;
begin
  {$IFDEF D14}
    {$IFDEF D15}
    JSON:=TJSONObject.ParseJSONValue(AText);
    {$ELSE}
    JSON:=TJSONObject.ParseJSONValue(TEncoding.ASCII.GetBytes(CleanJSON(AText)),0);
    {$ENDIF}
  {$ELSE}
  JSON:=TlkJSON.ParseText(AText);
  {$ENDIF}

  if Assigned(JSON) then
  try
    Import(JSON,Root,Field);
  finally
    if not AnalyzeMode then
       JSON.Free;
  end
  else
    Raise EDataImport.Create('Error parsing JSON content');
end;

procedure TDataImport.Import(const AJSON:TJSONValue; const Root:String='';
                             const Field:String='');
var tmp : TJSONFormat;
begin
  tmp:=TJSONFormat.Create(AJSON,Root,Field,AnalyzeMode);
  try
    Import(tmp);
  finally
    FinishLocalFormat(tmp);
  end;
end;
{$ENDIF}

type
  TCSVFormat=class(TDataFormat)
  private
    S : String;

    FStrings : TStrings;

    Separator : String;

    FreeStrings : Boolean;

    function GetTextField(const AIndex:Integer):String;
  protected
    procedure Analyze; override;
    function GetText(const ARow:Cardinal):String; override;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; override;
    function ValidRow(const ARow:Cardinal):Boolean; override;
  public
    Constructor Create(const AStrings:TStrings; const DoFreeStrings:Boolean=False);
    Destructor Destroy; override;
  end;

Constructor TCSVFormat.Create(const AStrings:TStrings; const DoFreeStrings:Boolean=False);
begin
  inherited Create;

  FreeStrings:=DoFreeStrings;
  FStrings:=AStrings;
end;

Destructor TCSVFormat.Destroy;
begin
  if FreeStrings then
     FStrings.Free;

  inherited;
end;

// Returns the AIndex'th field in string "S", trimming and removing quotes.
function TCSVFormat.GetTextField(const AIndex:Integer):String;
begin
  result:=RemoveQuotes(Trim(TeeExtractField(S,AIndex,Separator)));
end;

function TCSVFormat.GetText(const ARow:Cardinal):String;
begin
  if TextField=-1 then
     result:=''
  else
     result:=GetTextField(TextField);
end;

function TCSVFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
begin
  result:=TryStrToFloat(TeeExtractField(S,SeriesIndex[ACol],Separator),Value {$IFDEF D7},ISettings{$ENDIF});
end;

function TCSVFormat.ValidRow(const ARow:Cardinal):Boolean;
begin
  S:=Trim(FStrings[ARow]);
  result:=S<>'';
end;

procedure TCSVFormat.Analyze;

  function CountValues(const S:String):Integer;

    function CountWith(const ASeparator:String):Integer;
    var t : Integer;
    begin
      result:=0;

      SeriesIndex:=nil;
      SeriesTitles:=nil;

      TextField:=-1;
      Separator:=ASeparator;

      for t:=1 to TeeNumFields(S,ASeparator) do
          if GuessValue(TeeExtractField(S,t,ASeparator),t,'') then
             Inc(result);
    end;

  begin
    if Pos(',',S)>0 then
       result:=CountWith(',')
    else
    if Pos(';',S)>0 then
       result:=CountWith(';')
    else
    if Pos('|',S)>0 then
       result:=CountWith('|')
    else
    if Pos(#9,S)>0 then
       result:=CountWith(#9)
    else
    if Pos(' ',S)>0 then
       result:=CountWith(' ')
    else
    if S<>'' then
       result:=CountWith(' ')
    else
       result:=0;
  end;

  procedure GuessTitles;
  var MinHeaderFields : Integer;
      t : Integer;
  begin
    Title.Text.Clear;

    if HeaderLines>0 then
    begin
      MinHeaderFields:=SeriesCount;

      if TextField<>-1 then
         Inc(MinHeaderFields);

      for t:=0 to HeaderLines-1 do
      begin
        S:=Headers[t];

        if TeeNumFields(S,Separator)>=MinHeaderFields then
        begin
          if TextField<>-1 then
             TextFieldName:=GetTextField(TextField);

          break;
        end
        else
          Title.Text.Add(S);

      end;
    end;
  end;

var t,
    tmpCount : Integer;
    S : String;
begin
  inherited;

  for t:=0 to Min(10,FStrings.Count-1) do
  begin
    S:=FStrings[t];

    if CountValues(S)=0 then
    begin
      Inc(HeaderLines);
      SetLength(Headers,HeaderLines);
      Headers[HeaderLines-1]:=S;
    end
    else
      break;
  end;

  if HeaderLines>0 then
  begin
    S:=Headers[HeaderLines-1];

    if UpperCase(Trim(S))='XYZ' then
       ActualStyle:=isGrid;

    if ActualStyle<>isGrid then
    begin
      t:=1;
      tmpCount:=0;

      while t<=TeeNumFields(S,Separator) do
      begin
        if (TextField=-1) or (t<>TextField) then
        begin
          if tmpCount<Length(SeriesTitles) then
          begin
            SeriesTitles[tmpCount]:=Trim(TeeExtractField(S,t,Separator));
            Inc(tmpCount);
          end;
        end;

        Inc(t);
      end;
    end;
  end;

  DataQuantity:=Max(0,FStrings.Count-HeaderLines);

  FirstRow:=HeaderLines;

  if FStrings.Count>0 then
     LastRow:=FStrings.Count-1;

  GuessTitles;
end;

{$IFDEF HASXML}
type
  {$IFDEF FPC}
  IXMLNode=TDOMNode;
  IXMLDocument=TXMLDocument;
  IXMLNodeList=TDOMNodeList;
  {$ENDIF}

  IXMLNodeArray=Array of IXMLNode;

  THTMLFormat=class(TCSVFormat)
  private
    FTables : IXMLNodeArray;
    FTableID : String;
  protected
    procedure ChangedOption(Sender:TObject); override;
    function Options:TStrings; override;
    procedure PrepareOption(const AComponent:TComponent; const AIndex:Integer); override;
  end;

  TXMLFormat=class(TDataFormat)
  private
    Doc : TXMLDocument;

    FXML : IXMLDocument;

    FreeXML : Boolean;

    ArrayNode : IXMLNode;
    IRowNode : IXMLNode;

  protected
    procedure Analyze; override;
    function GetText(const ARow:Cardinal):String; override;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; override;
    function ValidRow(const ARow:Cardinal):Boolean; override;
  public
    Constructor Create(const ADoc:TXMLDocument; const AXML:IXMLDocument; const DoFreeXML:Boolean);
    Destructor Destroy; override;
  end;

// Case-insensitive (vs. ANode.Attributes[])
function FindAttributeValue(const ANode:IXMLNode; const AValue:String):String;
var t : Integer;
    tmp : IXMLNode;
begin
  for t:=0 to ANode.{$IFDEF FPC}Attributes.Length{$ELSE}AttributeNodes.Count{$ENDIF}-1 do
  begin
    tmp:=ANode.{$IFDEF FPC}Attributes{$ELSE}AttributeNodes{$ENDIF}[t];

    if SameText(tmp.NodeName,AValue) then
    begin
      result:=tmp.NodeValue;
      Exit;
    end;
  end;

  result:='';
end;

{ THTMLFormat  }

function TableToCSV(const ANode:IXMLNode):TStrings;

  function RowOf(ANode:IXMLNode):String;
  var t : Integer;
      tmp : IXMLNode;
  begin
    result:='';

    for t:=0 to ANode.ChildNodes.Count-1 do
    begin
      tmp:=ANode.ChildNodes[t];

      if SameText(tmp.NodeName,'td') or SameText(tmp.NodeName,'th') then
      begin
        if result<>'' then
           result:=result+#9;

        result:=result+tmp.NodeValue;
      end;
    end;
  end;

  procedure AddRows(const ANode:IXMLNode);
  var t : Integer;
      tmp : IXMLNode;
  begin
    for t:=0 to ANode.ChildNodes.Count-1 do
    begin
      tmp:=ANode.ChildNodes[t];

      if SameText(tmp.NodeName,'tr') then
         result.Add(RowOf(tmp));
    end;
  end;

var t : Integer;
    tmp : IXMLNode;
begin
  result:=TStringList.Create;

  if Assigned(ANode) then
  for t:=0 to ANode.ChildNodes.Count-1 do
  begin
    tmp:=ANode.ChildNodes[t];

    if SameText(tmp.NodeName,'thead') then
       AddRows(tmp)
    else
    if SameText(tmp.NodeName,'tbody') then
       AddRows(tmp)
    else
    if SameText(tmp.NodeName,'tr') then
       result.Add(RowOf(tmp));
  end;
end;

function THTMLFormat.Options:TStrings;
begin
  result:=inherited Options;
  result.Add('&Table:,Combo');
end;

procedure THTMLFormat.ChangedOption(Sender:TObject);
var tmp : TComboBox;
    tmpStrings : TStrings;
begin
  if Sender is TComboBox then
  begin
    tmp:=TComboBox(Sender);

    if tmp.Tag=0 then
    begin
      FTableID:=tmp.Items[tmp.ItemIndex];

      FStrings.Clear;

      tmpStrings:=TableToCSV(FTables[tmp.ItemIndex]);

      if Assigned(tmpStrings) then
         FStrings.AddStrings(tmpStrings);

      Analyze;
    end;
  end;
end;

procedure THTMLFormat.PrepareOption(const AComponent:TComponent; const AIndex:Integer);
var t : Integer;
    Combo : TComboBox;
    tmp : String;
begin
  inherited;

  if AIndex=0 then
     if AComponent is TComboBox then
     begin
       AComponent.Tag:=AIndex;
       Combo:=TComboBox(AComponent);

       Combo.Items.BeginUpdate;
       try
         for t:=Low(FTables) to High(FTables) do
         begin
           tmp:=FindAttributeValue(FTables[t],'id');

           if tmp='' then
              tmp:=IntToStr(t);

           Combo.Items.Add(tmp);
         end;
       finally
         Combo.Items.EndUpdate;
       end;

       Combo.OnChange:=ChangedOption;
     end;
end;

{ TXMLFormat }

Constructor TXMLFormat.Create(const ADoc:TXMLDocument; const AXML:IXMLDocument; const DoFreeXML:Boolean);
begin
  inherited Create;

  Doc:=ADoc;
  FXML:=AXML;

  FreeXML:=DoFreeXML;
end;

Destructor TXMLFormat.Destroy;
begin
  if FreeXML then
     Doc.Free;

  inherited;
end;

function TXMLFormat.GetText(const ARow:Cardinal):String;
var tmp : IXMLNodeList;
begin
  if TextField=-1 then
     result:=''
  else
  begin
    tmp:=IRowNode.ChildNodes;

    if tmp.{$IFDEF FPC}Length{$ELSE}Count{$ENDIF}>TextField then
       result:=tmp[TextField].NodeValue
    else
       result:='';
  end;
end;

function TXMLFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
var tmp : Integer;
begin
  tmp:=SeriesIndex[ACol];

  result:=(IRowNode.ChildNodes.Count>tmp) and TryStrToFloat(IRowNode.ChildNodes[tmp].NodeValue,Value {$IFDEF D7},ISettings{$ENDIF});
end;

function TXMLFormat.ValidRow(const ARow:Cardinal):Boolean;
begin
  IRowNode:=ArrayNode.ChildNodes[ARow];
  result:=True;
end;

procedure TXMLFormat.Analyze;
var
  Traverse : Boolean;

  {$IFDEF FPC}
  function IsTextElement(const ANode:IXMLNode): Boolean;
  begin
    Result := (ANode.NodeType = ELEMENT_NODE) and (ANode.ChildNodes.Length = 1) and
              (ANode.ChildNodes[0].NodeType = TEXT_NODE);
  end;
  {$ENDIF}

  procedure GuessFields(const ANode:IXMLNode);
  var t,
      tmpCount : Integer;
      tmp : IXMLNode;
      NodeName,
      Value : String;
  begin
    for t:=0 to ANode.ChildNodes.Count-1 do
    begin
      tmp:=ANode.ChildNodes[t];

      NodeName:=tmp.NodeName;

      if {$IFDEF FPC}IsTextElement(tmp){$ELSE}tmp.IsTextElement{$ENDIF} then
      begin
        Value:=tmp.NodeValue;

        if Traverse then
        begin
          Traverse:=False;

          ArrayNode:=ANode.ParentNode;

          DataQuantity:=ANode.ParentNode.ChildNodes.Count;
        end;

        if GuessValue(Value,t,ANode.NodeName) then
        begin
          tmpCount:=Length(SeriesIndex);
          SetLength(Headers,tmpCount);
          Headers[tmpCount-1]:=NodeName;
        end;
      end
      else
      if Traverse then
         GuessFields(tmp);
    end;
  end;

var List : IXMLNodeList;
    tmpNode : IXMLNode;
    t : Integer;
    NodeName : String;
begin
  inherited;

  List:=FXML.ChildNodes;

  if List.Count>0 then
  begin
    for t:=0 to List.Count-1 do
    begin
      tmpNode:=List.{$IFDEF FPC}Item{$ELSE}Nodes{$ENDIF}[t];

      if UpperCase(tmpNode.NodeName)<>'XML' then
      begin
        NodeName:=tmpNode.NodeName;

        if (tmpNode.ChildNodes.Count=1) and
           (UpperCase(tmpNode.ChildNodes.{$IFDEF FPC}Item{$ELSE}Nodes{$ENDIF}[0].NodeName)='#TEXT') then
           TrySetTextField(0,tmpNode.NodeName)
        else
        begin
          Title.Caption:=NodeName;

          Traverse:=True;
          GuessFields(tmpNode);
        end;
      end;
    end;

    FirstRow:=0;

    if DataQuantity>0 then
       LastRow:=DataQuantity-1;
  end;
end;

procedure TDataImport.Import(const AXML:TXMLDocument);
var IXML : IXMLDocument;
    tmp  : TXMLFormat;
begin
  {$IFDEF FPC}
  IXML:=AXML;
  {$ELSE}
  if AXML.GetInterface(IXMLDocument, IXML) then
  {$ENDIF}
  begin
    tmp:=TXMLFormat.Create(AXML,IXML,AnalyzeMode);
    try
      Import(tmp);
    finally
      FinishLocalFormat(tmp);
    end
  end
  {$IFNDEF FPC}
  else
     Raise EDataImport.Create('Cannot import from XML document. Interface IXMLDocument not found.');
  {$ENDIF}
end;

{$IFNDEF FPC}
procedure TDataImport.Import(const AXML:IXMLDocument);
var tmp : TXMLFormat;
begin
  tmp:=TXMLFormat.Create(nil,AXML,False);
  try
    Import(tmp);
  finally
    FinishLocalFormat(tmp);
  end;
end;
{$ENDIF}

procedure TDataImport.ImportXML(const AStrings:TStrings);
begin
  ImportXML(AStrings.Text);
end;

procedure TDataImport.ImportXML(const AXML:String);
var XML : TXMLDocument;
    {$IFDEF FPC}
    S : TStringStream;
    {$ENDIF}
begin
  {$IFDEF FPC}
  S := TStringStream.Create(AXML);
  try
    ReadXMLFile(XML, S);
    try
      Import(XML);
    finally
      if not AnalyzeMode then
         XML.Free;
    end;
  finally
    S.Free;
  end;

  {$ELSE}

  XML:=TXMLDocument.Create(Self);
  try
    // DOMVendor
    // TParseOptions [] (exclude: poValidateOnParse) ?

    XML.LoadFromXML(AXML);
    Import(XML);
  finally
    if not AnalyzeMode then
       XML.Free;
  end;
  {$ENDIF}
end;

procedure TDataImport.ImportHTML(const AHTML:String; const ATableID:String='');
var
  XML : TXMLDocument;

  function GetTables:IXMLNodeArray;

    procedure SearchIn(const AList:IXMLNodeList);
    var t,L : Integer;
        tmp : IXMLNode;
    begin
      for t:=0 to AList.Count-1 do
      begin
        tmp:=AList[t];

        if tmp.NodeName='table' then
        begin
          L:=Length(result);
          SetLength(result,L+1);
          result[L]:=tmp;
        end
        else
          SearchIn(tmp.ChildNodes);
      end;
    end;

  begin
    result:=nil;
    SearchIn(XML.ChildNodes);
  end;

  function FindTable(const ATableID:String):IXMLNode;

    procedure SearchIn(const AList:IXMLNodeList);

      function IsTable(const ANode:IXMLNode):Boolean;
      begin
        result:=SameText(ANode.NodeName,'table') and
                SameText(FindAttributeValue(ANode,'id'),ATableID);
      end;

    var t : Integer;
        tmp : IXMLNode;
    begin
      for t:=0 to AList.Count-1 do
      begin
        tmp:=AList[t];

        if IsTable(tmp) then
        begin
          result:=tmp;
          break;
        end
        else
        begin
          SearchIn(tmp.ChildNodes);

          if Assigned(result) then
             break;
        end;
      end;
    end;

  begin
    result:=nil;
    SearchIn(XML.ChildNodes);
  end;

  function SearchTable(out ATables:IXMLNodeArray):TStrings;
  var t : Integer;
  begin
    if ATableID='' then
    begin
      result:=nil;

      ATables:=GetTables;

      for t:=Low(ATables) to High(ATables) do
      begin
        result:=TableToCSV(ATables[t]);

        if Assigned(result) then
           if result.Count>0 then
              break
           else
              result.Free;
      end;
    end
    else
      result:=TableToCSV(FindTable(ATableID));
  end;

var Strings : TStrings;
    tmpFormat : THTMLFormat;
    tmpTables : IXMLNodeArray;
    {$IFDEF FPC}
    S : TStringStream;
    {$ENDIF}
begin
  {$IFDEF FPC}
  S := TStringStream.Create(AHTML);
  try
    ReadXMLFile(XML, S);
  finally
    S.Free;
  end;
  {$ELSE}
  XML:=TXMLDocument.Create(Self);
  try
    //XML.ParseOptions:=XML.ParseOptions-[poValidateOnParse];
    XML.LoadFromXML(AHTML);
  finally
    XML.Free;
  end;
  {$ENDIF}

  Strings:=SearchTable(tmpTables);

  if Assigned(Strings) then
  try
    tmpFormat:=THTMLFormat.Create(Strings);
    try
      tmpFormat.FTables:=tmpTables;
      tmpFormat.FTableID:=ATableID;

      Import(tmpFormat);
    finally
      FinishLocalFormat(tmpFormat);
    end;
  finally
    if not AnalyzeMode then
       Strings.Free;
  end;
end;
{$ENDIF}

{$IFDEF HASEXCEL}
function IsExcelFile(const AExtension:String):Boolean;
begin
  result:=(AExtension='.XLSX') or (AExtension='.XLS');
end;

const
  ExcelApp='Excel.Application';

function IsExcel_Installed:Boolean;
var tmp : String;
    ID  : TCLSID;
begin
  tmp:=ExcelApp;
  result:=CLSIDFromProgID(PWideChar(WideString(tmp)),ID)=S_OK;
end;

// Example: ARange = 'A1:H100'
procedure TDataImport.ImportExcelFile(const APath:String; const AWorkSheet:String='';
                                      const ARange:String='');
var Excel : Variant;

  function OpenExcel(const AName:String):Boolean;
  begin
    try
      Excel:=GetActiveOleObject(ExcelApp);
    except
      try
        Excel:=CreateOleObject(ExcelApp);
      except
      on E:Exception do
         Raise EDataImport.Create(TeeMsg_ExcelNotInstalled+#13#13+E.Message);
      end;
    end;

    Excel.Workbooks.Open(AName);
    result:=True;
  end;

  function ParseRange(const ARange:String):TRect;

    procedure ParseColumnRow(const S:String; out Column,Row:Integer);
    //var tmp : String;
    begin
      Column:=0;
      Row:=0;

      // Pending: Parse, ie. C123 and return: Column=3 and Row=123
    end;

  var i : Integer;
  begin
    result:=Rect(0,0,0,0);

    i:=Pos(':',ARange);

    if i>0 then
    begin
      ParseColumnRow(Copy(ARange,FirstStringChar,i),result.Left,result.Top);
      ParseColumnRow(Copy(ARange,i+1,Length(ARange)),result.Right,result.Bottom);
    end;
  end;

  function RangeToStrings(const ASheet:Variant; const ARange:TRect):TStrings;
  var x,y : Integer;
      S,
      Value : String;
  begin
    result:=TStringList.Create;
    result.BeginUpdate;
    try
      for y:=ARange.Top to ARange.Bottom do
      begin
        S:='';

        for x:=ARange.Left to ARange.Right do
        begin
          Value:=ASheet.Cells.Item[y,x].Value;

          if S='' then
             S:=Value
          else
             S:=S+#9+Value;
        end;

        result.Add(S);
      end;
    finally
      result.EndUpdate;
    end;
  end;

  function ParseExcel:TStrings;
  var t : Integer;
      Sheet : Variant;
      Range : Variant;

      R : TRect;
  begin
    result:=nil;

    for t:=1 to Excel.WorkSheets.Count do
    begin
      Sheet:=Excel.WorkSheets[t];

      if not VarIsNull(Sheet) then
      if (AWorkSheet='') or SameText(AWorkSheet,Sheet.Name) then
      begin
        if ARange='' then
        begin
          Range:=Sheet.UsedRange;

          if VarIsNull(Range) then
             R:=Rect(0,0,0,0)
          else
          begin
            R.Left:=Range.Column;
            R.Top:=Range.Row;
            R.Right:=R.Left+Range.Columns.Count;
            R.Bottom:=R.Top+Range.Rows.Count;
          end;
        end
        else
          R:=ParseRange(ARange);

        if (R.Right>0) and (R.Left>0) and (R.Top>0) and (R.Bottom>0) then
        begin
          if (R.Bottom>=R.Top) and (R.Right>=R.Left) then
          begin
            result:=RangeToStrings(Sheet,R);
            break;
          end;
        end;
      end;
    end;

    Excel.Workbooks.Close;
    //ExcelApp.DisplayAlerts:=False;
    Excel.Quit;
  end;

var Strings : TStrings;
begin
  if IsExcel_Installed then
  begin
    if OpenExcel(APath) then
    begin
      Strings:=ParseExcel;

      if Assigned(Strings) then
      try
        Import(Strings);
      finally
        if not AnalyzeMode then
           Strings.Free;
      end;
    end
    else
      Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from Excel file. Cannot open file: '+APath)
  end
  else
     Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from Excel file. Excel not installed: '+APath)
end;
{$ENDIF}

{$IFDEF FPC}
type
  TClientDataSet=TBufDataset;
{$ENDIF}

procedure TDataImport.ImportFromFile(const APath: String);

  procedure ImportFromClientDataset;
  var Dataset : TClientDataSet;
  begin
    Dataset:=TClientDataSet.Create(nil);
    try
      Dataset.LoadFromFile(APath);

      if AnalyzeMode then
         LocalImport(Dataset,True)
      else
         Import(Dataset);
    finally
      if not AnalyzeMode then
         Dataset.Free;
    end;
  end;

  function CorrectName(const S:String):String;
  const
    AllowedChars = ['a'..'z','A'..'Z','0'..'9','_'];

  var t : Integer;
  begin
    result:=S;

    for t:=1 to Length(S) do
        {$IFDEF NEXTGEN}
        if not S.Chars[t].IsLetterOrDigit then
        {$ELSE}
        if not {$IFDEF D12}CharInSet(S[t],AllowedChars){$ELSE}(S[t] in AllowedChars){$ENDIF} then
        {$ENDIF}
           result[t]:='_';
  end;

  function IsGraphicFile(const Extension:String):Boolean;
  begin
    result:=(Extension='.PNG') or
            (Extension='.JPG') or
            (Extension='.JPEG') or
            (Extension='.BMP') or
            (Extension='.GIF');
  end;

  procedure ImportURLFile;
  var IniFile : TIniFile;
      URL     : String;
  begin
    IniFile:=TIniFile.Create(APath);
    try
      URL:=IniFile.ReadString('InternetShortcut','URL','');

      if URL='' then
         Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from malformed URL file: '+APath)
      else
         ImportFromURL(URL);
    finally
      IniFile.Free;
    end;
  end;

  {$IFDEF HASXML}
  procedure ImportFromXML;
  var XML : TXMLDocument;
  begin
    {$IFDEF FPC}
    ReadXMLFile(XML, APath);
    try
    {$ELSE}
    XML:=TXMLDocument.Create(Self);
    try
      XML.LoadFromFile(APath);
    {$ENDIF}
      Import(XML);
    finally
      if not AnalyzeMode then
         XML.Free;
    end;
  end;
  {$ENDIF}

  procedure ImportImage;
  var Picture : {$IFDEF FMX}TBitmap{$ELSE}TPicture{$ENDIF};
  begin
    {$IFDEF FMX}
    Picture:=TBitmap.CreateFromFile(APath);
    try
      Import(Picture);
    finally
      Picture.Free;
    end;
    {$ELSE}
    Picture:=TPicture.Create;
    try
      Picture.LoadFromFile(APath);
      Import(Picture.Graphic);
    finally
      Picture.Free;
    end;
    {$ENDIF}
  end;

  {$IFDEF HASZIP}
  procedure ImportZIP;
  var z : TZipFile;
      t : Integer;
      tmpTemp : TFileName;
  begin
    if TZipFile.IsValid(APath) then
    begin
      z:=TZipFile.Create;
      try
        z.Open(APath, zmRead);
        try
          for t:=0 to z.FileCount-1 do
          begin
            tmpTemp:={$IFDEF FMX}System.IOUtils.TPath.GetTempPath{$ELSE}TTeeExportFormBase.GetTempFolder{$ENDIF};

            z.Extract(t,tmpTemp,False);
            try
              ImportFromFile({$IFDEF FMX}System.IOUtils.TPath.Combine(tmpTemp,z.FileName[t]){$ELSE}tmpTemp+'\'+z.FileName[t]{$ENDIF});

              // Stop at first file?
              break;
            finally
              DeleteFile(tmpTemp);
            end;
          end;
        finally
          z.Close;
        end;
      finally
        z.Free;
      end;
    end
    else
       Raise EDataImport.Create('Zip file format not supported: '+APath);
  end;
  {$ENDIF}

  {$IFDEF HASBDE}
  procedure ImportFromTable;
  var tmp : TTable;
  begin
    tmp:=TTable.Create(nil);
    try
      tmp.TableName:=APath;
      tmp.Open;

      if AnalyzeMode then
         LocalImport(tmp,True)
      else
         Import(tmp);
    finally
      if not AnalyzeMode then
         tmp.Free;
    end;
  end;
  {$ENDIF}

var Extension : String;
    Strings : TStrings;
begin
  if FileExists(APath) then
  begin
    Extension:=UpperCase(ExtractFileExt(APath));

    if IsGraphicFile(Extension) then
       ImportImage
    else
    if (Extension='.CSV') or (Extension='.TXT') then
    begin
      Strings:=TStringList.Create;
      try
        Strings.LoadFromFile(APath);

        if AnalyzeMode then
           LocalImport(Strings,True)
        else
           Import(Strings);
      finally
        if not AnalyzeMode then
           Strings.Free;
      end;
    end
    else
    if Extension='.URL' then
       ImportURLFile
    else
    if Extension='.CDS' then
       ImportFromClientDataset

    {$IFDEF HASXML}
    else
    if (Extension='.HTM') or (Extension='.HTML') then
       ImportHTML(TextFromFile(APath))
    {$ENDIF}

    {$IFDEF HASBDE}
    else
    if (Extension='.DBF') or (Extension='.DB') then
       ImportFromTable
    {$ENDIF}

    {$IFDEF HASZIP}
    else
    if Extension='.ZIP' then
       ImportZIP
    {$ENDIF}

    {$IFDEF HASXML}
    else
    if Extension='.XML' then
       ImportFromXML
    {$ENDIF}

    {$IFDEF HASEXCEL}
    else
    if IsExcelFile(Extension) then
       ImportExcelFile(APath)
    {$ENDIF}

    {$IFDEF HASJSON}
    else
    if Extension='.JSON' then
       ImportJSON(TextFromFile(APath));
    {$ENDIF}
  end
  else
  if URLStyle(APath)<>usNone then
     ImportFromURL(APath);
end;

procedure TDataImport.Finish(const AFormat:TDataFormat; const AChart:TCustomChart; const ASeries:TChartSeriesList);

  function FindFirstToolClass(const AClass:TTeeCustomToolClass):TTeeCustomTool;
  var t : Integer;
  begin
    for t:=0 to AChart.Tools.Count-1 do
        if AChart.Tools[t] is AClass then
        begin
          result:=AChart.Tools[t];
          Exit;
        end;

    result:=nil;
  end;

  procedure AddSeries(const AGroup:TSeriesGroup);

    // Returns True when all values in AList are ordered (sorted) in ascending order.
    function IsSequential(const AList:TChartValueList):Boolean;
    var t : Integer;
        tmpPrevious : TChartValue;
    begin
      if AList.Count>1 then
      begin
        tmpPrevious:=AList.Value[0];

        for t:=1 to AList.Count-1 do
            if AList.Value[t]<tmpPrevious then
            begin
              result:=False;
              Exit;
            end
            else
              tmpPrevious:=AList.Value[t];
      end;

      result:=True;
    end;

    // Try guessing one series has the "X" values for the others
    procedure TryMergeXY;
    var t,
        tt : Integer;
    begin
      if ASeries.Count>1 then
      begin
        t:=0;

        while t<ASeries.Count do
        begin
          if (ASeries[t].Count>1) and IsSequential(ASeries[t].MandatoryValueList) then
          begin
            for tt:=0 to ASeries.Count-1 do
                if tt<>t then
                   if ASeries[t].Count=ASeries[tt].Count then
                      if IsSequential(ASeries[tt].NotMandatoryValueList) then
                         ASeries[tt].NotMandatoryValueList.Value:=ASeries[t].MandatoryValueList.Value;

            ASeries[t].{$IFDEF AUTOREFCOUNT}DisposeOf{$ELSE}Free{$ENDIF};
            ASeries.Delete(t);
            break;
          end
          else
            Inc(t);
        end;
      end;
    end;

    function TryMergeFinancial:Boolean;

      function Find(const ATitle:String):Integer;
      var t : Integer;
      begin
        for t:=0 to ASeries.Count-1 do
            if SameText(ASeries[t].Title,ATitle) then
            begin
              result:=t;
              Exit;
            end;

        result:=-1;
      end;

    var O,
        //H,L,
        C : Integer;
        Candle : TCandleSeries;
        t : Integer;
    begin
      result:=False;

      if (ASeries.Count=2) and (ASeries[0].Count=ASeries[1].Count) then
      begin
        O:=Find('Open');
        C:=Find('Close');

        if (O<>-1) and (C<>-1) then
        begin
           Candle:=TCandleSeries.Create(AChart.Owner);
           Candle.ManualData:=True;
           Candle.BeginUpdate;

           Candle.CandleStyle:=csOpenClose;

           Candle.OpenValues.Value:=ASeries[O].MandatoryValueList.Value;
           Candle.CloseValues.Value:=ASeries[C].MandatoryValueList.Value;

           Candle.NotMandatoryValueList.Value:=ASeries[O].NotMandatoryValueList.Value;
           Candle.NotMandatoryValueList.Count:=ASeries[O].NotMandatoryValueList.Count;

           SetLength(Candle.HighValues.Value,Candle.Count);
           SetLength(Candle.LowValues.Value,Candle.Count);

           for t:=0 to Candle.Count-1 do
           if Candle.OpenValues.Value[t]>Candle.CloseValues.Value[t] then
           begin
             Candle.HighValues.Value[t]:=Candle.OpenValues.Value[t];
             Candle.LowValues.Value[t]:=Candle.CloseValues.Value[t];
           end
           else
           begin
             Candle.HighValues.Value[t]:=Candle.CloseValues.Value[t];
             Candle.LowValues.Value[t]:=Candle.OpenValues.Value[t];
           end;

           Candle.Labels.Assign(ASeries[O].Labels);

           ASeries[O].{$IFDEF AUTOREFCOUNT}DisposeOf{$ELSE}Free{$ENDIF};
           ASeries[C].{$IFDEF AUTOREFCOUNT}DisposeOf{$ELSE}Free{$ENDIF};

           if O>C then
           begin
             ASeries.Delete(O);
             ASeries.Delete(C);
           end
           else
           begin
             ASeries.Delete(C);
             ASeries.Delete(O);
           end;

           ASeries.Add(Candle);

           if Candle.Count>100 then
              Candle.Pen.Hide;

           result:=True;
        end;
      end;
    end;

  var t : Integer;
      tmp : TChartSeries;
  begin
    for t:=0 to ASeries.Count-1 do
        ASeries[t].EndUpdate;

    if FStyle=isAutomatic then
       if TryMergeFinancial then
          AFormat.ActualStyle:=isFinancial
       else
          TryMergeXY;

    for t:=0 to ASeries.Count-1 do
    begin
      tmp:=ASeries[t];

      if tmp.Visible then
      begin
        tmp.ParentChart:=AGroup.Series.Owner;

        if Assigned(AGroup) then
           AGroup.Add(tmp);

        if tmp is TFastLineSeries then
        begin
          TFastLineSeries(tmp).DrawStyle:=flAll;
          tmp.Pen.Show;
        end
        else
        if tmp is TCustomSeries then
           TCustomSeries(tmp).DrawStyle:=dsAll;
      end;
    end;

    if AFormat.ActualStyle=isGeographic then
    begin
      // Force reset to 2D view
      AChart.View3D:=False;
      AChart.View3DOptions.Orthogonal:=True;
    end
    else
    if (AFormat.ActualStyle=isGrid) or (AFormat.ActualStyle=isXYZ) then
    begin
      if not (ksView3D in FKeep) then
      begin
        AChart.View3D:=True;

        // Setup 3D view
        AChart.View3DOptions.Orthogonal:=False;

        AChart.View3DOptions.Rotation:=360;
        AChart.View3DOptions.Elevation:=360;
        AChart.View3DOptions.Perspective:=70;
        AChart.View3DOptions.Zoom:=80;
        AChart.Chart3DPercent:=100;
      end;

      if not Assigned(FindFirstToolClass(TRotateTool)) then
         AChart.Tools.Add(TRotateTool);
    end;
  end;

  procedure TryAddScrollPager;
  var tmpScroller : TTeeCustomTool;
  begin
    tmpScroller:=FindFirstToolClass(TScrollPagerTool);

    if (AFormat.ActualStyle=isFinancial) and (AChart.SeriesCount>0) and
       (AChart[0].Count>100) then
    begin
      if not Assigned(tmpScroller) then
      begin
        tmpScroller:=TScrollPagerTool.Create(AChart.Owner);
        AChart.Tools.Add(tmpScroller);
      end;

      TScrollPagerTool(tmpScroller).Series:=AChart[0];
    end
    else
      tmpScroller.Free;
  end;

  procedure TryPostDrawImprovements;
  begin
    // AChart.TeeCreateBitmap.Free; <-- Not yet necessary. Warning: Locks XE4 FMX display

    // 1) Vertical Legend, reduce % top position ?
    //AChart.Legend.TopPercent:=0;

    // 2 )Vertical Legend, word-wrap items to reduce legend width ?

    // 3) Horizontal axis, fit more labels if they are Angle=90 ?
    // AChart.Axes.Bottom.Texts.Angle:=90;

    // 4) Chart Title, word-wrap to avoid width overflow ?

    // Tools:

    // 5) Suitable to add a ScrollPager tool?
    TryAddScrollPager;

    // 6) Suitable to add Animations ?
    // SeriesTransition, SeriesTransform, ProgressiveLines, etc...

    // 7) Legend Items Number Format, equalize number of decimals to right-align
    // values, forcing format to #.000 instead of default #.###
  end;

  procedure TryAddToolTips;
  var tmpTips : TMarksTipTool;
  begin
    if not Assigned(FindFirstToolClass(TMarksTipTool)) then
    begin
      tmpTips:=TMarksTipTool.Create(AChart.Owner);
      tmpTips.Style:=smsLabelValue;

      AChart.Tools.Add(tmpTips);
    end;
  end;

var t : Integer;
    Group : TSeriesGroup;
begin
  Group:=AChart.SeriesList.AddGroup('Group '+IntToStr(AChart.SeriesGroups.Count+1));

  AddSeries(Group);

  // Default Axes Min Max
  AChart.Axes.Reset;

  AFormat.SetTitles(AChart,FKeep);

  for t:=0 to AChart.SeriesCount-1 do
      AChart[t].Marks.Style:=smsValue;

  if AChart.SeriesCount=1 then
     AChart[0].ColorEachPoint:=not (AChart[0] is TCustom3DSeries);

  // Hide legend when having a single FastLine series with lots of points
  AChart.Legend.Visible:=(AChart.SeriesCount>1) or
                        ((AChart.SeriesCount>0) and (not (AChart[0] is TFastLineSeries)));

  if AFormat.ActualStyle<>isGeographic then
     GroupSimilarSeries(Group.Series);

  TryAddToolTips;

  TryPostDrawImprovements;
end;

type
  TGuessStrings=(gsCSV, gsJSON, gsXML, gsHTML);

function GuessStrings(const AStrings:TStrings):TGuessStrings;
var Text : String;
    L : Integer;
    C : Char;
begin
  result:=gsCSV;

  Text:=Trim(AStrings.Text);

  L:=Length(Text);

  if L>0 then
  begin
    C:=Text[FirstStringChar];

    if (C='{') or (C='[') then
    begin
      if ZBS then
         C:=Text[L-1]
      else
         C:=Text[L];

      if (C='}') or (C=']') then
         result:=gsJSON;
    end
    else
    if SameText(Copy(Text,FirstStringChar,5),'<?XML') then
       result:=gsXML
    else
    if SameText(Copy(Text,FirstStringChar,10),'<!DOCTYPE ') or
       SameText(Copy(Text,FirstStringChar,5),'<HTML') then
         result:=gsHTML;
  end;
end;

function TDataImport.FindGeoEntity(const ASeries:TWorldSeries; const AEntity:String):Integer;
var tmpS : String;
    tmp : TTeePolygon;
begin
  tmpS:=UpperCase(Trim(AEntity));

  tmp:=ASeries.Shapes.ByName[tmpS];

  if not Assigned(tmp) then
     tmp:=ASeries.Shapes.ByCode[tmpS];

  if not Assigned(tmp) then
     if tmpS='UNITED KINGDOM' then
        tmp:=ASeries.Shapes.ByCode['GB']
     else
     if tmpS='USA' then
        tmp:=ASeries.Shapes.ByCode['US'];

  if Assigned(tmp) then
     result:=tmp.Index
  else
     result:=-1;
end;

procedure TDataImport.SetGeoEntity(const ASeries:TWorldSeries; const AEntity:String; const AValue:TChartValue);
var tmp : Integer;
begin
  tmp:=FindGeoEntity(ASeries,AEntity);

  if tmp=-1 then
     ASeries.Add(AValue,AEntity)
  else
  begin
    ASeries.MandatoryValueList.Value[tmp]:=AValue;
    ASeries.SetNull(tmp,False);
  end;
end;

procedure TDataImport.LocalImport(const AStrings:TStrings; DoFree:Boolean);
var tmp : TCSVFormat;
begin
  if AStrings.Count>0 then
    case GuessStrings(AStrings) of
      gsJSON: {$IFDEF HASJSON}ImportJSON(AStrings){$ENDIF};
       gsXML: {$IFDEF HASXML}ImportXML(AStrings){$ENDIF};
      gsHTML: {$IFDEF HASXML}ImportHTML(AStrings.Text){$ENDIF};
    else
      begin
        tmp:=TCSVFormat.Create(AStrings,DoFree);
        try
          Import(tmp);
        finally
          FinishLocalFormat(tmp);
        end;
      end;
    end;
end;

// Imports text, guessing the field separator (CSV, tabs, spaces, etc)
procedure TDataImport.Import(const AStrings: TStrings);
begin
  LocalImport(AStrings,False);
end;

procedure TDataImport.Import(const AText: String);
var Strings : TStrings;
begin
  if AText<>'' then
  begin
    Strings:=TStringList.Create;
    try
      Strings.Text:=AText;
      Import(Strings);
    finally
      if not AnalyzeMode then
         Strings.Free;
    end;
  end;
end;

procedure TDataImport.Import(const AStream:TStream; const AFormat:TDataImportStreamFormat);
var Strings : TStrings;
    tmp     : TCSVFormat;
    {$IFDEF HASXML}
    XML : TXMLDocument;
    {$ENDIF}
begin
  case AFormat of
    ifCSV: begin
              Strings:=TStringList.Create;
              try
                Strings.LoadFromStream(AStream);

                tmp:=TCSVFormat.Create(Strings,AnalyzeMode);
                try
                  Import(tmp);
                finally
                  FinishLocalFormat(tmp);
                end;
              finally
                if not AnalyzeMode then
                   Strings.Free;
              end;
            end;

    {$IFDEF HASXML}
    ifXML: begin
             {$IFDEF FPC}
             ReadXMLFile(XML, AStream);
             try
             {$ELSE}
             XML:=TXMLDocument.Create(Self);
             try
               XML.LoadFromStream(AStream);
             {$ENDIF}
               Import(XML);
             finally
               if not AnalyzeMode then
                  XML.Free;
             end;
           end;
    {$ENDIF}

    {$IFDEF HASJSON}
   ifJSON: begin
              Strings:=TStringList.Create;
              try
                Strings.LoadFromStream(AStream);
                ImportJSON(Strings);
              finally
                if not AnalyzeMode then
                   Strings.Free;
              end;
           end;
    {$ENDIF}
  end;
end;

procedure TDataImport.Import(const AStream: TStringStream);
begin
  Import(TStringStream(AStream).DataString);
end;

// Returns a simplified field type from a DB field type:
function DataTypeOf(const AField:TFieldType):TDataType;
begin
  case AField of

    ftFixedChar,
    ftWideString,
    ftString : result:=dtText;

    ftSmallint,
    ftInteger,
    ftWord,
    ftFloat,
    ftCurrency,
    ftLargeint,

    {$IFDEF D6}
    ftFMTBcd,
    {$ENDIF}

    ftBCD : result:=dtNumber;

    ftBoolean: result:=dtBoolean;

    {$IFDEF D6}
    ftTimeStamp,
    {$ENDIF}

    ftDate,
    ftTime,
    ftDateTime : result:=dtDateTime;

    ftUnknown,
    ftBytes,
    ftVarBytes,
    ftBlob,
    ftParadoxOle,
    ftDBaseOle,
    ftTypedBinary,
    ftReference,// <-- Can be used?
    ftArray,    // <-- Can be used?
    ftDataSet,  // <-- Can be used?
    ftOraBlob,
    ftOraClob,
    ftInterface,
    ftIDispatch,
    ftGuid :   result:=dtBlob;

    ftGraphic : result:=dtPicture;
  else
    result:=dtNone;
  end;
end;

type
  TDatasetFormat=class(TDataFormat)
  private
    FDataset : TDataset;

    FreeDataset :Boolean;

    B : TBookmark;
    ITextField : TField;
  protected
    procedure Analyze; override;
    procedure BeginLoop; override;
    procedure EndLoop; override;

    function GetText(const ARow:Cardinal):String; override;
    function GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean; override;
    function NextIteration:Boolean; override;
    function ValidRow(const ARow:Cardinal):Boolean; override;
  public
    Constructor Create(const ADataset:TDataset; const DoFree:Boolean=False);
    Destructor Destroy; override;
  end;

Constructor TDatasetFormat.Create(const ADataset:TDataset; const DoFree:Boolean=False);
begin
  inherited Create;

  FDataset:=ADataset;
  FreeDataset:=DoFree;
end;

Destructor TDatasetFormat.Destroy;
begin
  if FreeDataset then
     FDataset.Free;

  inherited;
end;

procedure TDatasetFormat.BeginLoop;
begin
  FDataset.DisableControls;
  B:=FDataset.GetBookmark;
  FDataset.First;

  if TextField=-1 then
     ITextField:=nil
  else
     ITextField:=FDataset.Fields[TextField];
end;

function TDatasetFormat.ValidRow(const ARow:Cardinal):Boolean;
begin
  result:=not FDataset.Eof;
end;

function TDatasetFormat.GetText(const ARow:Cardinal):String;
begin
  if Assigned(ITextField) then
     result:=ITextField.AsString
  else
     result:='';
end;

function TDatasetFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
var tmpField : TField;
begin
  tmpField:=FDataset.Fields[SeriesIndex[ACol]];

  if tmpField.IsNull then
     result:=False
  else
  begin
    Value:=tmpField.AsFloat;
    result:=True;
  end;
end;

function TDatasetFormat.NextIteration:Boolean;
begin
  FDataset.Next;
  result:=not FDataset.Eof;
end;

procedure TDatasetFormat.EndLoop;
begin
  FDataset.GotoBookmark(B);
  FDataset.FreeBookmark(B);
  FDataset.EnableControls;
end;

procedure TDatasetFormat.Analyze;

  procedure Process(const Index:Integer; const AType:TFieldType);
  var tmp : String;
  begin
    tmp:=FDataset.Fields[Index].DisplayName;

    case DataTypeOf(AType) of
         dtNumber : NewSeriesIndex(Index,tmp);

           dtText : TrySetTextField(Index,tmp);
    end;
  end;

var t : Integer;
begin
  inherited;

  if FDataset.FieldCount>0 then
     for t:=0 to FDataset.FieldCount-1 do
         Process(t,FDataset.Fields[t].DataType)
  else
     for t:=0 to FDataset.FieldDefs.Count-1 do
         Process(t,FDataset.FieldDefs[t].DataType);

  DataQuantity:=FDataset.RecordCount;

  FirstRow:=0;

  if DataQuantity>0 then
     LastRow:=DataQuantity-1;

  if FDataset.Name='' then
     Title.Caption:=FDataset.ClassName
  else
     Title.Caption:=FDataset.Name;
end;

procedure TDataImport.LocalImport(const ADataset:TDataset; DoFree:Boolean);
var tmp : TDatasetFormat;
begin
  tmp:=TDatasetFormat.Create(ADataset,DoFree);
  try
    Import(tmp);
  finally
    FinishLocalFormat(tmp);
  end;
end;

procedure TDataImport.Import(const ADataset: TDataset);
begin
  if ADataset is TSeriesDataset then
     Import(TSeriesDataset(ADataset).Series)
  else
  if ADataset is TChartDataSet then
     Import(TChartDataSet(ADataset).Chart)
  else
  if ADataset.Active then
     LocalImport(ADataset,False)
  else
     Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from closed Dataset: '+ADataset.Name);
end;

{$IFDEF HASRTTI}
procedure TDataImport.Import(const ACollection: TCollection);
begin
  Import(TValue.From(ACollection));
end;
{$ENDIF}

procedure TDataImport.ImportFromURL(const AURL: String);

  procedure ImportHttp(const AURL:String; SSL:Boolean=False);
  var Stream : TStringStream;

      {$IFDEF HASINDY}
      Http : TIdHTTP;


      {$IFDEF D9}
      LHandler : TIdSSLIOHandlerSocketOpenSSL;
      {$ELSE}
      {$IFDEF D7}
      LHandler : TIdSSLIOHandlerSocket;
      {$ENDIF}
      {$ENDIF}

      {$ENDIF}
  begin
    Stream:=TStringStream.Create('');
    try
      TeeChangeCursor(Self,crHourGlass);
      try
        {$IFDEF HASINDY}

        Http:=TIdHTTP.Create(nil);
        try
          {$IFDEF D7}
          if SSL then
          begin
            {$IFDEF D9}
            LHandler:=TIdSSLIOHandlerSocketOpenSSL.Create(nil);
            {$ELSE}
            LHandler:=TIdSSLIOHandlerSocket.Create(nil);
            {$ENDIF}

            Http.IOHandler:=LHandler;
          end
          else
             LHandler:=nil;

          try
          {$ENDIF}

            Http.Get(AURL,Stream);

            Stream.Position:=0;

            Import(Stream);

          {$IFDEF D7}
          finally
            if SSL then
               LHandler.Free;
          end;
          {$ENDIF}

        finally
          Http.Free;
        end;

        {$ELSE}

        if DownloadURL(AURL,Stream)=0 then
        begin
          Stream.Position:=0;
          Import(Stream);
        end;

        {$ENDIF}

      finally
        TeeChangeCursor(Self,crDefault);
      end;
    finally
      Stream.Free;
    end;
  end;

  procedure ImportFtp;
  {$IFDEF HASINDY}
  var Ftp : TIdFTP;
      Stream : TStringStream;
      URI : TIdURI;
  begin
    Ftp:=TIdFTP.Create(Chart.Owner);
    try
      Stream:=TStringStream.Create('');
      try
        TeeChangeCursor(Self,crHourGlass);
        try
          URI := TIdURI.Create(AURL);
          try
            {$IFDEF D7}
            Ftp.Username := URI.Username;
            Ftp.Password := URI.Password;
            {$ENDIF}

            Ftp.Host := URI.Host;
            Ftp.Passive:=True;

            if URI.Port<>'' then
               Ftp.Port:=StrToInt(URI.Port);

            Ftp.Connect;
            Ftp.Get(URI.Document,Stream);

            Stream.Position:=0;

            Import(Stream);
          finally
            URI.Free;
          end;
        finally
          TeeChangeCursor(Self,crDefault);
        end;
      finally
        Stream.Free;
      end;
    finally
      Ftp.Free;
    end;
  {$ELSE}
  begin
  {$ENDIF}
  end;

begin
  case URLStyle(AURL) of
    usNone: ImportHttp('http://'+AURL);
    usHttp: ImportHttp(AURL);
   usHttps: ImportHttp(AURL,True);
     usFtp: ImportFtp;
  else
    Raise EDataImport.Create('Cannot import to chart '+Chart.Name+' from wrong URL: '+AURL);
  end;
end;

function TDataImport.InitFormat(const AQuantity:Cardinal):TDataFormat;
begin
  result:=TDataFormat.Create;

  result.AutoLoaded:=True;
  result.DataQuantity:=AQuantity;
  result.NewSeriesIndex(0,'Array');

  result.CreateSeries(nil);
end;

procedure TDataImport.Import(const Data: Array of Double);
var t : Integer;
    tmp : TDataFormat;
begin
  tmp:=InitFormat(Length(Data));
  try
    for t:=Low(Data) to High(Data) do
        tmp.Series[0].Add(Data[t]);

    if not AnalyzeMode then
       ImportToChart(tmp,Chart);
  finally
    FinishLocalFormat(tmp);
  end;
end;

procedure TDataImport.Import(const Data: Array of Integer);
var t : Integer;
    tmp : TDataFormat;
begin
  tmp:=InitFormat(Length(Data));
  try
    for t:=Low(Data) to High(Data) do
        tmp.Series[0].Add(Data[t]);

    if not AnalyzeMode then
       ImportToChart(tmp,Chart);
  finally
    FinishLocalFormat(tmp);
  end;
end;

procedure TDataImport.Import(const Data:Array of Int64);
var t : Integer;
    tmp : TDataFormat;
begin
  tmp:=InitFormat(Length(Data));
  try
    for t:=Low(Data) to High(Data) do
        tmp.Series[0].Add(Data[t]);

    if not AnalyzeMode then
       ImportToChart(tmp,Chart);
  finally
    FinishLocalFormat(tmp);
  end;
end;

{ TDataFormat }

Constructor TDataFormat.Create;
begin
  inherited Create(nil);

  {$IFDEF FPC}
  ISettings:=FormatSettings;
  {$ELSE}
  {$IFDEF D15}
  ISettings:=FormatSettings;
  {$ELSE}
  {$IFDEF D7}
  ISettings.DecimalSeparator:=DecimalSeparator;
  ISettings.ThousandSeparator:=ThousandSeparator;
  {$ENDIF}
  {$ENDIF}
  {$ENDIF}

  TextField:=-1;
end;

function TDataFormat.CloneSeries(const AOwner:TComponent):TChartSeriesList;
var t : Integer;
begin
  result:=TChartSeriesList.Create;

  for t:=0 to SeriesCount-1 do
      result.Add(CloneChartSeries(Series[t],AOwner,nil));
end;

function TDataFormat.Options:TStrings;
begin
  result:=TStringList.Create;
end;

procedure TDataFormat.ChangedOption(Sender:TObject);
begin
end;

procedure TDataFormat.PrepareOption(const AComponent:TComponent; const AIndex:Integer);
begin
end;

procedure TDataFormat.CreateSeries(const AOwner:TComponent);

  function NewSeries(const ASeries:TChartSeries):TChartSeries;
  var t : Integer;
      tmpW : TWorldSeries;
  begin
    result:=ASeries;

    if ASeries is TWorldSeries then
    begin
      tmpW:=TWorldSeries(ASeries);

      tmpW.Map:=IGeoMap;

      for t:=0 to tmpW.Shapes.Count-1 do
          tmpW.SetNull(t);

      tmpW.PaletteSteps:=16;
      tmpW.Marks.Show;
      tmpW.ColorEachPoint:=False;
    end;

    result.ManualData:=True;
    result.BeginUpdate;
  end;

  function IsGeoData:Boolean;

    function GuessMap(const S:String):TWorldMap;
    begin
      if S='USA' then
         result:=wmUSA
      else
      if S='AFRICA' then
         result:=wmAfrica
      else
      if S='ASIA' then
         result:=wmAsia
      else
      if S='AUSTRALIA' then
         result:=wmAustralia
      else
      if S='EUROPE' then
         result:=wmEurope
      else
         result:=wmWorld;
    end;

  var tmp : String;
  begin
    result:=(Length(Headers)>0);

    if result then
    begin
      tmp:=TextFieldName;

      if tmp='' then
         tmp:=Title.Caption;

      IGeoMap:=GuessMap(UpperCase(Trim(tmp)));

      result:=IGeoMap<>wmWorld;
    end;
  end;

  function BestSeriesClass(const AQuantity:Cardinal):TChartSeries;
  begin
    if IsGeoData then
    begin
      result:=TWorldSeries.CreateMap(AOwner,IGeoMap);
      ActualStyle:=isGeographic;
    end
    else
    if AQuantity>100 then
       result:=TFastLineSeries.Create(AOwner)
    else
    if AQuantity>15 then
       result:=TLineSeries.Create(AOwner) // TPointSeries when ?
    else
       result:=TBarSeries.Create(AOwner); // TPieSeries when ?
  end;

var t : Integer;
    tmp : TChartSeries;
begin
  if (ActualStyle=isGrid) or (ActualStyle=isXYZ) then
  begin
    NewSeries(TSurfaceSeries.Create(AOwner)).ParentChart:=Self;

    if ActualStyle=isGrid then
    begin
      (Series[0] as TCustom3DGridSeries).NumXValues:=Length(SeriesIndex);
      (Series[0] as TCustom3DGridSeries).NumZValues:=DataQuantity;
    end;
  end
  else
    for t:=0 to Length(SeriesIndex)-1 do
    begin
      tmp:=NewSeries(BestSeriesClass(DataQuantity));

      tmp.Title:=SeriesTitles[t];

      {
      if tmp.Title='' then
         tmp.Title:=IntToStr(t);
      }
      
      tmp.ParentChart:=Self;
    end;
end;

procedure TDataFormat.TrySetTextField(const AIndex:Integer; const ATitle:String);
begin
  if TextField=-1 then
  begin
    TextField:=AIndex;
    TextFieldName:=ATitle;
  end;
end;

procedure TDataFormat.NewSeriesIndex(const AIndex:Integer; const ATitle:String);
var L : Integer;
begin
  L:=Length(SeriesIndex);

  SetLength(SeriesIndex,L+1);
  SeriesIndex[L]:=AIndex;

  SetLength(SeriesTitles,L+1);

  {
  if ATitle='' then
     SeriesTitles[L]:=IntToStr(AIndex)
  else
  }
     SeriesTitles[L]:=ATitle;
end;

function TDataFormat.GuessValue(const AText:String; const AIndex:Integer; const ATitle:String):Boolean;

  procedure ReverseDecimal;
  begin
    if {$IFDEF D7}ISettings.{$ENDIF}DecimalSeparator='.' then
       {$IFDEF D7}ISettings.{$ENDIF}DecimalSeparator:=','
    else
       {$IFDEF D7}ISettings.{$ENDIF}DecimalSeparator:='.';
  end;

var Value : TChartValue;
begin
  result:=False;

  if TryStrToFloat(AText,Value {$IFDEF D7},ISettings{$ENDIF}) then
  begin
    NewSeriesIndex(AIndex,ATitle);
    result:=True;
  end
  else
  if DecimalDetected then
     TrySetTextField(AIndex,ATitle)
  else
  begin
    ReverseDecimal;

    if TryStrToFloat(AText,Value {$IFDEF D7},ISettings{$ENDIF}) then
    begin
      NewSeriesIndex(AIndex,ATitle);
      result:=True;
    end
    else
    begin
      ReverseDecimal;
      TrySetTextField(AIndex,ATitle);
    end;

    DecimalDetected:=True;
  end;
end;

procedure TDataFormat.Analyze;
begin
  FirstRow:=0;
  LastRow:=0;

  DecimalDetected:=False;
  ActualStyle:=isAutomatic;

  TextField:=-1;
  TextFieldName:='';

  Headers:=nil;
  HeaderLines:=0;

  SeriesTitles:=nil;
  SeriesIndex:=nil; // of Array of Word <-- Multiple ValueLists

  DataQuantity:=0;
end;

procedure TDataFormat.BeginLoop;
begin
end;

procedure TDataFormat.EndLoop;
begin
end;

function TDataFormat.GetText(const ARow:Cardinal):String;
begin
  result:='';
end;

function TDataFormat.GetValue(const ACol:Integer; const ARow:Cardinal; out Value:TChartValue):Boolean;
begin
  result:=False;
end;

function TDataFormat.NextIteration:Boolean;
begin
  result:=True; // Dont stop
end;

function TDataFormat.ValidRow(const ARow:Cardinal):Boolean;
begin
  result:=True;
end;

procedure TDataFormat.SetTitles(const AChart:TCustomChart; const AKeep:TKeepSettings);

  procedure TrySetAxis(const Axis:TChartAxis; const ATitle:String);
  begin
    if Axis.Title.Caption='' then
       Axis.Title.Caption:=ATitle;
  end;

var tmp : TChartSeries;
    t   : Integer;
begin
  if not (ksTitle in AKeep) then
     AChart.Title.Text:=Title.Text;

  if not (ksAxesTitle in AKeep) then
  begin
    for t:=0 to AChart.Axes.Count-1 do
        AChart.Axes[t].Title.Caption:='';

    if (ActualStyle=isGeographic) and (AChart.SeriesCount>0) then
    begin
      tmp:=AChart[0];

      tmp.GetHorizAxis.Title.Caption:=TeeMsg_Longitude;
      tmp.GetVertAxis.Title.Caption:=TeeMsg_Latitude;

      AChart.Axes.Hide;
      AChart.Walls.Back.Pen.Hide;

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

      if tmp.YMandatory then
      begin
        TrySetAxis(tmp.GetHorizAxis,TextFieldName);
        TrySetAxis(tmp.GetVertAxis,tmp.Title);
      end
      else
      begin
        TrySetAxis(tmp.GetVertAxis,TextFieldName);
        TrySetAxis(tmp.GetHorizAxis,tmp.Title);
      end;
    end;
  end;

  if not (ksLegendTitle in AKeep) then
     if AChart.SeriesCount=1 then
        if TextFieldName='' then
           AChart.Legend.Title.Caption:=AChart[0].Title
        else
           AChart.Legend.Title.Caption:=TextFieldName;
end;

procedure TDataFormat.TitleFromHeaders(const AStrings:TStrings);
var t : Integer;
begin
  AStrings.Clear;

  for t:=0 to HeaderLines-1 do
      AStrings.Add(Headers[t]);
end;

class procedure TDataImport.GroupSimilarSeries(const AList:TCustomSeriesList);

  function SimilarRange(const A,B:TChartSeries):Boolean;
  var tmpA,
      tmpB : TChartValueList;
  begin
    tmpA:=A.MandatoryValueList;
    tmpB:=B.MandatoryValueList;

    if (tmpA.MinValue>tmpB.MaxValue) or (tmpA.MaxValue<tmpB.MinValue) then
       result:=False
    else
       result:=True;
  end;

  function OtherVertAxis(const S:TChartSeries):TVertAxis;
  begin
    if S.VertAxis=aLeftAxis then
       result:=aRightAxis
    else
    if S.VertAxis=aRightAxis then
       result:=aLeftAxis
    else
    if S.VertAxis=aBothVertAxis then
    begin
      S.VertAxis:=aLeftAxis;
      result:=aRightAxis;
    end
    else
      result:=aLeftAxis;
  end;

  function OtherHorizAxis(const S:TChartSeries):THorizAxis;
  begin
    if S.HorizAxis=aBottomAxis then
       result:=aTopAxis
    else
    if S.HorizAxis=aTopAxis then
       result:=aBottomAxis
    else
    if S.HorizAxis=aBothHorizAxis then
    begin
      S.HorizAxis:=aBottomAxis;
      result:=aTopAxis;
    end
    else
      result:=aBottomAxis;
  end;

var t : Integer;
    FirstSeries,
    tmpSeries : TChartSeries;
begin
  if AList.Count>0 then
  begin
    FirstSeries:=AList[0];

    for t:=1 to AList.Count-1 do
    begin
      tmpSeries:=AList[t];

      if tmpSeries.YMandatory=FirstSeries.YMandatory then
         if SimilarRange(tmpSeries,FirstSeries) then
            if tmpSeries.YMandatory then
               tmpSeries.VertAxis:=FirstSeries.VertAxis
            else
               tmpSeries.HorizAxis:=FirstSeries.HorizAxis
         else
         begin
           if tmpSeries.YMandatory then
              tmpSeries.VertAxis:=OtherVertAxis(FirstSeries)
           else
              tmpSeries.HorizAxis:=OtherHorizAxis(FirstSeries);

           if tmpSeries.YMandatory then
              tmpSeries.GetVertAxis.Grid.Hide
           else
              tmpSeries.GetHorizAxis.Grid.Hide;
         end;
    end;
  end;
end;

procedure CheckZBS;
begin
  {$IFDEF D18}
  ZBS:=Low(String)=0;
  {$ELSE}
  ZBS:=False;
  {$ENDIF}

  if ZBS then
     FirstStringChar:=0
  else
     FirstStringChar:=1;
end;

initialization
  CheckZBS;

  RegisterTeeStandardSeries;
end.
