fix: langue imposee par slide, titres sans numerotation, meta Designer tronquee, finitions C5
This commit is contained in:
+115
-15
@@ -47,7 +47,11 @@ SYSTEM_PROMPT = (
|
||||
"d'important ; condenser légèrement si nécessaire.\n"
|
||||
"- Texte brut uniquement : aucun balisage Markdown (pas de **, __, #, `, "
|
||||
"ni tirets de liste dans les valeurs).\n"
|
||||
"- Français, ton affirmatif, guillemets typographiques évités.\n"
|
||||
"- LANGUE : conserve strictement la langue du contenu fourni (un plan "
|
||||
"anglais donne des champs 100 % anglais). Ces instructions sont en "
|
||||
"français mais ce n'est JAMAIS la langue du livrable. Ne traduis "
|
||||
"rien.\n"
|
||||
"- Ton affirmatif, guillemets typographiques évités.\n"
|
||||
"- Champs optionnels sans contenu correspondant : null.\n"
|
||||
"- Les nombres des champs numériques (start, end, x, y, numero) sont des "
|
||||
"entiers, pas des chaînes.\n"
|
||||
@@ -58,7 +62,22 @@ SYSTEM_PROMPT = (
|
||||
"ignore-les."
|
||||
)
|
||||
|
||||
# "SLIDE 12 — layout_name" (tiret cadratin, demi-cadratin ou simple)
|
||||
# FORMAT RÉEL du Designer (prompt_the_designer_v3) : le Markdown du
|
||||
# Narrator repris intégralement, avec une ligne "@layout: nom" ajoutée
|
||||
# sous chaque titre de slide, et des "@note:" facultatives.
|
||||
# ### 3. The CEO's 3 priorities
|
||||
# @layout: two_cols_text
|
||||
# @note: ...
|
||||
# <contenu>
|
||||
# On découpe donc sur les lignes @layout, et on déduit la position du
|
||||
# titre qui précède (### 3. …) ou, à défaut, de l'ordre d'apparition.
|
||||
LAYOUT_RE = re.compile(r"^[ \t]*@layout[ \t]*:[ \t]*([a-z][a-z0-9_]*)[ \t]*$",
|
||||
re.MULTILINE)
|
||||
# Titre de slide : "### 12. Titre" ou "### [OPT-EXEC] 5. Titre"
|
||||
TITLE_NUM_RE = re.compile(
|
||||
r"^[ \t]*#{1,6}[ \t]*(?:\[[^\]]+\][ \t]*)?(\d+)[.)][ \t]*",
|
||||
re.MULTILINE)
|
||||
# Compat ascendante : ancien format "SLIDE 12 — layout_name"
|
||||
SLIDE_RE = re.compile(
|
||||
r"^\s*SLIDE\s+(\d+)\s*[—–\-]+\s*([a-z][a-z0-9_]*)\s*$",
|
||||
re.MULTILINE)
|
||||
@@ -72,22 +91,60 @@ MD_PATTERNS = [
|
||||
|
||||
# ── Découpage du plan ────────────────────────────────────────────────────────
|
||||
|
||||
def _position_before(plan_md, idx, fallback):
|
||||
"""Numéro de la dernière ligne de titre '### N.' avant idx."""
|
||||
last = None
|
||||
for m in TITLE_NUM_RE.finditer(plan_md, 0, idx):
|
||||
last = m
|
||||
if last:
|
||||
try:
|
||||
return int(last.group(1))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return fallback
|
||||
|
||||
|
||||
def split_plan(plan_md):
|
||||
"""Découpe le plan Designer en slides.
|
||||
"""Découpe le plan annoté par le Designer en slides.
|
||||
Format principal : lignes '@layout: nom'. Compat : 'SLIDE N — nom'.
|
||||
Retourne (segments, errors) ; segment = {position, layout, content}."""
|
||||
matches = list(SLIDE_RE.finditer(plan_md))
|
||||
segments, errors = [], []
|
||||
if not matches:
|
||||
return [], ["Aucune ligne 'SLIDE N — layout' détectée dans le plan."]
|
||||
valid = set(known_layouts())
|
||||
|
||||
matches = list(LAYOUT_RE.finditer(plan_md))
|
||||
if matches:
|
||||
for i, m in enumerate(matches):
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(plan_md)
|
||||
start = m.start()
|
||||
end = (matches[i + 1].start() if i + 1 < len(matches)
|
||||
else len(plan_md))
|
||||
layout = m.group(1)
|
||||
pos = _position_before(plan_md, start, i + 1)
|
||||
seg = {"position": pos, "layout": layout,
|
||||
"content": plan_md[start:end].strip()}
|
||||
if layout not in valid:
|
||||
errors.append("Slide %d : layout '%s' sans schéma (connus"
|
||||
" : %s)" % (pos, layout,
|
||||
", ".join(sorted(valid))))
|
||||
continue
|
||||
segments.append(seg)
|
||||
# Positions dupliquées (titre absent) → renumérotation d'ordre
|
||||
seen = [s["position"] for s in segments]
|
||||
if len(set(seen)) != len(seen):
|
||||
for i, s in enumerate(segments, start=1):
|
||||
s["position"] = i
|
||||
return segments, errors
|
||||
|
||||
# Fallback : ancien format explicite
|
||||
matches = list(SLIDE_RE.finditer(plan_md))
|
||||
if not matches:
|
||||
return [], ["Aucune annotation '@layout: nom' (ni 'SLIDE N — "
|
||||
"layout') détectée dans le plan du Designer."]
|
||||
for i, m in enumerate(matches):
|
||||
end = (matches[i + 1].start() if i + 1 < len(matches)
|
||||
else len(plan_md))
|
||||
layout = m.group(2)
|
||||
seg = {
|
||||
"position": int(m.group(1)),
|
||||
"layout": layout,
|
||||
"content": plan_md[m.start():end].strip(),
|
||||
}
|
||||
seg = {"position": int(m.group(1)), "layout": layout,
|
||||
"content": plan_md[m.start():end].strip()}
|
||||
if layout not in valid:
|
||||
errors.append("Slide %d : layout '%s' sans schéma (connus : %s)"
|
||||
% (seg["position"], layout,
|
||||
@@ -121,7 +178,7 @@ def _post_with_retry(payload, api_key):
|
||||
% (MAX_RETRY, last_err))
|
||||
|
||||
|
||||
def encode_slide(content, layout, api_key, model=None):
|
||||
def encode_slide(content, layout, api_key, model=None, lang=None):
|
||||
"""Encode UNE slide. Retourne (dict_contenu, usage_dict).
|
||||
Lève RuntimeError/ValueError en cas d'échec (isolé par l'appelant)."""
|
||||
schema = get_schema(layout)
|
||||
@@ -131,7 +188,9 @@ def encode_slide(content, layout, api_key, model=None):
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content":
|
||||
"Layout : %s\n\nContenu de la slide :\n%s"
|
||||
("LANGUE DU DECK : %s — tous les champs texte restent "
|
||||
"dans cette langue.\n" % lang if lang else "")
|
||||
+ "Layout : %s\n\nContenu de la slide :\n%s"
|
||||
% (layout, content)},
|
||||
],
|
||||
"response_format": {
|
||||
@@ -181,6 +240,41 @@ def _accumulate(total, usage):
|
||||
return total
|
||||
|
||||
|
||||
def detect_lang(text):
|
||||
"""Heuristique stopwords : 'anglais' / 'français' / None (mixte)."""
|
||||
t = " %s " % re.sub(r"\s+", " ", text.lower())
|
||||
en = sum(t.count(w) for w in (" the ", " and ", " of ", " to ",
|
||||
" is ", " for ", " with "))
|
||||
fr = sum(t.count(w) for w in (" le ", " la ", " les ", " des ",
|
||||
" est ", " pour ", " avec ", " une "))
|
||||
if en > fr * 1.5:
|
||||
return "anglais"
|
||||
if fr > en * 1.5:
|
||||
return "français"
|
||||
return None
|
||||
|
||||
|
||||
TITLE_PREFIX_RE = re.compile(r"^\s*(?:\[[^\]]+\]\s*)?\d+[.)]\s*")
|
||||
|
||||
|
||||
def strip_title_number(content):
|
||||
"""Retire la numérotation Markdown résiduelle du titre ('13. X' → 'X')."""
|
||||
t = content.get("titre")
|
||||
if isinstance(t, str):
|
||||
content["titre"] = TITLE_PREFIX_RE.sub("", t)
|
||||
return content
|
||||
|
||||
|
||||
def trim_trailing_meta(segments):
|
||||
"""Le DERNIER segment court jusqu'à la fin du texte : il peut embarquer
|
||||
les commentaires de conclusion du Designer (après un '---'). On coupe."""
|
||||
if segments:
|
||||
parts = segments[-1]["content"].split("\n---")
|
||||
if parts[0].count("\n") >= 1:
|
||||
segments[-1]["content"] = parts[0].rstrip()
|
||||
return segments
|
||||
|
||||
|
||||
# ── Orchestration ────────────────────────────────────────────────────────────
|
||||
|
||||
def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
@@ -190,6 +284,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
progress : callable(str) pour l'affichage (info du facilitator).
|
||||
Retourne (data, usage, errors) :
|
||||
data = {"titre_presentation": str|None, "slides": [...]}
|
||||
La langue du plan est détectée et imposée à chaque slide (anti-dérive).
|
||||
usage = tokens cumulés {"prompt_tokens","completion_tokens","total_tokens"}
|
||||
errors = liste de messages (slides en échec — absentes de data).
|
||||
"""
|
||||
@@ -198,6 +293,10 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
progress(msg)
|
||||
|
||||
segments, errors = split_plan(plan_md)
|
||||
segments = trim_trailing_meta(segments)
|
||||
lang = detect_lang(plan_md)
|
||||
if lang:
|
||||
say("Langue du deck : %s (imposée à chaque slide)" % lang)
|
||||
if only_positions is not None:
|
||||
wanted = set(int(p) for p in only_positions)
|
||||
segments = [s for s in segments if s["position"] in wanted]
|
||||
@@ -207,7 +306,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
say("Slide %d (%s)..." % (seg["position"], seg["layout"]))
|
||||
try:
|
||||
content, u = encode_slide(seg["content"], seg["layout"],
|
||||
api_key, model)
|
||||
api_key, model, lang=lang)
|
||||
except (RuntimeError, ValueError, KeyError,
|
||||
json.JSONDecodeError) as e:
|
||||
errors.append("Slide %d (%s) : %s"
|
||||
@@ -215,6 +314,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
continue
|
||||
_accumulate(usage, u)
|
||||
content = strip_md(drop_nulls(content))
|
||||
content = strip_title_number(content)
|
||||
if seg["layout"] == "cover_split" and titre_presentation is None:
|
||||
titre_presentation = content.get("titre")
|
||||
slide = {"position": seg["position"], "layout": seg["layout"]}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
patch_prompt_designer_meta.py — Le Designer ne conclut plus.
|
||||
Bug : ses commentaires de synthèse (« Points clés de la proposition… »)
|
||||
après la dernière slide fuyaient dans le deck (slide 27 du deck Sisley).
|
||||
Défense en profondeur : encoder_schema v3 tronque déjà après '---' ;
|
||||
cette règle prompt tarit la source.
|
||||
Usage : python3 patch_prompt_designer_meta.py
|
||||
puis prompt_injection_v2.py + recoller le Designer dans Mistral Studio.
|
||||
"""
|
||||
import shutil, sys
|
||||
from pathlib import Path
|
||||
|
||||
BLOC = """- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
|
||||
slide. AUCUN commentaire, bilan, synthèse ou justification après —
|
||||
tout texte postérieur serait transcrit dans le deck comme du contenu.
|
||||
"""
|
||||
p = Path("prompt_the_designer_v3.md")
|
||||
if not p.exists():
|
||||
print(" ! prompt_the_designer_v3.md introuvable"); sys.exit(1)
|
||||
c = p.read_text(encoding="utf-8")
|
||||
if "se termine à la DERNIÈRE ligne" in c:
|
||||
print(" = déjà à jour"); sys.exit(0)
|
||||
anchor = "- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`."
|
||||
if c.count(anchor) != 1:
|
||||
print(" ! ancre introuvable/non unique"); sys.exit(1)
|
||||
shutil.copy2(p, str(p) + ".bak-meta")
|
||||
p.write_text(c.replace(anchor, anchor + "\n" + BLOC), encoding="utf-8")
|
||||
print(" + règle anti-conclusion insérée — propager avec prompt_injection_v2.py")
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
patch_render_engine_c5fin.py — Finitions C5 (2 micro-fixes visuels)
|
||||
===================================================================
|
||||
F1. bar_chart horizontal : PowerPoint affiche les catégories de BAS
|
||||
en HAUT en BAR_CLUSTERED — la première catégorie du YAML se
|
||||
retrouvait en bas. On inverse catégories ET valeurs quand
|
||||
horizontal, pour un ordre de lecture naturel (haut → bas).
|
||||
F2. waterfall : les libellés de catégories longs (Décommissionnements…)
|
||||
wrappaient en orphelin. Réduction automatique à 9 pt au-delà de
|
||||
14 caractères.
|
||||
|
||||
Usage (dossier du pipeline) : python3 patches/patch_render_engine_c5fin.py
|
||||
Vérifie les ancres, écrit .bak-c5fin, compile, idempotent.
|
||||
"""
|
||||
import py_compile
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TARGET = Path("render_engine_v2.py")
|
||||
|
||||
P1_OLD = """ cd = CategoryChartData()
|
||||
cd.categories = cats
|
||||
for s in series:
|
||||
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
|
||||
vals += [0.0] * (len(cats) - len(vals))
|
||||
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
|
||||
x, y, w, h = self._chart_zone(d)
|
||||
horiz = bool(d.get("horizontal"))
|
||||
"""
|
||||
P1_NEW = """ horiz = bool(d.get("horizontal"))
|
||||
if horiz:
|
||||
# BAR_CLUSTERED trace de bas en haut : on inverse pour un
|
||||
# ordre de lecture naturel (1re catégorie du YAML en haut).
|
||||
cats = cats[::-1]
|
||||
cd = CategoryChartData()
|
||||
cd.categories = cats
|
||||
for s in series:
|
||||
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
|
||||
vals += [0.0] * (len(cats) - len(vals))
|
||||
if horiz:
|
||||
vals = vals[::-1]
|
||||
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
|
||||
x, y, w, h = self._chart_zone(d)
|
||||
"""
|
||||
|
||||
P2_OLD = """ def cat_label(i, label):
|
||||
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
|
||||
slot - 0.1, 1.05, label, size=10,
|
||||
color=self.C["slate"], align=PP_ALIGN.CENTER)
|
||||
"""
|
||||
P2_NEW = """ def cat_label(i, label):
|
||||
sz = 9 if len(str(label)) > 14 else 10
|
||||
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
|
||||
slot - 0.1, 1.05, label, size=sz,
|
||||
color=self.C["slate"], align=PP_ALIGN.CENTER)
|
||||
"""
|
||||
|
||||
|
||||
def fail(m):
|
||||
print(" ! %s" % m)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if not TARGET.exists():
|
||||
fail("%s introuvable." % TARGET)
|
||||
c = TARGET.read_text(encoding="utf-8")
|
||||
if "cats = cats[::-1]" in c:
|
||||
fail("Déjà patché — rien à faire.")
|
||||
for i, old in enumerate((P1_OLD, P2_OLD), 1):
|
||||
if c.count(old) != 1:
|
||||
fail("Ancre F%d introuvable ou non unique." % i)
|
||||
shutil.copy2(TARGET, str(TARGET) + ".bak-c5fin")
|
||||
c = c.replace(P1_OLD, P1_NEW).replace(P2_OLD, P2_NEW)
|
||||
TARGET.write_text(c, encoding="utf-8")
|
||||
try:
|
||||
py_compile.compile(str(TARGET), doraise=True)
|
||||
except py_compile.PyCompileError as e:
|
||||
shutil.copy2(str(TARGET) + ".bak-c5fin", TARGET)
|
||||
fail("Compilation : %s" % e)
|
||||
print(" + F1 (ordre bar horizontal) + F2 (labels waterfall) OK.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -338,11 +338,27 @@ annoté complet mis à jour, pas seulement la partie modifiée.
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue.
|
||||
- Tu reprends le Markdown INTÉGRALEMENT — aucune perte de contenu.
|
||||
- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`.
|
||||
- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
|
||||
slide. AUCUN commentaire, bilan, synthèse ou justification après —
|
||||
tout texte postérieur serait transcrit dans le deck comme du contenu.
|
||||
|
||||
- Tu ne produis ni YAML, ni plan séparé, ni JSON.
|
||||
- `cover_split` en premier, `end_slide` en dernier, toujours.
|
||||
|
||||
|
||||
@@ -101,11 +101,27 @@ annoté complet mis à jour, pas seulement la partie modifiée.
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue.
|
||||
- Tu reprends le Markdown INTÉGRALEMENT — aucune perte de contenu.
|
||||
- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`.
|
||||
- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
|
||||
slide. AUCUN commentaire, bilan, synthèse ou justification après —
|
||||
tout texte postérieur serait transcrit dans le deck comme du contenu.
|
||||
|
||||
- Tu ne produis ni YAML, ni plan séparé, ni JSON.
|
||||
- `cover_split` en premier, `end_slide` en dernier, toujours.
|
||||
|
||||
|
||||
@@ -451,13 +451,6 @@ bullets:
|
||||
|
||||
---
|
||||
|
||||
### Notes du présentateur
|
||||
|
||||
Si une slide du plan contient une ligne « Notes : ... », transcris son
|
||||
contenu dans un champ `notes` de la slide YAML (chaîne simple). Les
|
||||
lignes de justification du Designer (commençant par →) ne sont NI du
|
||||
contenu NI des notes : ignore-les.
|
||||
|
||||
## GESTION DE LA LONGUEUR
|
||||
|
||||
Si la présentation dépasse 8 slides, tu travailles en blocs :
|
||||
@@ -469,6 +462,18 @@ Le YAML de chaque bloc doit être **syntaxiquement valide indépendamment** —
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue — aucun autre nom n'existe
|
||||
|
||||
@@ -81,6 +81,18 @@ Le YAML de chaque bloc doit être **syntaxiquement valide indépendamment** —
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue — aucun autre nom n'existe
|
||||
|
||||
+9
-2
@@ -1711,14 +1711,20 @@ class RenderEngineV2:
|
||||
if len(series) > 3:
|
||||
print(f" ~ bar_chart : {len(series)} séries → 3 (borne)")
|
||||
series = series[:3]
|
||||
horiz = bool(d.get("horizontal"))
|
||||
if horiz:
|
||||
# BAR_CLUSTERED trace de bas en haut : on inverse pour un
|
||||
# ordre de lecture naturel (1re catégorie du YAML en haut).
|
||||
cats = cats[::-1]
|
||||
cd = CategoryChartData()
|
||||
cd.categories = cats
|
||||
for s in series:
|
||||
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
|
||||
vals += [0.0] * (len(cats) - len(vals))
|
||||
if horiz:
|
||||
vals = vals[::-1]
|
||||
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
|
||||
x, y, w, h = self._chart_zone(d)
|
||||
horiz = bool(d.get("horizontal"))
|
||||
ctype = (XL_CHART_TYPE.BAR_CLUSTERED if horiz
|
||||
else XL_CHART_TYPE.COLUMN_CLUSTERED)
|
||||
ch = self._chart_base(slide, x, y, w, h, ctype, cd)
|
||||
@@ -1929,8 +1935,9 @@ class RenderEngineV2:
|
||||
align=PP_ALIGN.CENTER, fit=False)
|
||||
|
||||
def cat_label(i, label):
|
||||
sz = 9 if len(str(label)) > 14 else 10
|
||||
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
|
||||
slot - 0.1, 1.05, label, size=10,
|
||||
slot - 0.1, 1.05, label, size=sz,
|
||||
color=self.C["slate"], align=PP_ALIGN.CENTER)
|
||||
|
||||
def connector(x1, x2, level):
|
||||
|
||||
Reference in New Issue
Block a user