{------------------------------------------------------------------------------- The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is NiceChart.pas released at May 26th, 2007. The Initial Developer of the Original Code is Priyatna. (Website: http://www.priyatna.org Email: me@priyatna.org) All Rights Reserved. Alternatively, the contents of this file may be used under the terms of the GNU General Public License Version 2 or later (the "GPL"), in which case the provisions of the GPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the GPL and not to allow others to use your version of this file under the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL. If you do not delete the provisions above, a recipient may use your version of this file under either the MPL or the GPL. -------------------------------------------------------------------------------} {$IFDEF FPC} {$MODE Delphi} {$ENDIF} unit NiceChart; interface uses {$IFDEF FPC} LclIntf, LCLType, LMessages, {$ELSE} Windows, Messages, {$ENDIF} Classes, Graphics, Forms, Controls, ExtCtrls, SysUtils, BSplines, Math; const UndefinedValueChart: Double = MaxDouble; type TNiceChart = class; TSeriesKind = (skLine, skSmooth, skBar); TNiceSeries = class(TObject) private Top: Integer; Values: TList; Chart: TNiceChart; FCaption: string; Spline: TBSpline; FKind: TSeriesKind; FLineWidth: Integer; procedure SetCaption(const Value: string); function GetMaxXValue: Double; function GetMinXValue: Double; function GetMinYValue: Double; function GetMaxYValue: Double; procedure SetKind(const Value: TSeriesKind); procedure SetLineWidth(const Value: Integer); protected procedure InternalClear; public constructor Create(AChart: TNiceChart; AKind: TSeriesKind); destructor Destroy; override; function AddXY(AX, AY: Double; AHint: string = ''): Integer; procedure Remove(Index: Integer); procedure Clear; property Caption: string read FCaption write SetCaption; property Kind: TSeriesKind read FKind write SetKind; property LineWidth: Integer read FLineWidth write SetLineWidth default 1; end; TValueTranslator = record MinValue: Double; Scale: Double; Base: Integer; end; TMarkerProc = procedure (ACanvas: TCanvas; X, Y, Size: Integer); TNiceChart = class(TCustomPanel) private Brushes: array [0..15] of TBitmap; Temp: TStringList; MarkSize: Integer; Marker: TMarkerProc; BarCount: Integer; BarWidth: Integer; DestWidth, DestHeight: Integer; YZero: Integer; ChartEmpty: Boolean; List: TList; XAxis: TList; YAxis: TList; FShowLegend: Boolean; FShowTitle: Boolean; FShowXAxisLine: Boolean; FShowYAxisLine: Boolean; FTitle: string; FTitleFont: TFont; FNormalFont: TFont; FUpdating: Boolean; RcChart, RcLegend, RcTitle: TRect; FXTranslate: TValueTranslator; FYTranslate: TValueTranslator; FAxisXOnePerValue: Boolean; FAxisYTitle: string; FAxisXTitle: string; FAxisLineWidth: Integer; FShowYGrid: Boolean; FShowXGrid: Boolean; FAxisYScale: Single; FAxisXScale: Single; FMonochrome: Boolean; FGridColor: TColor; FSoftColors: Boolean; FTickLength: Integer; FOuterMargin: Integer; FInnerMargin: Integer; FSmallMargin: Integer; FAxisDefSize: Integer; FLegendItemSize: Integer; {$IFNDEF FPC} FClipRgn: HRGN; {$ENDIF} procedure InternalClear; procedure InternalPaint(ACanvas: TCanvas); procedure Calculate(AWidth, AHeight: Integer); procedure DoCalculate(ACanvas: TCanvas; AWidth, AHeight: Integer); procedure DoPaint; procedure SetGridColor(const Value: TColor); procedure SetShowLegend(const Value: Boolean); procedure SetShowTitle(const Value: Boolean); procedure SetTickLength(const Value: Integer); procedure SetTitle(const Value: string); procedure SetTitleFont(const Value: TFont); procedure TitleFontChanged(Sender: TObject); {$IFDEF FPC} procedure WMSize(var Msg: TLMSize); message LM_SIZE; procedure WMEraseBkgnd(var Msg: TLMEraseBkgnd); message LM_ERASEBKGND; {$ELSE} procedure WMSize(var Msg: TWMSize); message WM_SIZE; procedure WMEraseBkgnd(var Msg: TWMEraseBkgnd); message WM_ERASEBKGND; {$ENDIF} function GetSeries(Index: Integer): TNiceSeries; function GetSeriesCount: Integer; procedure DrawLegend(ACanvas: TCanvas); procedure DrawTitle(ACanvas: TCanvas); procedure SetAxisXTitle(const Value: string); procedure SetAxisYTitle(const Value: string); procedure BuildYAxis; procedure DrawYAxis(ACanvas: TCanvas); procedure DrawXAxis(ACanvas: TCanvas); procedure DrawChart(ACanvas: TCanvas); procedure BuildXAxis; procedure ClearAxis; procedure AdjustYAxis; procedure SetAxisXOnePerValue(const Value: Boolean); procedure SetShowXGrid(const Value: Boolean); procedure SetShowYGrid(const Value: Boolean); procedure SetShowXAxisLine(const Value: Boolean); procedure SetShowYAxisLine(const Value: Boolean); procedure CalculateSeries; procedure DrawSeries(ACanvas: TCanvas; Index: Integer); procedure AutoColors(ACanvas: TCanvas; Index: Integer; IsBar: Boolean); procedure SetAxisXScale(const Value: Single); procedure SetAxisYScale(const Value: Single); procedure SetMonochrome(const Value: Boolean); function GetLabel(Value: Double): string; procedure SetSoftColors(const Value: Boolean); function IsAxisXScaleStored: Boolean; function IsAxisXTitleStored: Boolean; function IsAxisYScaleStored: Boolean; function IsAxisYTitleStored: Boolean; function IsTitleStored: Boolean; procedure SetAxisLineWidth(const Value: Integer); protected procedure Paint; override; procedure Changed; procedure ChartToClient(const AX, AY: Double; var X, Y: Integer); procedure CreateHandle; override; procedure ClipToRect(ACanvas: TCanvas; const ARect: TRect; AEnable: Boolean); {$IFDEF FPC} procedure DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy; const AXProportion, AYProportion: Double); override; {$ENDIF} public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure BeginUpdate; procedure EndUpdate; function AddSeries(AKind: TSeriesKind): TNiceSeries; function ClientToChart(const X, Y: Integer; var AX, AY: Double): Boolean; procedure RemoveSeries(ASeries: TNiceSeries); procedure Clear; property Series[Index: Integer]: TNiceSeries read GetSeries; property SeriesCount: Integer read GetSeriesCount; {$IFDEF FPC} procedure FixDesignFontsPPI(const ADesignTimePPI: Integer); override; procedure ScaleFontsPPI(const AToPPI: Integer; const AProportion: Double); override; {$ELSE} // Lazarus does not have TMetaFile... function CreateMetafile: TMetafile; procedure CopyToClipboard; {$ENDIF} function CreateBitmap(AWidth, AHeight: Integer): TBitmap; procedure CopyToClipboardAsBitmap; published property AxisLineWidth: Integer read FAxisLineWidth write SetAxisLineWidth default 3; property ShowLegend: Boolean read FShowLegend write SetShowLegend default true; property ShowTitle: Boolean read FShowTitle write SetShowTitle default true; property ShowXGrid: Boolean read FShowXGrid write SetShowXGrid default true; property ShowYGrid: Boolean read FShowYGrid write SetShowYGrid default true; property ShowXAxisLine: boolean read FShowXAxisLine write SetShowXAxisLine default false; property ShowYAxisLine: Boolean read FShowYAxisLine write SetShowYAxisLine default false; property Title: string read FTitle write SetTitle stored IsTitleStored; property TitleFont: TFont read FTitleFont write SetTitleFont; property AxisXTitle: string read FAxisXTitle write SetAxisXTitle stored IsAxisXTitleStored; property AxisYTitle: string read FAxisYTitle write SetAxisYTitle stored IsAxisYTitleStored; property AxisXOnePerValue: Boolean read FAxisXOnePerValue write SetAxisXOnePerValue default false; property AxisXScale: Single read FAxisXScale write SetAxisXScale stored IsAxisXScaleStored; property AxisYScale: Single read FAxisYScale write SetAxisYScale stored IsAxisYScaleStored; property GridColor: TColor read FGridColor write SetGridColor default clGray; property Monochrome: Boolean read FMonochrome write SetMonochrome default false; property SoftColors: Boolean read FSoftColors write SetSoftColors default false; property TickLength: Integer read FTickLength write SetTickLength default 2; property Align; property Anchors; property BevelInner default bvNone; property BevelOuter default bvNone; property BorderStyle default bsSingle; {$IFDEF FPC} property BorderSpacing; {$ELSE} property BevelKind; {$ENDIF} property OnMouseMove; property OnMouseDown; property OnMouseUp; property PopupMenu; end; procedure CalculateAxis(AMin, AMax: Double; Count: Integer; out Delta, Lowest: Double); procedure Register; implementation {$R NiceChart.res} {$IFDEF FPC} {$R nicechart_images.res} {$ENDIF} uses ClipBrd; procedure Register; begin RegisterComponents('priyatna.org', [TNiceChart]); end; const OUTER_MARGIN = 20; INNER_MARGIN = 10; SMALL_MARGIN = 2; LEGEND_ITEM = 20; AXIS_DEFSIZE = 50; Formatter = '0.##'; type PXYInfo = ^TXYInfo; TXYInfo = record X, Y: Double; Px, Py: Integer; Rc: TRect; Hint: string; end; PAxisInfo = ^TAxisInfo; TAxisInfo = record Value: Double; Px, Py: Integer; Caption: string; end; function GetMan10(Value: Double): Double; var Str: string; begin Str := UpperCase(Format('%E', [Value])); Result := StrToFloat('1E' + Copy(Str, Pos('E', Str) + 1, Length(Str))); end; procedure CalculateAxis(AMin, AMax: Double; Count: Integer; out Delta, Lowest: Double); label Retry; var c, n, m10: Double; begin c := Max(2, Count-1); n := (Abs(AMax - AMin) / c); m10 := GetMan10(n); Delta := 0; while (Delta < n) do Delta := Delta + (0.5 * m10); if (Delta = 0) then begin Delta := 1; Lowest := AMin - (Count div 2); Exit; end; Retry: Lowest := Trunc(AMin / Delta) * Delta; if (Lowest > AMin) then Lowest := Lowest - Delta; if ((Lowest + (Delta * c)) < AMax) then begin Delta := Delta + (0.5 * m10); goto Retry; end; end; { TNiceSeries } constructor TNiceSeries.Create(AChart: TNiceChart; AKind: TSeriesKind); begin inherited Create; Chart := AChart; Values := TList.Create; FCaption := 'Series'; Spline := TBSpline.Create; FKind := AKind; FLineWidth := 1; end; destructor TNiceSeries.Destroy; begin Spline.Free; InternalClear; Values.Free; inherited Destroy; end; procedure TNiceSeries.InternalClear; var x: Integer; begin for x := 0 to Values.Count-1 do Dispose(PXYInfo(Values[x])); Values.Clear; end; procedure TNiceSeries.Clear; begin InternalClear; Chart.Changed; end; function TNiceSeries.AddXY(AX, AY: Double; AHint: string): Integer; var Info: PXYInfo; begin Info := New(PXYInfo); Info^.X := AX; Info^.Y := AY; Info^.Px := 0; Info^.Py := 0; Info^.Rc := Rect(0, 0, 0, 0); Info^.Hint := AHint; Result := Values.Add(Info); Chart.Changed; end; procedure TNiceSeries.Remove(Index: Integer); var P: PXYInfo; begin if (Index >= 0) and (Index < Values.Count) then begin P := Values[Index]; Values.Remove(P); Dispose(P); Chart.Changed; end; end; function TNiceSeries.GetMaxXValue: Double; var x: Integer; begin Result := -MaxDouble; for x := 0 to Values.Count-1 do Result := Max(Result, PXYInfo(Values[x])^.X); end; function TNiceSeries.GetMinXValue: Double; var x: Integer; begin Result := MaxDouble; for x := 0 to Values.Count-1 do Result := Min(Result, PXYInfo(Values[x])^.X); end; function TNiceSeries.GetMaxYValue: Double; var x: Integer; begin Result := -MaxDouble; for x := 0 to Values.Count-1 do Result := Max(Result, PXYInfo(Values[x])^.Y); end; function TNiceSeries.GetMinYValue: Double; var x: Integer; begin Result := MaxDouble; for x := 0 to Values.Count-1 do Result := Min(Result, PXYInfo(Values[x])^.Y); end; procedure TNiceSeries.SetCaption(const Value: string); begin if (FCaption <> Value) then begin FCaption := Value; Chart.Changed; end; end; procedure TNiceSeries.SetKind(const Value: TSeriesKind); begin if (FKind <> Value) then begin FKind := Value; Chart.Changed; end; end; procedure TNiceSeries.SetLineWidth(const Value: Integer); begin if FLineWidth <> Value then begin FLineWidth := Value; Chart.Changed; end; end; { TNiceChart } constructor TNiceChart.Create(AOwner: TComponent); begin inherited Create(AOwner); ParentColor := False; ParentBackground := False; ParentFont := False; Temp := TStringList.Create; Width := 300; Height := 200; Color := clWhite; BevelOuter := bvNone; BevelInner := bvNone; BorderStyle := bsSingle; ControlStyle := ControlStyle + [csNeedsBorderPaint]; List := TList.Create; FShowLegend := True; FShowTitle := True; FShowXGrid := True; FShowYGrid := True; FGridColor := clGray; FMonochrome := False; FTitle := 'Chart Title'; FTitleFont := TFont.Create; FTitleFont.Name := 'Arial'; FTitleFont.Size := 14; FTitleFont.Style := []; FTitleFont.OnChange := TitleFontChanged; FNormalFont := TFont.Create; FNormalFont.Name := 'Arial'; FAxisLineWidth := 3; FAxisXTitle := 'X Axis'; FAxisYTitle := 'Y Axis'; FAxisXScale := 1; FAxisYScale := 1; FTickLength := 2; FOuterMargin := OUTER_MARGIN; FInnerMargin := INNER_MARGIN; FSmallMargin := SMALL_MARGIN; FAxisDefSize := AXIS_DEFSIZE; FLegendItemSize := LEGEND_ITEM; XAxis := TList.Create; YAxis := TList.Create; FUpdating := False; end; destructor TNiceChart.Destroy; var x: Integer; begin for x := 0 to 15 do begin if Assigned(Brushes[x]) then Brushes[x].Free; end; InternalClear; List.Free; FTitleFont.Free; FNormalFont.Free; XAxis.Free; YAxis.Free; Temp.Free; inherited Destroy; end; procedure TNiceChart.InternalClear; var x: Integer; begin for x := 0 to List.Count-1 do TNiceSeries(List[x]).Free; ClearAxis; List.Clear; end; procedure TNiceChart.Paint; begin if HandleAllocated then DoPaint; end; procedure TNiceChart.DoPaint; begin InternalPaint(Canvas); end; procedure TNiceChart.SetGridColor(const Value: TColor); begin if FGridColor <> value then begin FGridColor := Value; Invalidate; end; end; procedure TNiceChart.SetMonochrome(const Value: Boolean); begin if (FMonochrome <> Value) then begin FMonochrome := Value; Changed; end; end; procedure TNiceChart.SetSoftColors(const Value: Boolean); begin if (FSoftColors <> Value) then begin FSoftColors := Value; Changed; end; end; procedure TNiceChart.SetShowLegend(const Value: Boolean); begin if (FShowLegend <> Value) then begin FShowLegend := Value; Changed; end; end; procedure TNiceChart.SetAxisXOnePerValue(const Value: Boolean); begin if (FAxisXOnePerValue <> Value) then begin FAxisXOnePerValue := Value; Changed; end; end; procedure TNiceChart.SetShowTitle(const Value: Boolean); begin if (FShowTitle <> Value) then begin FShowTitle := Value; Changed; end; end; procedure TNiceChart.SetTitle(const Value: string); begin if (FTitle <> Value) then begin FTitle := Value; Changed; end; end; procedure TNiceChart.SetTitleFont(const Value: TFont); begin FTitleFont.Assign(Value); end; procedure TNiceChart.TitleFontChanged(Sender: TObject); begin Changed; end; procedure TNiceChart.SetAxisXTitle(const Value: string); begin if (FAxisXTitle <> Value) then begin FAxisXTitle := Value; Changed; end; end; procedure TNiceChart.SetAxisYTitle(const Value: string); begin if (FAxisYTitle <> Value) then begin FAxisYTitle := Value; Changed; end; end; function TNiceChart.IsAxisXScaleStored: Boolean; begin Result := FAxisXScale <> 1.0; end; function TNiceChart.IsAxisXTitleStored: Boolean; begin Result := FaxisXTitle <> ''; end; function TNiceChart.IsAxisYScaleStored: Boolean; begin Result := FAxisYScale <> 1.0; end; function TNiceChart.IsAxisYTitleStored: Boolean; begin Result := FAxisYTitle <> ''; end; function TNiceChart.IsTitleStored: Boolean; begin Result := FTitle <> ''; end; procedure TNiceChart.SetAxisXScale(const Value: Single); begin if (FAxisXScale <> Value) then begin FAxisXScale := Value; if (FAxisXScale = 0) then FAxisXScale := 1; Changed; end; end; procedure TNiceChart.SetAxisLineWidth(const Value: Integer); begin if (FAxisLineWidth <> Value) then begin FAxisLineWidth := Value; if FAxisLineWidth < 1 then FAxisLineWidth := 1; Changed; end; end; procedure TNiceChart.SetAxisYScale(const Value: Single); begin if (FAxisYScale <> Value) then begin FAxisYScale := Value; if (FAxisYScale = 0) then FAxisYScale := 1; Changed; end; end; procedure TNiceChart.SetShowXAxisLine(const Value: Boolean); begin if (FShowXAxisLine <> Value) then begin FShowXAxisLine := Value; Invalidate; end; end; procedure TNiceChart.SetShowXGrid(const Value: Boolean); begin if (FShowXGrid <> Value) then begin FShowXGrid := Value; Invalidate; end; end; procedure TNiceChart.SetShowYAxisLine(const Value: Boolean); begin if (FShowYAxisLine <> Value) then begin FShowYAxisLine := Value; Invalidate; end; end; procedure TNiceChart.SetShowYGrid(const Value: Boolean); begin if (FShowYGrid <> Value) then begin FShowYGrid := Value; Invalidate; end; end; procedure TNiceChart.SetTickLength(const Value: Integer); begin if (FTickLength <> Value) and (Value >= 0) then begin FTickLength := Value; Invalidate; end; end; procedure TNiceChart.BeginUpdate; begin FUpdating := True; end; procedure TNiceChart.EndUpdate; begin FUpdating := False; Calculate(ClientWidth, ClientHeight); Invalidate; end; procedure TNiceChart.Changed; begin if not FUpdating then begin Calculate(ClientWidth, ClientHeight); Invalidate; end; end; procedure TNiceChart.WMSize(var Msg: {$IFDEF FPC}TLMSize{$ELSE}TWMSize{$ENDIF}); begin inherited; Changed; end; procedure TNiceChart.WMEraseBkgnd(var Msg: {$IFDEF FPC}TLMEraseBkgnd{$ELSE}TWMEraseBkgnd{$ENDIF}); begin Msg.Result := 1; end; function TNiceChart.GetSeries(Index: Integer): TNiceSeries; begin Result := TNiceSeries(List[Index]); end; function TNiceChart.AddSeries(AKind: TSeriesKind): TNiceSeries; begin Result := TNiceSeries.Create(Self, AKind); List.Add(Result); end; procedure TNiceChart.Clear; begin InternalClear; Changed; end; procedure TNiceChart.RemoveSeries(ASeries: TNiceSeries); begin if Assigned(ASeries) then begin List.Remove(ASeries); ASeries.Free; Changed; end; end; function TNiceChart.GetSeriesCount: Integer; begin Result := List.Count; end; procedure TNiceChart.DrawLegend(ACanvas: TCanvas); var x, y, l, t: Integer; th, g: Integer; begin with ACanvas do begin Pen.Width := 1; Pen.Style := psSolid; Font.Assign(FNormalFont); g := TextHeight('Ag'); th := (FLegendItemSize - g) div 2; Brush.Style := bsSolid; Brush.Color := clBlack; FillRect(Rect(RcLegend.Right, RcLegend.Top + 3, RcLegend.Right + 3, RcLegend.Bottom + 3)); FillRect(Rect(RcLegend.Left + 3, RcLegend.Bottom, RcLegend.Right + 3, RcLegend.Bottom + 3)); Brush.Style := bsClear; Rectangle(RcLegend); Brush.Style := bsClear; l := RcLegend.Left + FInnerMargin + FLegendItemSize + FSmallMargin; for x := 0 to List.Count-1 do begin Temp.Text := Trim(TNiceSeries(List[x]).FCaption); t := RcLegend.Top + TNiceSeries(List[x]).Top; for y := 0 to Temp.Count-1 do begin TextOut(l, t + th, Trim(Temp[y])); Inc(t, g); end; end; end; end; procedure TNiceChart.DrawTitle(ACanvas: TCanvas); begin with ACanvas do begin Brush.Style := bsClear; Font.Assign(FTitleFont); DrawText(Handle, PChar(FTitle), Length(FTitle), RcTitle, DT_CENTER or DT_VCENTER or DT_WORDBREAK); end; end; procedure RotTextOut(ACanvas: TCanvas; x, y, Angle: Integer; Txt: String); {$IFNDEF FPC} var RotFont, OldFont: Integer; FBold, FItalic, FUnderline, FStrikeOut: integer; {$ENDIF} begin if (Txt = '') then Exit; SetBkMode(ACanvas.Handle, TRANSPARENT); {$IFDEF FPC} ACanvas.Font.Orientation := Angle * 10; ACanvas.TextOut(x, y, Txt); ACanvas.Font.Orientation := 0; {$ELSE} if (fsItalic in ACanvas.Font.Style) then FItalic := 1 else FItalic := 0; if (fsUnderline in ACanvas.Font.Style) then FUnderline := 1 else FUnderline := 0; if (fsStrikeOut in ACanvas.Font.Style) then FStrikeOut := 1 else FStrikeOut := 0; if (fsBold in ACanvas.Font.Style) then FBold := FW_BOLD else FBold := FW_NORMAL; RotFont := CreateFont(ACanvas.Font.Height, 0, Angle*10, 0, FBold, FItalic, FUnderline, FStrikeOut, 1, 4, $10, ANTIALIASED_QUALITY, 4, PChar(ACanvas.Font.Name)); OldFont := SelectObject(ACanvas.Handle, RotFont); TextOut(ACanvas.Handle, x, y, PChar(Txt), Length(Txt)); SelectObject(ACanvas.Handle, OldFont); DeleteObject(RotFont); {$ENDIF} end; procedure TNiceChart.InternalPaint(ACanvas: TCanvas); begin with ACanvas do begin Pen.Color := clBlack; Pen.Width := 1; Brush.Style := bsSolid; Brush.Color := Color; FillRect(Rect(0, 0, DestWidth, DestHeight)); end; if FShowLegend and (List.Count > 0) then DrawLegend(ACanvas); if FShowTitle and (FTitle <> '') then DrawTitle(ACanvas); DrawXAxis(ACanvas); DrawYAxis(ACanvas); DrawChart(ACanvas); end; procedure TNiceChart.Calculate(AWidth, AHeight: Integer); var bmp: TBitmap; begin if Canvas.HandleAllocated then DoCalculate(Canvas, AWidth, AHeight) else begin // Use an auxiliary bitmap in case of early calls when the Canvas has not handle, yet. bmp := TBitmap.Create; try bmp.Width := AWidth; bmp.Height := AHeight; DoCalculate(bmp.Canvas, AWidth, AHeight); finally bmp.Free; end; end; end; procedure TNiceChart.DoCalculate(ACanvas: TCanvas; AWidth, AHeight: Integer); var x, w, h, y, g: Integer; Titled: Boolean; begin ClearAxis; DestWidth := AWidth; DestHeight := AHeight; RcChart := Rect(0, 0, DestWidth, DestHeight); MarkSize := Max(1, Round(DestWidth * 0.006)); InflateRect(RcChart, -FOuterMargin, -FOuterMargin); Titled := False; if FShowTitle and (FTitle <> '') then begin ACanvas.Font.Assign(TitleFont); w := ACanvas.TextHeight(FTitle); RcTitle := Rect(RcChart.Left, RcChart.Top, RcChart.Right, RcChart.Left + w); DrawText(ACanvas.Handle, PChar(FTitle), Length(FTitle), RcTitle, DT_CENTER or DT_VCENTER or DT_WORDBREAK or DT_CALCRECT); RcChart.Top := RcTitle.Bottom + FInnerMargin; Titled := True; end else SetRectEmpty(RcTitle); ACanvas.Font.Assign(FNormalFont); h := ACanvas.TextHeight('Ag'); RcChart.Bottom := RcChart.Bottom - (2 * h) - FInnerMargin - FTickLength - FSmallmargin; BuildYAxis; w := 0; for x := 0 to YAxis.Count-1 do w := Max(w, ACanvas.TextWidth(PAxisInfo(YAxis[x])^.Caption)); RcChart.Left := RcChart.Left + h + FInnerMargin + w + FTickLength + FSmallMargin; RcTitle.Left := RcChart.Left; RcTitle.Right := RcChart.Right; AdjustYAxis; if FShowLegend and (List.Count > 0) then begin ACanvas.Font.Assign(FNormalFont); w := 0; h := FInnerMargin; g := ACanvas.TextHeight('Ag'); for x := 0 to List.Count-1 do begin TNiceSeries(List[x]).Top := h; Temp.Text := Trim(TNiceSeries(List[x]).FCaption); for y := 0 to Temp.Count-1 do w := Max(w, ACanvas.TextWidth(Trim(Temp[y]))); h := h + Max(FLegendItemSize, Temp.Count * g); if (x <> List.Count-1) then h := h + FSmallMargin; end; w := w + (2 * FInnerMargin) + FLegendItemSize + FSmallMargin; h := h + FInnerMargin; RcLegend := Rect(RcChart.Right - w, RcChart.Top, RcChart.Right, RcChart.Top + h); RcChart.Right := RcLegend.Left - (2 * FInnerMargin); if Titled then RcTitle.Right := RcChart.Right; end else SetRectEmpty(RcLegend); BuildXAxis; CalculateSeries; end; procedure TNiceChart.ClearAxis; var x: Integer; begin for x := 0 to XAxis.Count-1 do Dispose(PAxisInfo(XAxis[x])); XAxis.Clear; for x := 0 to YAxis.Count-1 do Dispose(PAxisInfo(YAxis[x])); YAxis.Clear; end; type PDoubleList = ^TDoubleList; TDoubleList = array [0..0] of Double; procedure QuickSortDouble(SortList: PDoubleList; L, R: Integer); var I, J: Integer; P, T: Double; begin repeat I := L; J := R; P := SortList^[(L + R) shr 1]; repeat while (SortList^[I] < P) do Inc(I); while (SortList^[J] > P) do Dec(J); if I <= J then begin T := SortList^[I]; SortList^[I] := SortList^[J]; SortList^[J] := T; Inc(I); Dec(J); end; until I > J; if L < J then QuickSortDouble(SortList, L, J); L := I; until I >= R; end; function TNiceChart.GetLabel(Value: Double): string; begin if (Value = UndefinedValueChart) then Result := '~' else Result := FormatFloat(Formatter, Value); end; procedure TNiceChart.BuildXAxis; var x, y, w: Integer; mi, ma: Double; Cnt, i, n: Integer; Delta, Lowest, l: Double; P: PAxisInfo; tmp: PDoubleList; Vals: TList; Last: Double; Scale: Double; dx: Integer; begin if (List.Count = 0) or ChartEmpty then Exit; BarCount := 0; for x := 0 to List.Count-1 do begin if (TNiceSeries(List[x]).FKind = skBar) then Inc(BarCount); end; if (BarCount > 0) then FAxisXOnePerValue := True; if FAxisXOnePerValue then begin w := RcChart.Right - RcChart.Left; Cnt := 0; for x := 0 to List.Count-1 do Cnt := Cnt + Series[x].Values.Count; GetMem(tmp, Cnt * SizeOf(Double)); i := 0; for x := 0 to List.Count-1 do begin Vals := TNiceSeries(List[x]).Values; for y := 0 to Vals.Count-1 do begin tmp^[i] := PXYInfo(Vals[y])^.X; Inc(i); end; end; QuickSortDouble(tmp, 0, Cnt-1); n := 0; Last := MaxDouble; for x := 0 to Cnt-1 do begin l := tmp^[x]; if (l = Last) then Continue; Inc(n); Last := l; end; if (BarCount > 0) then begin Scale := w / n; dx := Round(Scale / 2); BarWidth := Round(Scale); end else begin Scale := w / (n-1); dx := 0; end; Last := MaxDouble; i := 0; for x := 0 to Cnt-1 do begin l := tmp^[x]; if (l = Last) then Continue; P := New(PAxisInfo); P^.Value := l; P^.Py := RcChart.Bottom; P^.Px := RcChart.Left + dx + Round(i * Scale); P^.Caption := GetLabel(l / FAxisXScale); XAxis.Add(P); Last := l; Inc(i); end; FreeMem(tmp); end else begin w := RcChart.Right - RcChart.Left; Cnt := (w div FAxisDefSize) + 1; mi := MaxDouble; ma := -MaxDouble; for x := 0 to List.Count-1 do begin mi := Min(mi, Series[x].GetMinXValue); ma := Max(ma, Series[x].GetMaxXValue); end; CalculateAxis(mi, ma, Cnt, Delta, Lowest); Scale := w / (Delta * Max(1, Cnt-1)); for x := 0 to Cnt-1 do begin l := x * Delta; P := New(PAxisInfo); P^.Py := RcChart.Bottom; P^.Px := RcChart.Left + Round(l * Scale); P^.Caption := GetLabel((Lowest + l) / FAxisXScale); XAxis.Add(P); end; FXTranslate.MinValue := Lowest; FXTranslate.Scale := Scale; FXTranslate.Base := RcChart.Left; end; end; procedure TNiceChart.BuildYAxis; var x, w: Integer; mi, ma: Double; Cnt: Integer; Delta, Lowest, t: Double; P: PAxisInfo; Scale: Double; begin if (List.Count = 0) then Exit; w := RcChart.Bottom - RcChart.Top; Cnt := (w div FAxisDefSize) + 1; ChartEmpty := True; mi := MaxDouble; ma := -MaxDouble; for x := 0 to List.Count-1 do begin if (Series[x].Values.Count > 0) then begin mi := Min(mi, Series[x].GetMinYValue); ma := Max(ma, Series[x].GetMaxYValue); ChartEmpty := False; end; end; if ChartEmpty then Exit; CalculateAxis(mi, ma, Cnt, Delta, Lowest); Scale := w / (Delta * Max(1, Cnt-1)); for x := 0 to Cnt-1 do begin t := x * Delta; P := New(PAxisInfo); P^.Value := Lowest + t; P^.Py := Round(t * Scale); P^.Caption := GetLabel((Lowest + t) / FAxisYScale); YAxis.Add(P); end; FYTranslate.MinValue := Lowest; FYTranslate.Scale := Scale; end; procedure TNiceChart.AdjustYAxis; var x: Integer; P: PAxisInfo; l: Integer; begin l := RcChart.Left; YZero := -1; for x := 0 to YAxis.Count-1 do begin P := PAxisInfo(YAxis[x]); P^.Px := l; P^.Py := RcChart.Bottom - P^.Py; if (P^.Value = 0) then YZero := P^.Py; end; if (YZero = -1) then YZero := RcChart.Bottom; FYTranslate.Base := RcChart.Bottom; end; procedure TNiceChart.DrawXAxis(ACanvas: TCanvas); var l, t, w, i: Integer; P: PAxisInfo; Str: string; Last: Integer; dummy, origin: Integer; begin with ACanvas do begin if FShowXGrid then begin Pen.Style := psDot; Pen.Width := 1; if FMonochrome then Pen.Color := clBlack else Pen.Color := FGridColor; t := RcChart.Top; for i := 0 to XAxis.Count-1 do begin P := PAxisInfo(XAxis[i]); if (P^.Px <= RcChart.Left) or (P^.Px >= RcChart.Right) then Continue; MoveTo(P^.Px, P^.Py); LineTo(P^.px, t); end; end; // Drawing the y axis here seems to be misplaced. But otherwise // it could be overpainted by an x grid line. if FShowYAxisLine then begin ChartToClient(0, 0, origin, dummy); if (origin > RcChart.Left) and (origin < RcChart.Right) then begin Pen.Style := psSolid; Pen.Color := clBlack; Pen.Width := 1; MoveTo(origin, RcChart.Top); LineTo(origin, RcChart.Bottom); end; end; Pen.Style := psSolid; Pen.Width := FAxisLineWidth; Pen.Color := clBlack; MoveTo(RcChart.Left, RcChart.Bottom); LineTo(RcChart.Right, RcChart.Bottom); Font.Assign(FNormalFont); Font.Style := [fsBold]; w := RcChart.Right - RcChart.Left; t := RcChart.Bottom + FInnerMargin + FTickLength + FSmallMargin + TextHeight('Ag'); l := RcChart.Left + ((w - TextWidth(FAxisXTitle)) div 2); TextOut(l, t, FAxisXTitle); Font.Assign(FNormalFont); Pen.Color := clBlack; Pen.Width := 1; Pen.Style := psSolid; t := RcChart.Bottom + FTickLength + SMALL_MARGIN; Last := 0; for i := 0 to XAxis.Count-1 do begin P := PAxisInfo(XAxis[i]); Str := P^.Caption; w := TextWidth(Str); l := P^.Px - (w div 2); if (Last < l) then begin TextOut(l, t, Str); Last := l + w; end; MoveTo(P^.Px, P^.Py); LineTo(P^.Px, P^.Py + FTickLength + 1); end; end; end; procedure TNiceChart.DrawYAxis(ACanvas: TCanvas); var l, t, h, w: Integer; i: Integer; Str: string; P: PAxisInfo; origin, dummy: Integer; begin with ACanvas do begin if FShowYGrid then begin Pen.Style := psDot; Pen.Width := 1; if FMonochrome then Pen.Color := clBlack else Pen.Color := FGridColor; for i := 0 to YAxis.Count-1 do begin P := PAxisInfo(YAxis[i]); if (P^.Py <= RcChart.Top) or (P^.Py >= RcChart.Bottom) then Continue; MoveTo(P^.Px, P^.Py); LineTo(RcChart.Right, P^.Py); end; Pen.Color := clBlack; end; // Drawing the x axis here seems to be misplaced. But otherwise // it could be overpainted by a y grid line. if FShowXAxisLine then begin ChartToClient(0, 0, dummy, origin); if (origin > RcChart.Top) and (origin < RcChart.Bottom) then begin Pen.Style := psSolid; Pen.Color := clBlack; Pen.Width := 1; MoveTo(RcChart.Left, origin); LineTo(RcChart.Right, origin); end; end; Pen.Style := psSolid; Pen.Width := FAxisLineWidth; MoveTo(RcChart.Left, RcChart.Top); LineTo(RcChart.Left, RcChart.Bottom); h := RcChart.Bottom - RcChart.Top; l := FOuterMargin; Font.Assign(FNormalFont); Font.Style := [fsBold]; t := RcChart.Bottom - ((h - TextWidth(FAxisYTitle)) div 2); RotTextOut(ACanvas, l, t, 90, FAxisYTitle); Font.Assign(FNormalFont); Pen.Color := clBlack; Pen.Width := 1; Pen.Style := psSolid; l := RcChart.Left - FTickLength - SMALL_MARGIN; for i := 0 to YAxis.Count-1 do begin P := PAxisInfo(YAxis[i]); Str := P^.Caption; w := TextWidth(Str); h := TextHeight(Str); t := P^.Py - (h div 2); TextOut(l - w, t, Str); MoveTo(P^.Px - FTickLength, P^.Py); LineTo(P^.Px, P^.Py); end; end; end; procedure TNiceChart.DrawChart(ACanvas: TCanvas); var x: Integer; begin with ACanvas do begin Brush.Style := bsClear; Pen.Style := psSolid; Pen.Width := 1; MoveTo(RcChart.Left, RcChart.Top); LineTo(RcChart.Right, RcChart.Top); LineTo(RcChart.Right, RcChart.Bottom); end; for x := 0 to List.Count-1 do begin if (TNiceSeries(List[x]).FKind = skBar) then DrawSeries(ACanvas, x); end; for x := 0 to List.Count-1 do begin if (TNiceSeries(List[x]).FKind <> skBar) then DrawSeries(ACanvas, x); end; end; //-----------------------------------------------------------------------------// procedure MarkerRectangle(ACanvas: TCanvas; X, Y, Size: Integer); begin ACanvas.Rectangle(X-Size, Y-Size, X+Size, Y+Size); end; procedure MarkerCircle(ACanvas: TCanvas; X, Y, Size: Integer); begin ACanvas.Ellipse(X-Size, Y-Size, X+Size, Y+Size); end; procedure MarkerTriangle1(ACanvas: TCanvas; X, Y, Size: Integer); begin ACanvas.Polygon([Point(x, y-Size), Point(x+Size, y+Size), Point(x-Size, y+Size)]); end; procedure MarkerTriangle2(ACanvas: TCanvas; X, Y, Size: Integer); begin ACanvas.Polygon([Point(x+Size, y-Size), Point(x-Size, y-Size), Point(x, y+Size)]); end; procedure MarkerDiamond(ACanvas: TCanvas; X, Y, Size: Integer); begin ACanvas.Polygon([Point(x, y-Size), Point(x+Size, y), Point(x, y+Size), Point(x-Size, y)]); end; const Colors1: array [0..13] of TColor = ( clRed, clBlue, clGreen, clFuchsia, clNavy, clMaroon, clBlack, clOlive, clPurple, clTeal, clGray, clLime, clYellow, clAqua ); Colors2: array [0..13] of TColor = ( $0066C2FF, $005AFADA, $00F4C84D, $00B54DF4, $00669FFF, $00F44D5A, $0066E0FF, $0066FFFF, $00F44DAE, $006863FE, $004DF474, $00F4934D, clSilver, clGray ); Markers: array [0..4] of TMarkerProc = ( MarkerRectangle, MarkerCircle, MarkerTriangle1, MarkerTriangle2, MarkerDiamond); procedure TNiceChart.AutoColors(ACanvas: TCanvas; Index: Integer; Isbar: Boolean); var cl: TColor; Idx: Integer; Bmp: TBitmap; begin if FMonochrome then cl := clBlack else if FSoftColors then cl := Colors2[Index mod 14] else cl := Colors1[Index mod 14]; Marker := Markers[Index mod 5]; with ACanvas do begin Pen.Color := cl; Brush.Bitmap := nil; Brush.Style := bsSolid; if IsBar then Brush.Color := cl else Brush.Color := clWhite; if IsBar and FMonochrome then begin Idx := Index mod 16; if not Assigned(Brushes[Idx]) then begin Bmp := TBitmap.Create; Bmp.LoadFromResourceName(hInstance, Format('brush%.2d', [Idx+1])); Brushes[Idx] := Bmp; end; Brush.Bitmap := Brushes[Idx]; end; end; end; //-----------------------------------------------------------------------------// procedure TNiceChart.DrawSeries(ACanvas: TCanvas; Index: Integer); var x: Integer; P: PXYInfo; l, t, t2: Integer; Sr: TNiceSeries; Rc: TRect; begin Sr := TNiceSeries(List[Index]); AutoColors(ACanvas, Index, sr.FKind = skBar); with ACanvas do begin if (sr.FKind = skBar) then begin ClipToRect(ACanvas, RcChart, true); try Pen.Width := 1; for x := 0 to Sr.Values.Count-1 do begin P := PXYInfo(Sr.Values[x]); Rectangle(P^.Rc); end; finally ClipToRect(ACanvas, RcChart, false); end; end else begin ClipToRect(ACanvas, RcChart, true); try if (sr.FKind = skLine) then begin Pen.Width := sr.LineWidth; for x := 0 to Sr.Values.Count-1 do begin P := PXYInfo(Sr.Values[x]); if (x = 0) then MoveTo(P^.Px, P^.Py) else LineTo(P^.Px, P^.Py); end; end else if (sr.FKind = skSmooth) then begin Pen.Width := sr.LineWidth; sr.Spline.Draw(ACanvas); end; finally ClipToRect(ACanvas, RcChart, false); end; Pen.Width := 1; for x := 0 to Sr.Values.Count-1 do begin P := PXYInfo(Sr.Values[x]); if PtInRect(RcChart, Point(P^.Px, P^.Py)) then Marker(ACanvas, P^.Px, P^.Py, MarkSize); end; end; if FShowLegend then begin l := RcLegend.Left + FInnerMargin; t := RcLegend.Top + Sr.Top; if (sr.FKind = skBar) then begin Rc := Rect(l, t, l + FLegendItemSize, t + FLegendItemSize); InflateRect(Rc, -2, -2); Rectangle(Rc); end else begin Pen.Width := sr.LineWidth; t2 := t + (FLegendItemSize div 2); MoveTo(l, t2); LineTo(l + FLegendItemSize, t2); Pen.Width := 1; Marker(ACanvas, l + (FLegendItemSize div 2), t2, MarkSize); end; end; end; end; procedure TNiceChart.CalculateSeries; var x, y: Integer; Values: TList; P: PXYInfo; S: TBSpline; Vertex: TVertex; sr: TNiceSeries; bw, rw, bi, dx, l: Integer; begin if (List.Count = 0) or ChartEmpty then Exit; bi := 0; bw := 0; if (BarCount > 0) then bw := Round(BarWidth / (BarCount + 1)); for x := 0 to List.Count-1 do begin sr := TNiceSeries(List[x]); s := sr.Spline; s.Clear; Values := sr.Values; case sr.FKind of skBar: begin dx := Round(-(BarWidth / 2) + (bw / 2) + (bi * bw) + (bw * 0.1)); rw := Round(bw * 0.8); for y := 0 to Values.Count-1 do begin P := PXYInfo(Values[y]); ChartToClient(P^.X, P^.Y, P^.Px, P^.Py); l := P^.Px + dx; if (P^.Y < 0) then P^.Rc := Rect(l, YZero, l + rw, P^.Py) else P^.Rc := Rect(l, P^.Py, l + rw, YZero); end; Inc(bi); end; skLine: begin for y := 0 to Values.Count-1 do begin P := PXYInfo(Values[y]); ChartToClient(P^.X, P^.Y, P^.Px, P^.Py); P^.Rc := Rect(P^.Px-MarkSize, P^.Py-MarkSize, P^.Px+MarkSize, P^.Py+MarkSize); end; end; skSmooth: begin for y := 0 to Values.Count-1 do begin P := PXYInfo(Values[y]); ChartToClient(P^.X, P^.Y, P^.Px, P^.Py); P^.Rc := Rect(P^.Px-MarkSize, P^.Py-MarkSize, P^.Px+MarkSize, P^.Py+MarkSize); Vertex.X := P^.Px; Vertex.Y := P^.Py; s.AddPoint(Vertex); end; s.Interpolated := True; s.Fragments := s.NumberOfPoints * 20; end; end; end; end; procedure TNiceChart.ChartToClient(const AX, AY: Double; var X, Y: Integer); var i: Integer; begin if FAxisXOnePerValue then begin for i := 0 to XAxis.Count-1 do begin if (AX = PAxisInfo(XAxis[i])^.Value) then begin X := PAxisInfo(XAxis[i])^.Px; Break; end; end; end else X := FXTranslate.Base + Round((AX - FXTranslate.MinValue) * FXTranslate.Scale); Y := FYTranslate.Base - Round((AY - FYTranslate.MinValue) * FYTranslate.Scale); end; function TNiceChart.ClientToChart(const X, Y: Integer; var AX, AY: Double): Boolean; var i: Integer; n, d: Integer; begin Result := PtInRect(RcChart, Point(X, Y)); if Result then begin if FAxisXOnePerValue then begin n := MaxInt; for i := 0 to XAxis.Count-1 do begin d := Abs(X - PAxisInfo(XAxis[i])^.Px); if (d < n) then begin AX := PAxisInfo(XAxis[i])^.Value; n := d; end; end; end else AX := FXTranslate.MinValue + ((X - FXTranslate.Base) / FXTranslate.Scale); AY := FYTranslate.MinValue + ((FYTranslate.Base - Y) / FYTranslate.Scale); end; end; procedure TNiceChart.CreateHandle; begin inherited; Changed; end; function TNiceChart.CreateBitmap(AWidth, AHeight: Integer): TBitmap; var w, h: Integer; begin Calculate(AWidth, AHeight); if (RcLegend.Bottom > (AHeight - FOuterMargin)) then h := RcLegend.Bottom + FOuterMargin else h := AHeight; if ((RcChart.Right - RcChart.Left) < (RcChart.Bottom - RcChart.Top)) then w := AWidth + ((RcChart.Bottom - RcChart.Top) - (RCChart.Right - RcChart.Left)) else w := AWidth; if (AWidth <> w) or (AHeight <> h) then Calculate(w, h); Result := TBitmap.Create; Result.Width := w; Result.Height := h; InternalPaint(Result.Canvas); Calculate(ClientWidth, ClientHeight); end; procedure TNiceChart.CopyToClipboardAsBitmap; var bmp: TBitmap; begin bmp := CreateBitmap(ClientWidth, ClientHeight); try Clipboard.Assign(bmp); finally bmp.Free; end; end; procedure TNiceChart.ClipToRect(ACanvas: TCanvas; const ARect: TRect; AEnable: Boolean); begin {$IFDEF FPC} if AEnable then ACanvas.ClipRect := ARect; ACanvas.Clipping := AEnable; {$ELSE} if AEnable then begin if FClipRgn <> 0 then DeleteObject(FClipRgn); FClipRgn := CreateRectRgn(ARect.Left, ARect.Top, ARect.Right, ARect.Bottom); SelectClipRgn(ACanvas.Handle, FClipRgn); end else begin SelectClipRgn(ACanvas.Handle, HRGN(nil)); DeleteObject(FClipRgn); FClipRgn := 0; end; {$ENDIF} end; {$IFNDEF FPC} function TNiceChart.CreateMetafile: TMetafile; const InitWidth = 800; InitHeight = 600; var mc: TMetafileCanvas; AWidth, AHeight: Integer; begin AWidth := InitWidth; AHeight := InitHeight; Calculate(AWidth, AHeight); if (RcLegend.Bottom > (AHeight - FOuterMargin)) then AHeight := RcLegend.Bottom + FOuterMargin; if ((RcChart.Right - RcChart.Left) < (RcChart.Bottom - RcChart.Top)) then AWidth := AWidth + ((RcChart.Bottom - RcChart.Top) - (RCChart.Right - RcChart.Left)); if (AWidth <> InitWidth) or (AHeight <> InitHeight) then Calculate(AWidth, AHeight); Result := TMetafile.Create; Result.Width := AWidth; Result.Height := AHeight; mc := TMetafileCanvas.Create(Result, 0); InternalPaint(mc); mc.Free; Calculate(ClientWidth, ClientHeight); end; procedure TNiceChart.CopyToClipboard; var Wmf: TMetafile; begin Wmf := CreateMetafile; Clipboard.Assign(Wmf); Wmf.Free; end; {$ENDIF} {$IFDEF FPC} // Handle Lazarus' High-DPI scaling procedure TNiceChart.DoAutoAdjustLayout(const AMode: TLayoutAdjustmentPolicy; const AXProportion, AYProportion: Double); begin inherited DoAutoAdjustLayout(AMode, AXProportion, AYProportion); if AMode in [lapAutoAdjustWithoutHorizontalScrolling, lapAutoAdjustForDPI] then begin FOuterMargin := Round(FOuterMargin * AXProportion); FInnerMargin := Round(FInnerMargin * AXProportion); FSmallMargin := Round(FSmallMargin * AXProportion); FAxisDefSize := Round(FAxisDefSize * AXProportion); FLegendItemSize := Round(FLegendItemSize * AXProportion); end; end; procedure TNiceChart.FixDesignFontsPPI(const ADesignTimePPI: Integer); begin inherited; DoFixDesignFontPPI(FTitleFont, ADesignTimePPI); DoFixDesignFontPPI(FNormalFont, ADesignTimePPI); end; procedure TNiceChart.ScaleFontsPPI(const AToPPI: Integer; const AProportion: Double); begin inherited; DoScaleFontPPI(FTitleFont, AToPPI, AProportion); DoScaleFontPPI(FNormalFont, AToPPI, AProportion); end; {$ENDIF} end.