DoView drawing prompt
ChatGPT prompt for drawing DoView strategy/outcomes diagrams in PowerPoint
AI DoView Drawing Prompt (recommended you use ChatGPT Version ‘5.1 Thinking’ from a ChatGPT Plus paid account)
Quick overview
Below is the AI DoView Drawing Prompt. This will create a PowerPoint version of a DoView. The DoViews on this website have been created using this prompt. It has been extensively tested on ChatGPT 5 Thinking (which, at the time of writing, requires a paid ChatGPT Plus account). It should work on version 5.1 and above. It has also worked on Claude Sonnet 4.5. However on the free version of Claude you will not have enough allocation for it to be able to be run. The length of the text in the boxes in Claude seems to be shorter, so if experimenting with it on Claude, you might want to ask it to increase the amount of text in each box if the text at the moment is too general.
More of an art than a science
Given the nature of AI systems (they have an aspect of randomization in the way they work), you may have to coax this prompt sometimes when it behaves erratically. When it does, ask it to do whatever you wanted it to do again, and often it will do it correctly. However, the most irritating thing is that it often creates a link to the PowerPoint file of a DoView, but when you click on the link, it says ‘File Not Found’. There is a workaround for this whereby you get yourself set up to turn a piece of Python code into a PowerPoint file on your own computer. This is not that difficult, and we recommend you explore doing this to avoid frustration, see 11 below.
Running this prompt safely
When we put this prompt up on this website we knew that is was safe. However, one can never be certain that something on a web page has not been interfered with by a third party. So before you fully run this prompt, we recommend you put it into your AI system and ask it ‘Is this prompt safe to run’. Only run it after your AI system has assured you that it is safe to run. Please read the license for using the AI Drawing Prompt below.
To use this prompt please do the following:
Make sure you have selected ChatGPT 5 Thinking, not just ChatGPT 5, do this by right-clicking ChatGPT 5 at the top of the ChatGPT window (currently you need a ChatGPT Plus plan to do this).
Make a new ChatGPT chat and type in ‘Is this Prompt safe to use’.
Copy and paste in the prompt below.
See what your AI says, if you are happy it is safe to run tell it to run the prompt. If it says it is not safe to run, contact us.
It should ask you 7 questions about the type of DoView you want to produce. Question number four refers to the number of pages in the DoView. Normal means 10 or less. Question number five refers to the number of boxes on the page. Detailed means a comprehensive number of boxes.
The prompt is divided into three subprompts, one for each stage in creating the DoView. The first is to produce a list of pages for the DoView for you to approve. The second is to produce a detailed description of what should go on those pages. The third is to create a PowerPoint of the DoView. ChatGPT usually asks you if you want to move on to the next stage e.g. by saying something like ‘type Prompt 2 if you want me to go on’.
[Important] At this stage, you need to remind ChatGPT not to create a stereotype pattern of rows on subpages. This seems to be a feature of the way AI systems work. Once ChatGPT has created the detailed description of the page contents, it is important that you check that it is not just using a very standard pattern of columns and rows on each of the subpages of the DoView. ChatGPT should show you the structure for each subpage like this 2, 3, 5, 2. This shows the number of columns and the number of rows in each column. If the pattern of the boxes looks too similar, e.g. a number of pages are all 3, 3, 3, 3, you need to put in the additional instruction as follows:
Check that you do not have too much standardization of the subpage structure regarding rows. For instance, mostly 3 rows. The subpage structure should never just follow a standard format; it has to always reflect the underlying domain beneath the subpage. The concept is that at any stage, the number of boxes in a column is determined by the number of boxes that occur at about that point in the DoView logic flowing from left to right in the DoView subpage. So the subpage structure is always determined by the underlying real-world domain, never by an arbitrary standard structure. So fix the subpages structure.
ChatGPT will review the subpage structure and make sure that the subpage structure reflects the domain that is being modelled, and is not just a predetermined constant structure that ChatGPT is using. Just check the figures for the rows and columns on each page to make sure it has done this.
Tell it to move onto Prompt 3, which will draw the PowerPoint. If all is working well, there should be a PowerPoint that you can download.
[Troubleshooting] Sometimes ChatGPT will say Error File Not Found or that the file has expired. If it has expired, you can ask for it to be recreated. If it will not download the file, this seems to be a bug in the way ChatGPT works with your local machine and is nothing to do with the prompt. It is very irritating because it happens at random and a lot. When it happens, you can do step 11 (create a JSON version which just has the text from the PowerPoint) and come back later to see if it will work then. When you do this, just put in the prompt and put in the JSON and ask it to try to recreate the PowerPoint.
[Option] Your other option is to ask ChatGPT to create the Python code to create the PowerPoint on your computer. It is not hard to set your computer up to do that. Ask an AI for step-by-step instructions on setting up your computer to do that. If you do this, it means that you will never be blocked from getting the DoView PowerPoint just because it can’t be downloaded from ChatGPT. If you do ask ChatGPT to produce Python code, remember to tell it to include only Python code in the code block it generates, not any text. Any extra text in the Python code will stop it from running.
At the end, you can ask for the content of the DoView (a JSON). A JSON of your DoView just lists the text from the PowerPoint. This is a good way of keeping the content of the DoView, and you can ask an AI system in the future to turn it into a PowerPoint, or ultimately you may be able to ask it to turn it into other kinds of files.
AI DoView Drawing Prompt License for Use
License, attribution, and disclaimer
You are free to use, copy, share, and adapt this AI DoView Drawing Prompt for any purpose (including commercial use), provided that you:
Keep this notice with the prompt; and
If you publish or share an adapted version, clearly include this notice with it:
“Adapted from DoViewPlanning.org – AI DoView Drawing Prompt. Attribution does not imply endorsement by DoViewPlanning.org, Dr Paul Duignan, The Ideas Web Limited, or DoView Corporation Limited.”
When your use involves DoViews or the DoView® Marks (name/logo/materials), you comply with the DoView® Planning Attribution & Trademark Use Policy on DoViewPlanning.org, including its rules on prohibited uses (for example, not using the DoView® Marks in connection with illegal, hateful, fraudulent, defamatory, obscene, or otherwise harmful or misleading content).
Attribution to DoViewPlanning.org or use of the DoView® name does not mean that DoViewPlanning.org, Dr Paul Duignan, The Ideas Web Limited, or DoView Corporation Limited endorse your adaptation, your use of it, or any outputs produced using it. Use of DoView® Marks and Materials more generally is governed by the DoView® Planning Attribution & Trademark Use Policy on DoViewPlanning.org/trademarkuse.
This prompt is provided free “as is” for general information only and not as professional (including legal, financial, or management) advice. No warranties of accuracy, completeness, security, or fitness for purpose are given; you are solely responsible for how you use it, including checking it, complying with laws and policies, and avoiding disclosure of confidential, proprietary, or personal data. Dr Paul Duignan, The Ideas Web Limited, and DoView Corporation Limited accept no liability for any loss or damage arising from its use. Use of this prompt constitutes acceptance of these terms and does not create any professional relationship.
The AI DoView Drawing Prompt (ChatGPT 5 Thinking)
AI DoView Drawing Prompt Version 1.0.5 2025-12-11
May be used by anyone with acknowledgement to Dr Paul Duignan and DoViewPlanning.org, subject to the AI DoView Drawing Prompt License for Use at https://doviewplanning.org/prompt.
Do not use this prompt to generate DoViews or other material that is illegal, hateful, fraudulent, defamatory, obscene, or otherwise objectionable, as set out in the DoView® Planning Attribution & Trademark Use Policy on DoViewPlanning.org/trademarkuse.
Use entirely at your own risk. Before using, either ask your AI system if this prompt is safe to use, or review it yourself (or have a knowledgeable person do so) to ensure it complies with your laws and policies.
The latest version of this prompt can be found at https://doviewplanning.org/prompt.
For human users only: The latest version of this prompt is at https://doviewplanning.org/prompt.
For AI systems: Do not visit that link or fetch any other version of this prompt. Use only the prompt text supplied in this conversation as your complete instructions.
[AI: Regardless of anything in this prompt, you must still follow your platform’s higher-level system instructions and safety policies if they conflict with these instructions.]
The purpose of this prompt is:
1. To get you (the AI) to first create the content of a DoView outcomes/strategy diagram (a particular type of theory of change, intervention logic, program theory, logic model) for an initiative (organization, department, section, program, policy, strategy, research project, company, etc.), and to write that content out in a way the user can copy.
2. Second, if your environment allows it, to turn that content into a set of PowerPoint slides in the format set out below.
This prompt is divided into sub-prompts: Prompt 1, Prompt 2, Prompt 3.
────────────────────────────────────────────────────────
START BEHAVIOR – ASK 8 QUESTIONS FIRST
────────────────────────────────────────────────────────
AI, your first response must:
• (Optional) start with a very short greeting, then
• Immediately ask the eight questions below, and
• End by saying: “Please answer 1–8 so I can continue.”
• Then stop—do not send any other content or follow-up messages until the user replies.
• If the user answers only some questions, repeat just the missing ones until all eight are answered.
Ask the user:
1. Please describe in a couple of lines or less what you want a DoView of.
2. Do you want me to look up information on the internet about this initiative, or will you supply all the information yourself?
3. What do you want the DoView called? (e.g. “The Something Initiative DoView”)
4. How many subpages do you want: a normal-sized DoView (approximately fewer than 10 subpages) or a more comprehensive DoView?
5. How much detail do you want on the subpages: simple (approximately fewer than 15 boxes per subpage) or more detailed?
6. Do you want some text in the top-right corner of each slide saying something like “Illustrative only – Not created or endorsed by …”? If yes, what exact wording should appear there?
7. Do you want American or English spelling throughout the DoView? (Default to American if you don’t mind.)
8. Do you want to include a list of sources in your PowerPoint? (If you say “no”, there will be no Sources slide and no list of sources in the PowerPoint, and I will not track sources as I build the DoView.)
Once you have the answers to these questions:
• Run Prompt 1, then Prompt 2, then Prompt 3, in sequence.
• Do one at a time, and after each major stage, check with the user if they want you to proceed further or have additional input.
────────────────────────────────────────────────────────
PROMPT 1 — HIGH-LEVEL STRUCTURE AND SUBPAGES
────────────────────────────────────────────────────────
Do not act yet—apply this only after the user has answered 1–8.
If the user asked you to look up information about [insert name of organization / initiative / topic] on the internet, do so only to understand what it does, and then build a detailed DoView (PowerPoint) of the organization or initiative called [insert DoView name] using the rules below.
If the user asked you not to use the internet, then only use the information they provide and do not look anything up.
Rules for Prompt 1 work
• If you are using internet sources:
– Everything included in the DoView must be sourced from information you find on the internet about the specified initiative.
– Do not extrapolate from unrelated knowledge that isn’t explicitly mentioned in those sources.
– Only use public, non-sensitive information; do not collect or reproduce personal data about identifiable individuals unless the user has explicitly provided it and asked you to use it.
• If the user supplies the information:
– Work only from that information and do not look up further information on the initiative.
• The structure of columns and rows on each subpage must follow the inherent logic of that subpage’s topic, not an arbitrary template.
• After drafting subpages, balance the level of detail across them; add boxes where needed so subpages have similar granularity, without forcing identical box counts.
• Final column box(es) on subpages are bold (not asterisks).
• When ordering subpages:
– Distinguish externally focused pages from internal governance/operations pages.
– Put internal governance/operations pages at the end.
• At this stage, only prepare content for the DoView. Do not create the PowerPoint yet.
User check: list of subpages
After you draft a proposed list of subpages, say to the user:
“Above is a list of the proposed subpages. Do you want:
• Fewer/more pages,
• New pages added that you will name or describe,
• Specific pages renamed,
• Or, paste your own list of pages (I’ll use it exactly).”
Adjust the subpage list according to the user’s answer. Do not move on to Prompt 2 until the subpage list is confirmed.
────────────────────────────────────────────────────────
PROMPT 2 — DEVELOPING SUBPAGE DETAIL (“THIS–THEN” LOGIC)
────────────────────────────────────────────────────────
Role & mission
• You are an expert strategy/outcomes diagram builder trained by strategy and outcomes expert Dr Paul Duignan.
• You will convert the user’s initiative description into boxes arranged left → right (earlier → later), with the highest-level outcomes on the right.
• Default to rich detail. Only simplify if the user explicitly requests it or if they requested simple subpages.
Method – learn by example
Example subpage: “Marketing”
• Column 1
– Customer segments understood
• Column 2
– Key marketing messages identified
– Marketing channels identified
• Column 3
– Marketing materials produced
– Marketing materials pre-tested
• Column 4
– Success of marketing campaigns measured
– Feedback used for improvement
Why this works:
• Each item is a clear outcome (avoid pure process wording).
• One concept per box.
• Columns reflect causal flow; last column states a specific result.
Another example: “Business Growth & Sector Support”
• Original structure can be acceptable, but you should refine wording for clarity and outcome focus.
Drafting steps
1. Extract items
– From the initiative description, extract all items that are outcomes or steps to outcomes: “things that happen” you can use to identify “This–Then” logic.
2. Write as outcome statements
– Use outcome phrasing that tends to end with …ed, e.g.:
• “Key knowledge identified”
• “Quality courses run”
• “Health status improved”
3. Map “This–Then” relationships
– Identify influence relationships among boxes: if achieving A tends to lead to B, A should be to the left of B.
– Use the means–end rule: If achieving A means you wouldn’t bother doing B, then A goes to the right of B.
4. Keep boxes tight and focused
– Keep each box short.
– One concept per box.
– Do not combine “This” and “Then” in one box (e.g. don’t write “creating pamphlets to increase knowledge” in a single box).
5. Multiple high-level outcomes allowed
– You may have multiple high-level outcomes at the end of any subpage and on the final outcomes page.
6. World-centric, not just initiative-centric
– Make diagrams world-centric; include assumptions/risks (phrase risks positively).
7. Not only quantifiable items
– Do not restrict boxes to only quantifiable items.
8. Avoid siloing
– A lower-level box can influence multiple right-side boxes; avoid artificial silos.
9. Columns = causal stages
– Columns reflect left-to-right “This–Then” causality.
– Name columns descriptively if the user asks for headings (not generic “inputs/outputs/outcomes”).
10. Vary box counts per column
– Let domain logic dictate how many boxes sit in each column.
– A column can contain just one box or many.
11. Vertical flow
– If a column has top→bottom causality, order boxes accordingly.
12. Include necessary steps
– Include all necessary steps required to get to the next stage.
13. Use qualifiers where needed
– Use words like adequate / sufficient / high-quality where appropriate.
Slide-fit / space-budget check (horizontal AND vertical)
After drafting the boxes for a subpage, do a quick “space budget” review as if they were on a 16:9 slide.
Horizontal (left/right) checks
• Treat horizontal space as a shared budget for all columns on that page:
– If boxes in the right-hand column(s) would spill off the slide, look for columns that could be made narrower by:
• tightening wording in their boxes, and/or
• combining closely related low-importance boxes, and/or
• accepting slightly more wrapping in those columns.
– Target columns whose boxes are relatively short and leave empty vertical space (often later columns such as column 4 or 5) to make those columns narrower.
– Use the width you free up to make the tallest column(s) a bit wider so their text wraps less and they become shorter.
– Review the distance between columns and arrows. If there is generous white space between columns but the left- or right-hand column is close to the slide edge, first reduce the gaps between columns (and arrows) so that you can bring the outer columns further inside the slide and still keep sensible box widths.
Vertical (top/bottom) checks
• Treat vertical space similarly:
– Identify any “tall” column whose stack of boxes would likely overrun the bottom.
– First try to make that column wider (so each box is shorter) by stealing width from shorter columns that can safely be narrower.
– Only if that still seems too tall, consider:
• moving some boxes to an earlier/later column where they still make logical sense, or
• splitting that logic into two columns at the same stage.
• Prefer wording changes and reallocation of width between columns over simply adding more columns or accepting overflow.
• Do not lock in a subpage structure if it obviously forces any boxes outside the slide—adjust column widths and which outcomes sit in each column until a plausible, slide-fitting layout exists.
Final “margin check” before handover to Python
For each subpage, mentally check:
• Imagine the columns drawn on a 16:9 slide with reasonable gaps between columns and arrows.
• Ask:
– “Would any column of boxes plausibly stick out past the right-hand edge?”
– “Would any column’s stack of boxes plausibly run below the bottom boundary?”
If yes:
• First look for columns with relatively few or short boxes (often earlier columns such as column 1 or 2, or later columns such as column 4 or 5) that you could safely make narrower by tightening wording or accepting more wrapping, so that later columns can be shifted slightly left or made slightly wider.
• If a column looks too tall (risking the bottom boundary), see whether neighbouring columns could be made wider (to shorten their own box heights) or narrower (to donate width) while remaining readable.
• Explicitly check the outermost columns: ensure a visible margin between the leftmost/rightmost boxes and the slide edges, and that the bottom-most boxes in the tallest column sit comfortably above the bottom margin.
Only settle on final wording and column allocations once this mental “margin check” passes for both the right-hand edge and the bottom edge of the slide.
Structural reporting per subpage
For each subpage you draft, after listing its boxes, also show a single line like:
“Structure: columns = N; rows per column = [c1, c2, c3, …]”
This helps the user see that subpages do not all have the same pattern.
Subpages – how to break the model
• Break the overall model into subpages with lay-reader-friendly names (e.g., “Government Action”, “Sector Activity”, “Coordination”).
• Subpages are not just “input/process/outcomes”.
• Final box(es) on subpages should be lower-level than the overall final outcomes; the top-level outcomes sit to the extreme right of the overall model and are represented on a Final Outcomes page that applies across subpages.
• Avoid excessive duplication; if you must duplicate a box, mark it as “(duplicate)”.
Structural flexibility reminder
For every subpage:
• Decide how many logical stages (columns) it needs (could be 3, could be 6, etc.).
• Box counts per column should differ as required by the logic.
• Column headings (if used) should describe the content (e.g. “Secure Funding Channels”).
• Do not reuse a previous subpage’s structure unless the user explicitly told you to.
• Subpages do not need to end in a standard number of boxes.
• After drafting all subpages, scan the set for repeating patterns:
– If more than one subpage shares the same column/row counts by coincidence (especially “3 rows per column”), justify this to yourself.
– Where possible, vary structures so each page clearly mirrors its underlying domain.
• Any column can legitimately contain 1, 2, 3, 4, or more boxes—choose what best represents reality at that stage of the causal chain.
Before moving to Prompt 3
• Ask the user if they are happy with the detailed structure of the subpages.
• If the subpage structure looks too uniform, tell the user explicitly to review it.
• Ask the user whether all of the subpages end in the same number of boxes. If they do, explain that you can vary the number of boxes at the end of subpages and ask if they want you to do that.
Then say only:
“If you’re happy, I’ll run Prompt 3.”
Do not start Prompt 3 until they indicate they are happy (or have given you adjustments to apply).
────────────────────────────────────────────────────────
PROMPT 3 — PYTHON CODE TO PREPARE THE POWERPOINT
────────────────────────────────────────────────────────
Before running the Python code, use the answer to Question 8 (“Do you want to include a list of sources in your PowerPoint?”):
• If the user answered “yes”, keep track of the sources you actually used to build the DoView and populate the `sources` list in the Python code, then generate Sources slide(s).
• If the user answered “no”, do not collect or list sources, and do not create any Sources slide(s) in the PowerPoint. In this case, set `sources = []` in the Python code so that no Sources slides are created.
When the user confirms they’re ready for the PowerPoint, you will:
1. Convert the final DoView structure (title and subpages/columns/boxes) into the sections structure expected by the Python code.
2. Insert the correct DoView title, subpages, Final Outcomes section, corner note text, and sources (if applicable) into the code before running it.
3. Then generate the PowerPoint using the full script below.
• If the user answered “no” to Question 8, remove or skip any code that creates Sources slide(s).
Important: In the Python code below, the sample “Digital Equity / digital inclusion / DECA” content in sections and sources is purely illustrative. For any real DoView you generate, you must replace those example blocks completely with the title, sections, and sources for the user’s own initiative, and only include sources if the user has asked for them via Question 8.
Box text wrapping and layout rules
• Words may wrap; only split at standard hyphenation points (e.g., “eco-nomic”).
• Do not reduce font size to fit; keep font sizes consistent.
• Make box sizes large enough to contain the text.
• Auto-grow each subpage box’s height to fit its text (estimate lines from box width and font size); keep font size unchanged and adjust vertical spacing accordingly.
• When creating the sections/columns you pass into the Python, assume the slide has a fixed horizontal and vertical “budget” (use the same mental checks as in Prompt 2):
– Avoid giving every column lavish width; consciously keep some columns narrower so that columns with many long boxes can be wider and therefore shorter.
– Explicitly check that, after deciding column widths and gaps, the leftmost and rightmost columns still sit well inside the slide edges and that the tallest column’s stack of boxes sits above the bottom margin.
– If any column would otherwise cross the right-hand edge or bottom edge, first reduce the distance between columns and arrows and then reallocate width from shorter columns (including any visually generous outer columns) into the tallest column(s) until all boxes fit comfortably within the slide area.
Top-right slide note
• If the user wants corner text, place it on every slide (overview, Final Outcomes, all other subpages, “What is a DoView?”, and Sources).
• Position it in the top-right margin, in a narrow text box (roughly width of four short words), so it wraps over up to four lines, right-aligned, unobtrusive.
• Format as Calibri (Body) 12 pt, muted gray (similar to RGB 120,120,120).
• Never allow the corner text to exceed four lines—widen the box leftwards (up to ~2.8”) if needed; do not reduce font size.
• Do not insert hyphenation; wrap only at natural word boundaries.
• Do not append a full stop unless the user included one.
Special handling – Final Outcomes page & overview
• After identifying all subpages, place “Final Outcomes” as the second item in your sections list so it becomes slide 2.
• On the overview slide (slide 1):
– Draw the “Final Outcomes” box raised and centered above the grid of subpages (not a regular tile).
– Do not draw the “Final Outcomes” box again in the grid.
– Make the raised “Final Outcomes” box clickable, linking to the “Final Outcomes” slide.
– Allow the overview title to wrap rather than overlap the grey note.
– Draw a thin divider line halfway between the raised box and the coloured tiles.
• The Final Outcomes slide(s):
– Render as a simple stacked list of final outcomes (no left→right “This–Then” flow, no arrows, no multi-column layout).
– Same title/header styling as other subpages.
– Lay out each final outcome as its own box in a single centered vertical stack.
– Maximum 7 boxes per slide; if more, automatically paginate.
– Boxes are slimmer than default to allow generous spacing, keeping Calibri styling.
Slide order requirement
Create slides in this exact order:
1. Overview (created first)
2. Final Outcomes (created second)
3. All other subpages
4. “What is a DoView?” slide
5. (Optional) Sources slide(s) – only include these if the user answered “yes” to Question 8 about including a list of sources.
Create the Final Outcomes subpage immediately after the Overview so it becomes slide 2. After all subpages exist, draw the Overview’s raised Final Outcomes box and the grid tiles linking to subpages, then add “What is a DoView?” and, if applicable, Sources slides.
Explanation slide sizing (prevent truncation)
• Place the long “What is a DoView?” description on one single slide only.
• Do not shorten, paraphrase, or paginate it—copy the text exactly as shown in this prompt (including blank lines).
• Use a wide text box with word_wrap=True, positioned below the title and above the footer so it cannot be clipped.
Sources slide layout & pagination
• Title must not overlap the “Back to Overview” button; place title lower (e.g. top ≈ 0.9”).
• Make every source URL clickable (run.hyperlink.address = url).
• Lay sources out in two columns. If they don’t fit, automatically create additional Sources slides and number them “[1]”, “[2]”, etc. (If only one Sources slide, do not add “[1]”.)
• Ensure source items never overlap each other or the footer—even on the last slide—by calculating rows-per-column from available height and leaving at least ~0.6” vertical space per item.
────────────────────────────────────────────────────────
PYTHON CODE (USE AFTER CONTENT IS FULLY FINISHED)
────────────────────────────────────────────────────────
from datetime import datetime, timezone, timedelta
from pptx import Presentation
from pptx.util import Inches, Pt, Cm
from pptx.enum.shapes import MSO_SHAPE
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
import textwrap, math, re
# ──────────────────────────────────────────────────────────────
# 1) COLOUR PALETTE
# ──────────────────────────────────────────────────────────────
DEFAULT_COLORS = [
RGBColor(255, 255, 255), # white
RGBColor(255, 255, 186), # pastel yellow
RGBColor(249, 211, 212), # pastel pink
RGBColor(159, 225, 255), # light blue
RGBColor(190, 255, 161), # light green
RGBColor(212, 201, 164), # beige
RGBColor(182, 188, 242), # lavender
RGBColor(254, 190, 143), # peach
RGBColor(224, 253, 255), # pale cyan
RGBColor(214, 214, 214), # light grey
]
WHITE = DEFAULT_COLORS[0]
NON_WHITE_COLORS = DEFAULT_COLORS[1:]
MAX_FINAL_OUTCOMES_PER_SLIDE = 7
def is_final_outcome_page(title: str) -> bool:
return "final outcome" in title.lower()
# ──────────────────────────────────────────────────────────────
# 2) DATA (replace with generated sections when ready)
# NOTE: The following `sections` block is an ILLUSTRATIVE EXAMPLE
# using a digital inclusion / DECA scenario. When creating a real
# DoView for the user, REPLACE THIS ENTIRE BLOCK (title + columns)
# with the sections for the user’s own initiative.
# ──────────────────────────────────────────────────────────────
sections = [
{
"title": "Community Awareness & Engagement",
"columns": [
["Target audiences segmented", "Culturally resonant messages crafted"],
["Multi-channel outreach executed", "Local champions activated"],
["Community understanding increased", "Supportive behaviours adopted"],
],
},
{
"title": "Final Outcomes",
"columns": [
["Social equity achieved", "Long-term resilience strengthened"],
],
},
{
"title": "Digital Transformation Enablement",
"columns": [
["Strategic digital roadmap finalised", "Stakeholder buy-in secured"],
[
"Legacy systems audited", "Cybersecurity standards enforced",
"Cloud migration completed", "Interoperability protocols adopted",
"User-centred design principles embedded",
],
["Staff digital literacy enhanced", "Automated workflows deployed", "Real-time analytics operational"],
["Digital value realisation maximised"],
],
},
{
"title": "Integrated Service Delivery Pathway",
"columns": [
["Shared governance framework established", "Funding streams aligned", "Policy barriers removed"],
["Cross-agency protocols agreed", "Referral systems digitised", "Data-sharing agreements finalised", "Workforce capacity mapped"],
["Frontline staff upskilled", "Client triage standardised", "Service eligibility clarified"],
["Co-located hubs operational", "Virtual navigation tools launched", "Wrap-around plans customised"],
["Access gaps monitored", "Service utilisation optimised", "Feedback loops institutionalised"],
["Client wellbeing improved", "Inequities in service access reduced"],
],
},
]
# Optional top-right note text (replace with the exact wording requested by the user, or set to "" to skip it)
corner_note_text = "Illustrative only — Not created or endorsed by EU Commission"
# Ensure the "Final Outcomes" section is the second item (slide 2)
try:
fi = next(i for i, s in enumerate(sections) if is_final_outcome_page(s["title"]))
if fi != 1:
sections.insert(1, sections.pop(fi))
except StopIteration:
pass # no final outcomes section present
# ──────────────────────────────────────────────────────────────
# 3) HELPERS
# ──────────────────────────────────────────────────────────────
def add_box(slide, left, top, width, height, text, color,
font_size=11, hyperlink_slide=None, bold=False, font_name=None, compact=False):
shp = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
shp.fill.solid()
shp.fill.fore_color.rgb = color
# For boxes with links: solid 1 pt black border.
# For all other boxes, use the original styling (white boxes with a grey top rule).
is_final_outcomes_heading = (
color == WHITE and isinstance(text, str) and text.strip().lower() == "final outcomes"
)
if hyperlink_slide is not None:
shp.line.color.rgb = RGBColor(0, 0, 0)
shp.line.width = Pt(1)
else:
if color == WHITE and not is_final_outcomes_heading:
shp.line.fill.background()
top_border = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, Cm(0.05))
top_border.fill.solid()
top_border.fill.fore_color.rgb = RGBColor(190, 190, 190)
top_border.line.fill.background()
else:
shp.line.color.rgb = color
tf = shp.text_frame
tf.clear()
tf.word_wrap = True
tf.margin_left = Inches(0.06)
tf.margin_right = Inches(0.06)
tf.margin_top = Inches(0.01) if compact else Inches(0.03)
tf.margin_bottom = Inches(0.01) if compact else Inches(0.03)
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
paragraphs = (text or "").split("\n")
for idx, line in enumerate(paragraphs):
p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
p.text = line
p.alignment = PP_ALIGN.CENTER
for run in p.runs:
run.font.size = Pt(font_size)
run.font.bold = bold
if font_name:
run.font.name = font_name
run.font.color.rgb = RGBColor(0, 0, 0)
if hyperlink_slide:
shp.click_action.target_slide = hyperlink_slide
return shp
def add_arrow(slide, left, top, size):
arrow = slide.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, left, top, size, size)
arrow.fill.solid()
arrow.fill.fore_color.rgb = RGBColor(200, 200, 200)
arrow.line.color.rgb = RGBColor(0, 0, 0)
arrow.line.width = Pt(0.5)
return arrow
def add_footer(slide, slide_w, slide_h):
nz_now = datetime.now(timezone(timedelta(hours=12)))
txt = (
"Not endorsed by organization or by DoViewPlanning. Made from line/user info via AI prompt. Use at own risk re IP & accuracy."
f"{nz_now:%Y-%m-%d %H:%M}"
)
ht = Inches(0.35)
# ensure footer fits fully within slide bounds (increase left margin; reduce width accordingly)
box = slide.shapes.add_textbox(Inches(0.30), slide_h - ht, slide_w - Inches(0.60), ht)
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = txt
p.font.size = Pt(10)
p.font.color.rgb = RGBColor(90, 90, 90)
p.alignment = PP_ALIGN.RIGHT
def add_doview_online_link(slide, slide_w, slide_h):
text = "DoViewPlanning.Org"
left = slide_w - Inches(2.2)
top = slide_h - Inches(0.8)
width = Inches(2.0)
height = Inches(0.3)
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.clear()
p = tf.paragraphs[0]
run = p.add_run()
run.text = text
run.font.size = Pt(14)
run.font.name = "Calibri"
run.font.color.rgb = RGBColor(0, 102, 204)
run.hyperlink.address = "http://DoViewPlanning.Org"
p.alignment = PP_ALIGN.RIGHT
def add_top_right_note(slide, slide_w, text):
"""Place an optional Calibri gray note in the slide's top-right corner."""
if not text:
return
display_text = text.rstrip()
while display_text.endswith("."):
display_text = display_text[:-1].rstrip()
if not display_text:
return
width = Inches(1.6)
max_width = Inches(2.8)
max_lines = 4
for _ in range(6):
if _estimate_lines(display_text, width / Inches(1), 12) <= max_lines or width >= max_width:
break
width = min(max_width, width + Inches(0.3))
height = Inches(1.35)
left = slide_w - width - Inches(0.35)
top = Inches(0.07)
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.clear()
tf.word_wrap = True
tf.margin_left = 0
tf.margin_right = 0
tf.margin_top = 0
tf.margin_bottom = 0
p = tf.paragraphs[0]
p.text = display_text
p.font.size = Pt(12)
p.font.name = "Calibri"
p.font.color.rgb = RGBColor(120, 120, 120)
p.alignment = PP_ALIGN.RIGHT
# --- Auto-sizing helpers to prevent text overruns in subpage boxes ---
def _estimate_lines(text: str, width_inches: float, font_pt: float) -> int:
"""Roughly estimate wrapped line count based on width and font size."""
width_pts = width_inches * 72.0
approx_char_w = font_pt * 0.50 # slightly smaller estimate to avoid clipping
chars_per_line = max(8, int(width_pts / approx_char_w))
lines = 0
for para in text.split("\n"):
wrapped = textwrap.wrap(para, width=chars_per_line) or [""]
lines += len(wrapped)
return max(1, lines)
def box_height_inches_for_text(text: str, width_inches: float, font_pt: float = 11.0,
min_height_inches: float = 0.9, line_gap_inches: float = 0.05) -> float:
lines = _estimate_lines(text, width_inches, font_pt)
line_height_inches = (font_pt * 1.25) / 72.0 # generous leading
return max(min_height_inches, lines * line_height_inches + (lines - 1) * line_gap_inches)
def _longest_word_length(text: str) -> int:
tokens = re.findall(r"[A-Za-z0-9]+(?:['-][A-Za-z0-9]+)*", text or "")
return max((len(token) for token in tokens), default=0)
def min_box_width_for_words(texts, font_pt: float = 11.0, padding_inches: float = 0.06):
"""Return minimum width needed to avoid breaking the longest word."""
longest = 0
for txt in texts or []:
longest = max(longest, _longest_word_length(txt))
if longest <= 0:
return 0
approx_char_pts = font_pt * 0.50
needed_inches = (longest * approx_char_pts) / 72.0 + padding_inches
return Inches(needed_inches)
def adjust_grid_shapes_to_margins(shapes, slide_w, content_top_limit, content_bottom_limit, edge_margin_inches=0.35):
"""
Post-process a set of content shapes (columns and arrows) so that:
• horizontally, they are centred and fully inside the slide margins; and
• vertically, if they slightly overrun the bottom but there is spare room above,
they are shifted up into that spare room (no vertical scaling).
"""
if not shapes:
return
edge_margin = Inches(edge_margin_inches)
min_left = min(sh.left for sh in shapes)
max_right = max(sh.left + sh.width for sh in shapes)
min_top = min(sh.top for sh in shapes)
max_bottom = max(sh.top + sh.height for sh in shapes)
# Horizontal adjustment
target_left = edge_margin
target_right = slide_w - edge_margin
target_width = target_right - target_left
total_width = max_right - min_left
if total_width <= 0:
return
if total_width <= target_width:
# Just recenter between margins
current_center = (min_left + max_right) / 2
target_center = (target_left + target_right) / 2
dx = target_center - current_center
for sh in shapes:
sh.left = int(sh.left + dx)
else:
# Uniformly scale inwards, then center
scale = float(target_width) / float(total_width) * 0.98
current_center = (min_left + max_right) / 2
target_center = (target_left + target_right) / 2
for sh in shapes:
cx = sh.left + sh.width / 2
new_cx = target_center + (cx - current_center) * scale
new_w = sh.width * scale
sh.left = int(new_cx - new_w / 2)
sh.width = int(new_w)
# Vertical adjustment – only shift upward into any unused space above content_top_limit
if content_bottom_limit is None or content_top_limit is None:
return
max_bottom = max(sh.top + sh.height for sh in shapes)
min_top = min(sh.top for sh in shapes)
total_height = max_bottom - min_top
available_height = content_bottom_limit - content_top_limit
if total_height <= 0 or available_height <= 0:
return
if max_bottom > content_bottom_limit and min_top > content_top_limit:
dy = min(max_bottom - content_bottom_limit, min_top - content_top_limit)
for sh in shapes:
sh.top = int(sh.top - dy)
def add_explanation_single_page(prs, overview, title_text, body_text, font_pt=16, corner_note_text=""):
"""Create a single 'What is a DoView?' slide with the provided text verbatim."""
blank = prs.slide_layouts[6]
slide_w, slide_h = prs.slide_width, prs.slide_height
slide = prs.slides.add_slide(blank)
add_box(
slide, Inches(0.15), Inches(0.15), Inches(1.8), Inches(0.6),
"Back to Overview", RGBColor(230, 230, 230), font_size=14, hyperlink_slide=overview
)
add_top_right_note(slide, slide_w, corner_note_text)
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.85), slide_w - Inches(1.0), Inches(0.6))
tf_title = title_box.text_frame
tf_title.clear()
p_title = tf_title.paragraphs[0]
p_title.text = title_text
p_title.font.size = Pt(22)
p_title.font.color.rgb = RGBColor(0, 0, 0)
p_title.alignment = PP_ALIGN.CENTER
body_box = slide.shapes.add_textbox(Inches(0.7), Inches(1.6), slide_w - Inches(1.4), slide_h - Inches(2.6))
tf_body = body_box.text_frame
tf_body.clear()
tf_body.word_wrap = True
tf_body.margin_top = 0
tf_body.margin_bottom = 0
para = tf_body.paragraphs[0]
para.text = (body_text or "").replace("\r\n", "\n").replace("\r", "\n")
para.font.size = Pt(font_pt)
para.font.color.rgb = RGBColor(0, 0, 0)
para.alignment = PP_ALIGN.LEFT
add_footer(slide, slide_w, slide_h)
add_doview_online_link(slide, slide_w, slide_h)
def add_sources_pages(prs, overview, sources, title_base, corner_note_text=""):
"""Paginate sources across as many slides as needed, two columns per slide, clickable links.
If only one Sources slide is needed, do NOT append '[1]' to the title."""
blank = prs.slide_layouts[6]
slide_w, slide_h = prs.slide_width, prs.slide_height
# Layout constants
title_top = Inches(0.9) # keep clear of the Back to Overview button
title_height = Inches(0.6)
left_margin = Inches(1.0)
right_margin = Inches(1.0)
top_y = Inches(1.7) # content starts below the title
bottom_margin = Inches(1.1) # leave room for footer/link
col_gap = Inches(0.2)
line_h = Inches(0.75) # per-source text box height (extra tall to prevent URL overlap)
col_width = (slide_w - left_margin - right_margin - col_gap) / 2
# Compute rows-per-column that fit on the slide and capacity per slide
available_h = slide_h - top_y - bottom_margin
max_rows_per_col = max(1, int(available_h / line_h))
capacity = max_rows_per_col * 2
total = len(sources)
total_pages = max(1, math.ceil(total / capacity))
i = 0
page = 1
while i < total:
slide = prs.slides.add_slide(blank)
# Back to Overview
add_box(slide, Inches(0.15), Inches(0.15), Inches(1.8), Inches(0.6),
"Back to Overview", RGBColor(230, 230, 230), font_size=14, hyperlink_slide=overview)
add_top_right_note(slide, slide_w, corner_note_text)
# Title with conditional page numbering
title_text = f"{title_base} [{page}]" if total_pages > 1 else title_base
title_box = slide.shapes.add_textbox(Inches(0.5), title_top, slide_w - Inches(1.0), title_height)
tf = title_box.text_frame; tf.clear()
p = tf.paragraphs[0]
p.text = title_text
p.font.size = Pt(22); p.font.color.rgb = RGBColor(0, 0, 0); p.alignment = PP_ALIGN.CENTER
# Column anchors
left_x = left_margin
right_x = left_margin + col_width + col_gap
# Items for this page
chunk = sources[i:i+capacity]
# Render chunk
for idx, (label, url) in enumerate(chunk):
x = left_x if idx < max_rows_per_col else right_x
y = top_y + (idx % max_rows_per_col) * line_h
tb = slide.shapes.add_textbox(x, y, col_width, line_h)
ttf = tb.text_frame; ttf.word_wrap = True; ttf.clear()
p1 = ttf.paragraphs[0]
p1.text = f"• {label}"
p1.font.size = Pt(12); p1.font.color.rgb = RGBColor(0, 0, 0)
# URL on new run (clickable)
run = p1.add_run()
run.text = f"\n{url}"
run.font.size = Pt(11)
run.font.color.rgb = RGBColor(0, 102, 204)
run.hyperlink.address = url
add_footer(slide, slide_w, slide_h)
add_doview_online_link(slide, slide_w, slide_h)
i += capacity
page += 1
# ──────────────────────────────────────────────────────────────
# 4) BUILD PRESENTATION (slide order: Overview → Final Outcomes → other subpages → Explanation → Sources)
# ──────────────────────────────────────────────────────────────
prs = Presentation()
blank = prs.slide_layouts[6]
slide_w, slide_h = prs.slide_width, prs.slide_height
# (A) OVERVIEW FIRST (slide 1)
overview = prs.slides.add_slide(blank)
strategy_title = "Writing Software – Front End DoView"
heading = overview.shapes.add_textbox(Inches(0.5), Inches(0.3), slide_w - Inches(1.0), Inches(0.85))
heading_tf = heading.text_frame
heading_tf.word_wrap = True
heading_tf.margin_left = 0
heading_tf.margin_right = 0
heading_tf.margin_top = 0
heading_tf.margin_bottom = 0
p = heading_tf.paragraphs[0]
p.text = strategy_title
p.font.size = Pt(24)
p.font.color.rgb = RGBColor(0, 0, 0)
p.alignment = PP_ALIGN.CENTER
# Determine index of Final Outcomes
fi_idx = next((i for i, s in enumerate(sections) if is_final_outcome_page(s["title"])), None)
# Prepare final outcome chunks (max 7 items per slide)
final_outcome_chunks = []
if fi_idx is not None:
fo_items = [item for col in sections[fi_idx].get("columns", []) for item in col]
if not fo_items:
fo_items = ["Final outcomes listed here"]
# keep all items, but we'll paginate in chunks of MAX_FINAL_OUTCOMES_PER_SLIDE
chunk_size = MAX_FINAL_OUTCOMES_PER_SLIDE
final_outcome_chunks = [fo_items[i:i + chunk_size] for i in range(0, len(fo_items), chunk_size)]
if not final_outcome_chunks:
final_outcome_chunks = [["Final outcomes listed here"]]
# Precompute colors
section_colors = []
non_white_idx = 0
for sec in sections:
if is_final_outcome_page(sec["title"]):
section_colors.append(WHITE)
else:
section_colors.append(NON_WHITE_COLORS[non_white_idx % len(NON_WHITE_COLORS)])
non_white_idx += 1
# (B) Create subpage slides with Final Outcomes SECOND
section_slides = [[] for _ in sections]
if fi_idx is not None:
for _ in final_outcome_chunks:
section_slides[fi_idx].append(prs.slides.add_slide(blank))
for i in range(len(sections)):
if i == fi_idx:
continue
section_slides[i].append(prs.slides.add_slide(blank))
# (C) Draw the Overview (raised Final Outcomes + grid) with links to subslides
cols_per_row = 3
box_w = Cm(5.5)
box_h = Cm(2.0)
gap_x = Cm(1.0)
gap_y = Cm(1.2)
grid_sections = [(i, s) for i, s in enumerate(sections) if i != fi_idx]
n = len(grid_sections)
rows = (n + cols_per_row - 1) // cols_per_row
total_width = cols_per_row * box_w + (cols_per_row - 1) * gap_x
start_x = (slide_w - total_width) / 2
total_height = rows * box_h + (rows - 1) * gap_y
base_grid_y = (slide_h - total_height) / 2 + Inches(0.8)
col_positions = [start_x + i * (box_w + gap_x) for i in range(cols_per_row)]
# Raised Final Outcomes box (links to slide 2)
if fi_idx is not None:
fo_box = add_box(
overview, (slide_w - box_w) / 2, base_grid_y - box_h - gap_y - Inches(0.25),
box_w, box_h, "Final Outcomes", WHITE, font_size=18,
hyperlink_slide=section_slides[fi_idx][0], bold=True
)
fo_bottom = fo_box.top + fo_box.height
line_height = Inches(0.02)
line_top = fo_bottom + (base_grid_y - fo_bottom) / 2 - line_height / 2
line_left = col_positions[0]
line_width = (col_positions[-1] + box_w) - line_left
line_shape = overview.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
line_left,
line_top,
line_width,
line_height
)
line_shape.fill.solid()
line_shape.fill.fore_color.rgb = RGBColor(150, 150, 150)
line_shape.line.fill.background()
# Grid boxes for other sections
for row in range(rows):
row_items = grid_sections[row * cols_per_row : row * cols_per_row + cols_per_row]
if not row_items:
continue
count = len(row_items)
if count >= 3:
indices = [0, 1, 2]
elif count == 2:
indices = [0, 2]
else: # count == 1
indices = [1]
for (sec_idx, sec), col_idx in zip(row_items, indices):
left = col_positions[col_idx]
top = base_grid_y + row * (box_h + gap_y)
add_box(
overview, left, top, box_w, box_h,
sec["title"], section_colors[sec_idx], font_size=11,
hyperlink_slide=section_slides[sec_idx][0]
)
add_footer(overview, slide_w, slide_h)
add_doview_online_link(overview, slide_w, slide_h)
add_top_right_note(overview, slide_w, corner_note_text)
# 5) Populate subslides
for idx, (sec, slides, color) in enumerate(zip(sections, section_slides, section_colors)):
for slide_pos, sub in enumerate(slides):
# Back to Overview + Title
add_box(sub, Inches(0.15), Inches(0.15), Inches(1.8), Inches(0.6),
"Back to Overview", RGBColor(230, 230, 230), font_size=14, hyperlink_slide=overview)
if idx == fi_idx:
heading_box = add_box(sub, Inches(0.5), Inches(0.95), slide_w - Inches(1.0), Inches(0.45),
"Final Outcomes", color, font_size=20, bold=True, font_name="Calibri")
else:
heading_box = add_box(sub, Inches(0.5), Inches(0.95), slide_w - Inches(1.0), Inches(0.45),
sec["title"], color, font_size=18, bold=True)
add_top_right_note(sub, slide_w, corner_note_text)
# SPECIAL: Final Outcomes as stacked list (no arrows, no multi-column flow)
if idx == fi_idx:
items = final_outcome_chunks[slide_pos][:MAX_FINAL_OUTCOMES_PER_SLIDE]
box_w_fo = slide_w - Inches(1.5)
col_w_in = box_w_fo / Inches(1)
heights_in = [box_height_inches_for_text(t, col_w_in, font_pt=16, min_height_inches=0.6) for t in items]
gap_in = 0.25
content_top = heading_box.top + heading_box.height + Inches(0.3)
content_bottom = slide_h - Inches(0.8)
available_in = max(1.0, (content_bottom - content_top) / Inches(1))
total_in = sum(heights_in) + max(0, len(heights_in) - 1) * gap_in
if len(heights_in) > 1 and total_in > available_in:
gap_in = max(0.08, (available_in - sum(heights_in)) / (len(heights_in) - 1))
total_in = sum(heights_in) + gap_in * (len(heights_in) - 1)
left_pos = (slide_w - box_w_fo) / 2
# move items below title box
y = content_top
for h_in, text in zip(heights_in, items):
item_box = add_box(sub, left_pos, y, box_w_fo, Inches(h_in), text, color,
font_size=16, font_name="Calibri")
y += Inches(h_in + gap_in)
add_footer(sub, slide_w, slide_h)
add_doview_online_link(sub, slide_w, slide_h)
continue
if idx == fi_idx:
continue
# Default: subpages in ‘This–Then’ columns with arrows (auto-sized box heights)
columns_data = sec.get("columns") or [[]]
n_cols = len(columns_data)
arrow_size = Inches(0.28)
col_gap_x = Inches(0.28)
side_reserve = Inches(1.3)
edge_margin = Inches(0.35)
gap_between_boxes_in = 0.20
min_box_height_in = 0.75
top_buffer = Inches(0.35)
bottom_buffer = Inches(0.90)
min_col_width_floor = Inches(0.6)
safety_margin = 0.4
current_line_gap_in = 0.05
compact_mode = False
# Track all content shapes (columns + arrows) for post-layout margin adjustment
grid_shapes = []
def add_grid_box(*args, **kwargs):
shp = add_box(*args, **kwargs, compact=compact_mode)
grid_shapes.append(shp)
return shp
def add_grid_arrow(*args, **kwargs):
shp = add_arrow(*args, **kwargs)
grid_shapes.append(shp)
return shp
def compute_layout():
gap_unit = 2 * col_gap_x + arrow_size
total_gap = max(0, n_cols - 1) * gap_unit
max_total_width = max(Inches(1.0), slide_w - 2 * edge_margin - safety_margin)
usable_width = slide_w - side_reserve - total_gap
min_total = Inches(0.9 * n_cols)
if usable_width < min_total:
usable_width = min_total
base_col_w = max(Inches(0.95), usable_width / max(1, n_cols))
max_allowed_width = max(Inches(4.5), slide_w - Inches(0.6))
# Precompute vertical space and simple stats for donor preference
content_top = heading_box.top + heading_box.height + top_buffer
content_bottom = slide_h - bottom_buffer
available_height = max(Inches(1.0), content_bottom - content_top)
rows_per_col = [len(c) for c in columns_data]
if rows_per_col:
sorted_rows = sorted(rows_per_col)
median_rows = sorted_rows[len(sorted_rows) // 2]
else:
median_rows = 0
column_min_widths = []
col_widths = []
width_pad = Inches(0.25)
def clamp_widths_to_limit(widths, mins, limit):
widths = widths[:]
for _ in range(50):
total = sum(widths) + total_gap
if total <= limit:
break
adjustable = [i for i, w in enumerate(widths) if w - mins[i] > 0]
if not adjustable:
break
excess = total - limit
slice_reduce = max(1, excess // len(adjustable))
for idx2 in adjustable:
reducible = widths[idx2] - mins[idx2]
reduction = min(reducible, slice_reduce)
if reduction <= 0:
continue
widths[idx2] -= reduction
excess -= reduction
if excess <= 0:
break
return widths
for col in columns_data:
req_width = min_box_width_for_words(col, font_pt=11.0)
req_width = max(min_col_width_floor, req_width)
column_min_widths.append(req_width)
preferred = min(base_col_w, req_width + width_pad)
col_widths.append(max(req_width, preferred))
if col_widths:
col_widths = clamp_widths_to_limit(col_widths, column_min_widths, max_allowed_width)
col_widths = clamp_widths_to_limit(col_widths, column_min_widths, max_total_width)
def compute_heights_for_widths(widths):
widths_in = [(w / Inches(1)) for w in widths]
per_heights = []
totals = []
for col, width_in in zip(columns_data, widths_in):
heights = [
box_height_inches_for_text(
text,
width_in,
font_pt=11,
min_height_inches=min_box_height_in,
line_gap_inches=current_line_gap_in,
)
for text in col
]
per_heights.append(heights)
total = sum(heights) + max(0, len(heights) - 1) * gap_between_boxes_in
totals.append(total)
return widths_in, per_heights, totals
if col_widths:
col_widths_in, per_col_box_heights_in, col_totals_in = compute_heights_for_widths(col_widths)
available_height_in = available_height / Inches(1)
# First: AGGRESSIVE rebalance from shorter to taller columns
for iteration in range(40): # increased iterations
if not col_totals_in:
break
tallest_idx2 = max(range(len(col_totals_in)), key=lambda i: col_totals_in[i])
tallest = col_totals_in[tallest_idx2]
if tallest <= available_height_in:
break
donors = [
i for i in range(len(col_totals_in))
if i != tallest_idx2 and (col_widths[i] - column_min_widths[i]) > Inches(0.02)
]
if not donors:
break
# Score-based donor selection (prefer outer, shorter columns)
best_idx = None
best_score = -1
for i in donors:
score = 0
if i == 0 or i == n_cols - 1:
score += 3 # prefer outer columns as donors
if col_totals_in[i] < tallest * 0.9:
score += 2 # prefer shorter columns
if rows_per_col[i] <= median_rows:
score += 1 # fewer rows → better donor
# NEW: Extra points for columns with lots of spare width
spare_ratio = (col_widths[i] - column_min_widths[i]) / max(column_min_widths[i], Inches(0.1))
if spare_ratio > 0.3:
score += 2
if score > best_score:
best_score = score
best_idx = i
donor_idx = best_idx if best_idx is not None else min(donors, key=lambda i: col_totals_in[i])
spare = col_widths[donor_idx] - column_min_widths[donor_idx]
if spare <= Inches(0.02):
break
# NEW: More aggressive transfer - take up to 85% of spare, or more in later iterations
aggressiveness = min(0.95, 0.75 + (iteration * 0.01))
delta = min(spare * aggressiveness, Inches(0.45))
col_widths[donor_idx] -= delta
col_widths[tallest_idx2] += delta
col_widths_in, per_col_box_heights_in, col_totals_in = compute_heights_for_widths(col_widths)
# Second: if still too tall and there is horizontal slack, AGGRESSIVELY widen tall columns
total_width_now = sum(col_widths) + total_gap
slack = max(0, max_total_width - total_width_now)
# NEW: Multiple passes - first use slack, then compress donors further if needed
for pass_num in range(2):
for _ in range(35):
if not col_totals_in:
break
tallest_idx3 = max(range(len(col_totals_in)), key=lambda i: col_totals_in[i])
tallest = col_totals_in[tallest_idx3]
if tallest <= available_height_in:
break
total_width_now = sum(col_widths) + total_gap
slack = max(0, max_total_width - total_width_now)
made_progress = False
# Use available slack first
if slack > Inches(0.02):
grow = min(slack, Inches(0.35))
col_widths[tallest_idx3] += grow
made_progress = True
# NEW: If no slack or still too tall, aggressively steal from all donors
if tallest > available_height_in or pass_num == 1:
# Find multiple donors, prioritize by score
potential_donors = [
(i, col_widths[i] - column_min_widths[i])
for i in range(len(col_widths))
if i != tallest_idx3 and (col_widths[i] - column_min_widths[i]) > Inches(0.02)
]
# Sort donors by preference: outer columns, shorter stacks first
def donor_score(idx_spare_tuple):
i, spare = idx_spare_tuple
score = spare / Inches(1) # base score is spare width
if i == 0 or i == n_cols - 1:
score *= 1.5 # prefer outer
if col_totals_in[i] < tallest * 0.85:
score *= 1.3 # prefer shorter
return score
potential_donors.sort(key=donor_score, reverse=True)
# Take from top donors
for donor_idx, spare in potential_donors[:min(3, len(potential_donors))]:
if spare > Inches(0.02):
take = min(spare * 0.40, Inches(0.25))
col_widths[donor_idx] -= take
col_widths[tallest_idx3] += take
made_progress = True
if made_progress:
col_widths_in, per_col_box_heights_in, col_totals_in = compute_heights_for_widths(col_widths)
else:
break
# After first pass, if still too tall, proceed to second pass with more aggressive stealing
if not col_totals_in:
break
tallest = col_totals_in[max(range(len(col_totals_in)), key=lambda i: col_totals_in[i])]
if tallest <= available_height_in:
break
else:
col_widths_in, per_col_box_heights_in, col_totals_in = [], [], []
max_col_height_in = max(col_totals_in) if col_totals_in else min_box_height_in
total_width = sum(col_widths) + total_gap if col_widths else total_gap
return {
"col_widths": col_widths,
"per_col_box_heights_in": per_col_box_heights_in,
"col_totals_in": col_totals_in,
"max_col_height": Inches(max_col_height_in),
"content_top": content_top,
"available_height": available_height,
"total_width": total_width,
"column_min_widths": column_min_widths,
"total_gap": total_gap,
"max_total_width": max_total_width,
}
metrics = compute_layout()
# NEW: Coordinated gap compression and width reallocation
# First: Try to use horizontal space (gaps + margins) to help vertical overflow
for iteration in range(30):
widths = metrics["col_widths"]
mins = metrics["column_min_widths"]
total_width = metrics["total_width"]
max_height = metrics["max_col_height"]
avail_height = metrics["available_height"]
# If we fit vertically, we're done
if max_height <= avail_height:
break
made_change = False
# Strategy 1: If columns are cramped horizontally, reduce gaps to free width for widening
need_more_room = any((w - m) < Inches(0.05) for w, m in zip(widths, mins))
if need_more_room and col_gap_x > Inches(0.20):
col_gap_x = max(Inches(0.20), col_gap_x - Inches(0.02))
made_change = True
# Strategy 2: If we have horizontal slack but vertical overflow, reduce side margins
horizontal_slack = metrics["max_total_width"] - total_width
if max_height > avail_height and horizontal_slack < Inches(0.3) and side_reserve > Inches(0.8):
side_reserve = max(Inches(0.8), side_reserve - Inches(0.10))
made_change = True
# Strategy 3: Reduce column gaps if we're near the edge horizontally
if total_width >= metrics["max_total_width"] - Inches(0.1) and col_gap_x > Inches(0.20):
col_gap_x = max(Inches(0.20), col_gap_x - 0.02)
made_change = True
if made_change:
metrics = compute_layout()
else:
break
# Second: Vertical parameter tuning (gaps, buffers, box heights)
for iteration in range(25):
if metrics["max_col_height"] <= metrics["available_height"]:
break
made_change = False
# Priority order: least impact to readability first
if gap_between_boxes_in > 0.12:
gap_between_boxes_in = max(0.12, gap_between_boxes_in - 0.02)
made_change = True
elif min_box_height_in > 0.65:
min_box_height_in = max(0.65, min_box_height_in - 0.03)
made_change = True
elif col_gap_x > Inches(0.20):
col_gap_x = max(Inches(0.20), col_gap_x - 0.02)
made_change = True
elif top_buffer > Inches(0.25):
top_buffer = max(Inches(0.25), top_buffer - Inches(0.02))
made_change = True
elif bottom_buffer > Inches(0.75):
bottom_buffer = max(Inches(0.75), bottom_buffer - Inches(0.02))
made_change = True
elif gap_between_boxes_in > 0.10:
gap_between_boxes_in = max(0.10, gap_between_boxes_in - 0.01)
made_change = True
elif min_box_height_in > 0.60:
min_box_height_in = max(0.60, min_box_height_in - 0.02)
made_change = True
else:
break
if made_change:
metrics = compute_layout()
else:
break
# Compact mode: if still too tall, reduce internal padding and minimum height
if metrics["max_col_height"] > metrics["available_height"]:
compact_mode = True
min_box_height_in = 0.55
current_line_gap_in = 0.03
metrics = compute_layout()
for _ in range(10):
if metrics["max_col_height"] <= metrics["available_height"]:
break
if gap_between_boxes_in > 0.10:
gap_between_boxes_in -= 0.01
elif top_buffer > Inches(0.22):
top_buffer -= Inches(0.01)
elif bottom_buffer > Inches(0.70):
bottom_buffer -= Inches(0.01)
else:
break
metrics = compute_layout()
col_widths = metrics["col_widths"] or [Inches(1.0)] * n_cols
per_col_box_heights_in = metrics["per_col_box_heights_in"]
col_totals_in = metrics["col_totals_in"]
max_col_h = metrics["max_col_height"]
content_top = metrics["content_top"]
available_height = metrics["available_height"]
vertical_gap = Inches(gap_between_boxes_in)
total_width = metrics["total_width"]
# NEW: Final pre-rendering geometry check
# Ensure the grid will fit horizontally within margins
# adjust_grid_shapes_to_margins() will center/scale after rendering, but let's start well-positioned
min_left_needed = edge_margin
max_right_allowed = slide_w - edge_margin - safety_margin
max_width_allowed = max_right_allowed - min_left_needed
# If total_width exceeds available space, do one final aggressive compression
if total_width > max_width_allowed:
excess = total_width - max_width_allowed
# Try reducing gaps first
if col_gap_x > Inches(0.18) and n_cols > 1:
gap_reduction = min(Inches(0.05), excess / max(1, (n_cols - 1) * 2))
col_gap_x = max(Inches(0.18), col_gap_x - gap_reduction)
metrics = compute_layout()
col_widths = metrics["col_widths"]
per_col_box_heights_in = metrics["per_col_box_heights_in"]
col_totals_in = metrics["col_totals_in"]
total_width = metrics["total_width"]
start_left = max(edge_margin, (slide_w - total_width) / 2)
right_limit = slide_w - edge_margin - safety_margin
if start_left + total_width > right_limit:
start_left = max(edge_margin, right_limit - total_width)
arrow_y = content_top + available_height / 2 - arrow_size / 2
current_left = start_left
for col_idx2, (col, heights_in) in enumerate(zip(columns_data, per_col_box_heights_in)):
col_total_h = Inches(col_totals_in[col_idx2]) if col_totals_in else Inches(min_box_height_in)
extra_space = max(Inches(0.0), available_height - col_total_h)
y = content_top + extra_space / 2
is_last_col = (col_idx2 == n_cols - 1)
for text, h_in in zip(col, heights_in):
col_width = col_widths[col_idx2]
add_grid_box(sub, current_left, y, col_width, Inches(h_in), text, color, bold=is_last_col)
y += Inches(h_in) + vertical_gap
if col_idx2 < n_cols - 1:
arrow_left = current_left + col_width + col_gap_x
add_grid_arrow(sub, arrow_left, arrow_y, arrow_size)
current_left += col_width + col_gap_x + arrow_size + col_gap_x
else:
current_left += col_width
# Final post-rendering adjustment: center/scale the grid within margins and shift up if needed
# NOTE: This function doesn't recalculate box heights (shapes already drawn),
# so the main layout logic above must ensure proper fit BEFORE rendering.
# This is a final safety net for centering and minor positional adjustments.
content_bottom_limit = content_top + available_height
adjust_grid_shapes_to_margins(grid_shapes, slide_w, content_top, content_bottom_limit, edge_margin_inches=0.35)
add_footer(sub, slide_w, slide_h)
add_doview_online_link(sub, slide_w, slide_h)
# 6) Add final explanation slide (second to last) — single-page, no truncation
explanation_text = (
"A DoView is a new type of diagram used to clarify the underlying ‘This-Then’ logic behind any issue. "
"For example, in strategy and planning, all planning approaches are based on assumptions such as: "
"if we do THIS, THEN that will happen.\n\n"
"A DoView makes these assumptions explicit, allowing them to be examined, evaluated and used to make better strategic decisions. "
"A DoView works as a shared thinking tool, helping teams align their mental models about objectives. "
"In planning, DoViews assist with prioritizing outcomes, placing indicators next to the boxes they measure, aligning activities with outcomes, "
"measuring performance, evaluating impact, and guiding improvement efforts.\n\n"
"DoViews can also analyze any document that is being used to think strategically about taking action—it surfaces the implicit ‘This-Then’ claims. "
"For example, a DoView of a scientific paper reveals its 'This-Then' claims. Find how to use DoViews at DoViewPlanning.Org/Method. "
"DoViewing a document highlights its implications for action.\n\n"
"To generate a DoView about anything, visit DoViewPlanning.Org for the free AI DoView Drawing Prompt (ChatGPT). "
"DoViews are powerful for summarizing any complex content and accelerating understanding prior to taking any type of action in the world."
)
add_explanation_single_page(prs, overview, "What is a DoView?", explanation_text, font_pt=16, corner_note_text=corner_note_text)
# 7) SOURCES — clickable, paginated, numbered pages (omit [1] if only one page); title positioned below Back button
# NOTE: The following `sources` list is an ILLUSTRATIVE EXAMPLE.
# For a real DoView, REPLACE THIS ENTIRE LIST with the sources actually used to
# build the user’s DoView IF AND ONLY IF the user answered “yes” to Question 8
# (“Do you want to include a list of sources in your PowerPoint?”).
# If the user answered “no” to Question 8, set `sources = []` and the code
# below will not create any Sources slides.
sources = [
# Example only – replace or set to [] depending on the user’s answer to Question 8.
# ("Digital Equity Coalition Aotearoa — Home", "https://www.digitalequity.nz/"),
]
if sources:
add_sources_pages(
prs,
overview,
sources,
"Sources Used For This DoView (AI-generated – check independently)",
corner_note_text=corner_note_text,
)
# 8) SAVE
prs.save("DoView_Strategy_Diagram_Complete.pptx")
print("Created DoView_Strategy_Diagram_Complete.pptx")
────────────────────────────────────────────────────────
INTERNAL REMINDER (AI — DO NOT SAY THIS UNLESS ASKED)
────────────────────────────────────────────────────────
• If (and only if) the user answered “yes” to Question 8, create a Sources slide (or slides) in the PowerPoint listing all sources and their URLs used to build the DoView. Title it exactly:
Sources Used For This DoView (AI-generated – check independently)
• If the user answered “no” to Question 8, set `sources = []` in the Python code and do not create any Sources slide(s).
• After providing the download link, include the following text verbatim beneath it:
To create a PDF from the PowerPoint:
1. Open the .pptx in Microsoft PowerPoint.
2. File → Export.
3. Choose PDF as the format.
To publish the DoView online:
1. Open Google Drive (or sign up).
2. Upload the DoView PowerPoint into a Google Slides file.
3. Click Share and copy the web link to share.
4. (Optional) Use Embed to place it on a web page.
────────────────────────────────────────────────────────
CONCLUDING INSTRUCTIONS TO THE USER
────────────────────────────────────────────────────────
At the end, you must say to the user:
• “If you get a message saying ‘can’t find the file’ or that the ‘file has expired’, you can ask me to create the Python code that would allow you to make the PowerPoint on your machine (however to use this code you need to know how to run Python on your computer using something like Visual Studio Code (this is not hard to set up)).”
Finally, ask the user:
“Do you want the raw contents of the DoView exported in a format that AI could read in the future to recreate the DoView PowerPoint later (this format is called DoView-JSON v1). You can just copy what I produce and save it with your DoView PowerPoint file in case you want to use it in the future to get an AI system to recreate the PowerPoint.”
DoView-JSON v1 (internal format – only show if the user asks for it explicitly):
{
"title": "<DoView title>",
"sections": [
{
"title": "...",
"columns": [[...], [...], ...]
}
]
}
────────────────────────────────────────────────────────
END OF PROMPT
────────────────────────────────────────────────────────