HotPDF n'a pas d'objet graphique (chart). Il n'y a pas de TPDFChart, pas de AddBarSeries, ni de méthode acceptant un tableau de nombres et renvoyant un graphique rendu. Ce qu'il vous offre est un canevas de page, avec le même vocabulaire de base que chaque modèle de dessin PDF utilise : rectangles, lignes, cercles, remplissages, contours et texte placé à des coordonnées précises. Par conséquent, les graphiques dans les documents HotPDF sont construits par vous-même, plutôt que générés sur demande. Cela semble représenter plus de travail que ce n'est le cas en réalité. Une fois que vous avez écrit le code de calcul des coordonnées, un diagramme à barres est une boucle de rectangles, un graphique linéaire est une ligne brisée, et un diagramme à secteurs est un ensemble de secteurs en arc, vous contrôlez chaque pixel du résultat
C'est important car la première alternative qui vient à l'esprit — pixéliser le contrôle de graphique d'écran en bitmap et le coller sur la page — produit un graphique figé à la résolution de l'écran, flou à l'impression, et fait gonfler la taille du fichier. Dessiner des graphiques à l'aide des primitives vectorielles de HotPDF maintient la sortie nette à n'importe quel facteur d'échelle et DPI d'impression, les barres et axes étant de véritables opérateurs de tracés PDF et non des pixels. Le prix à payer est que la mise en page relève de votre responsabilité. Les points clés résident en quelques aspects : l'inversion des coordonnées sur laquelle tout le monde trébuche, un exemple de graphique à barres, l'astuce des lignes de graphiques et le calcul des radians pour les portions de camembert
La seule partie difficile est le système de coordonnées
Les graphiques d'écran placent l'origine dans le coin supérieur gauche, Y augmentant vers le bas. Le PDF fait exactement le contraire. L'origine est située dans le coin inférieur gauche de la page, et Y augmente vers le haut, mesuré en points (1/72 de pouce). Chaque appel de dessin dans HotPDF — TextOut, Rectangle, MoveTo, LineTo, Circle — utilise la convention de l'origine en bas à gauche avec Y vers le haut. Si vous importez des habitudes de graphisme sur écran, votre premier diagramme s'affichera à l'envers et dépassera le bas de la page
Par conséquent, le véritable travail dans tout graphique consiste à effectuer un mappage : convertir les valeurs des données en coordonnées Y respectant la règle de croissance de Y vers le haut. Déterminez un rectangle de dessin (les quatre chiffres de la zone du graphique), puis mappez la valeur minimale des données sur le bord inférieur et la valeur maximale sur le bord supérieur. Pour une valeur V comprise entre 0 et MaxValue, le bord supérieur de la barre est , la barre croissant à partir de PlotBottom. Écrivez cette expression correctement, et le reste n'est que de la comptabilité. L'enregistrement d'aide suivant conserve la géométrie du graphique et effectue la conversion, de sorte que le code de dessin n'a pas à manipuler l'arithmétique brute deux fois :PlotBottom + (V / MaxValue) * PlotHeight
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // PDF points, bottom-left origin
MaxValue: Single; // top of the value scale
end;
// Map a data value to its Y coordinate inside the plot, Y growing upward.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
Il y a un choix caché dans MaxValue. Si vous le configurez sur la valeur maximale exacte des données, la barre la plus haute touchera le bord supérieur de la zone de dessin et semblera tronquée. Arrondissez cette valeur à l'entier supérieur au maximum des données, par exemple le multiple supérieur de 10 ou 100, afin que le graphique ait de la marge et que les étiquettes des lignes de grille s'affichent sous forme d'entiers plutôt que de pics atteints par les données
Le diagramme à barres est une boucle de rectangles
Une fois la correspondance définie, le graphique à barres s'écrit tout seul. Divisez la largeur de dessin équitablement par le nombre de catégories, laissez un espace entre les barres pour éviter qu'elles ne se touchent, puis dessinez chaque barre sous forme de rectangle rempli dont la hauteur provient de ValueToY. Le Rectangle de HotPDF accepte le coin inférieur gauche avec la largeur et la hauteur, ce qui correspond parfaitement à une barre croissant vers le haut depuis la ligne de base. Définissez d'abord la couleur de remplissage, tracez le chemin, puis appelez Fill pour dessiner. Les étiquettes de catégories sont placées sous la ligne de base, et les étiquettes de valeurs au-dessus des barres :
procedure DrawBarChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single; const Labels: array of string);
var
I, Count: Integer;
SlotW, BarW, BarX, BarH, Gap: Single;
begin
Count := Length(Values);
SlotW := Plot.Width / Count;
Gap := SlotW * 0.25; // quarter-slot gap on each side
BarW := SlotW - Gap;
// Baseline (the X axis) along the bottom of the plot.
Page.SetLineWidth(1.0);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left + Plot.Width, Plot.Bottom);
Page.Stroke;
Page.SetFont('Arial', [], 9);
for I := 0 to Count - 1 do
begin
BarX := Plot.Left + I * SlotW + Gap / 2;
BarH := ValueToY(Plot, Values[I]) - Plot.Bottom;
Page.SetRGBFillColor(RGB(56, 110, 219));
Page.Rectangle(BarX, Plot.Bottom, BarW, BarH); // X, Y, Width, Height
Page.Fill;
// Category label below the baseline, value above the bar.
Page.SetRGBFillColor(clBlack);
Page.TextOut(BarX, Plot.Bottom - 14, 0, Labels[I]);
Page.TextOut(BarX, Plot.Bottom + BarH + 4, 0,
FormatFloat('0', Values[I]));
end;
end;
Deux stockages de métadonnées contradictoires
Le système de coordonnées a son origine dans le coin inférieur gauche
Le graphique n'est pas complet tant que le lecteur ne comprend pas la signification des barres, et c'est précisément le rôle des axes. L'axe vertical est une ligne tracée le long du bord gauche de la zone de dessin, accompagnée de plusieurs graduations et de leurs valeurs. Réutilisez ValueToY pour que les graduations tombent sur la même échelle que les barres, sinon les barres et leurs lignes de grille ne correspondront pas, et le graphique mentira silencieusement :
procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
TickCount: Integer);
var
I: Integer;
TickV, TickY: Single;
begin
Page.SetLineWidth(1.0);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left, Plot.Bottom + Plot.Height);
Page.Stroke;
Page.SetFont('Arial', [], 8);
for I := 0 to TickCount do
begin
TickV := (Plot.MaxValue / TickCount) * I;
TickY := ValueToY(Plot, TickV);
Page.MoveTo(Plot.Left - 4, TickY); // short tick outside the axis
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Les étiquettes sont le domaine où les graphiques échouent le plus souvent en production, la cause de l'échec étant toujours la même : le texte approprié à l'écran dépasse son espace dans le PDF. Les longs noms de catégories entrent en collision avec leurs voisins, les noms de mois localisés comme "septembre" ou "Dezember" sont plus larges que votre anglais "Sep" utilisé pour les tests. Il n'y a pas d'ajustement automatique de taille pour vous sauver ici, laissez donc une marge suffisante sous la ligne de base, réduisez la police d'un point ou deux pour les ensembles de catégories denses, et faites pivoter les noms s'ils sont vraiment longs. Le troisième paramètre de TextOut accepte un angle, passer 90 permettant de redresser les étiquettes pour libérer de l'espace sans chevauchement. Testez la mise en page avec les étiquettes attendues les plus larges plutôt que les plus courtes avant d'exporter pour publication
Graphique linéaire : une ligne brisée reliant des points cartographiés
Le graphique linéaire réutilise l'ensemble du mappage de valeurs, changeant uniquement la façon de connecter les points. Au lieu d'avoir un rectangle par catégorie, on parcourt les données une fois, on convertit chaque valeur en (X, Y) avec ValueToY, puis on connecte les points avec un MoveTo suivi de LineTo, pour enfin tracer le contour. Le premier point ouvre le tracé ; chaque point suivant le prolonge :
procedure DrawLineChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single);
var
I, Count: Integer;
StepX, X, Y: Single;
begin
Count := Length(Values);
if Count < 2 then Exit;
StepX := Plot.Width / (Count - 1);
Page.SetLineWidth(1.5);
Page.SetRGBStrokeColor(RGB(214, 92, 36));
for I := 0 to Count - 1 do
begin
X := Plot.Left + I * StepX;
Y := ValueToY(Plot, Values[I]);
if I = 0 then
Page.MoveTo(X, Y) // open the path at the first point
else
Page.LineTo(X, Y); // extend it through every later point
end;
Page.Stroke; // one stroke paints the whole polyline
end;
Notez la différence d'espacement. Le graphique à barres divise la largeur par le nombre de barres car chaque barre occupe un emplacement. Le graphique linéaire divise par le nombre d'intervalles (Count - 1) car le premier et le dernier point se situent aux bords du dessin, la ligne traversant les intervalles intermédiaires. Confondre les deux est une cause fréquente de décalage d'un demi-emplacement du graphique linéaire par rapport au graphique à barres sur lequel il doit se superposer. Si vous souhaitez marquer chaque point de données, dessinez un petit Circle et appliquez Fill à chaque (X, Y) après avoir tracé le contour de la ligne
Diagramme circulaire : arcs ou simples secteurs
Les secteurs circulaires sont la seule forme nécessitant des fonctions trigonométriques, car un secteur est composé de deux rayons et d'un arc. La version conforme balaie l'arc en suivant de petits segments de ligne le long de la circonférence, approchant la courbe de manière imperceptible pour le lecteur. L'angle de balayage de chaque secteur correspond à sa part du total, soit (Value / Total) * 2π, l'angle de fonctionnement étant cumulé au fur et à mesure de la progression :
procedure DrawPieChart(Page: THPDFPage; CX, CY, Radius: Single;
const Values: array of Single; const Colors: array of TColor);
var
I, Step, Steps: Integer;
Total, Start, Sweep, A: Single;
begin
Total := 0;
for I := 0 to High(Values) do Total := Total + Values[I];
Start := 0;
for I := 0 to High(Values) do
begin
Sweep := (Values[I] / Total) * 2 * Pi;
Steps := Round(Sweep / (Pi / 90)) + 1; // ~2 degrees per segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // wedge apex at the center
for Step := 0 to Steps do
begin
A := Start + Sweep * (Step / Steps);
Page.LineTo(CX + Radius * Cos(A), CY + Radius * Sin(A));
end;
Page.LineTo(CX, CY); // close back to the center
Page.Fill;
Start := Start + Sweep; // advance to the next slice
end;
end;
Copier trois pages d'un PDF de 40 pages prend deux minutes, ce n'est pas un problème d'optimisation des performances. C'est le signe que le mauvais chemin d'API a été utilisé. Lorsque j'ai vu ce temps pour la première fois sur un exemple de copie de page de HotPDF Component, mon instinct a été de regarder d'abord la structure du document, puis le code. Il s'avère que cet ordre est important
Rien de tout cela ne dépend de l'installation d'une bibliothèque de graphiques, ce qui constitue un avantage silencieux du dessin direct d'éléments de base. Les appels de dessin sur canevas positionnant les logos ou les cadres de signature peuvent également construire ces graphiques, et le même servant à légender les champs de formulaire peut étiqueter les axes. En encapsulant la géométrie du dessin dans un enregistrement et en mappant les valeurs vers Y d'un coup, un diagramme à barres, linéaire ou circulaire se résume à une courte routine utilisant TextOut, Rectangle et LineTo insérable dans n'importe quel rapport. Les appels Rectangle, CircleMoveTo, LineTo, Circle, Fill, Stroke et TextOut utilisés ici font partie du composant HotPDF pour Delphi et C++Builder