Contour plot export to DXF file

Hello Everyone,
Is there a way to export contour lines created using python in excel to a DXF file?
Unfortunately ezdxf is not available yet.
Cheers,
Adam

Hi Adam

Interesting question! ezdxf isn’t available in the Python in Excel distribution, but you can install it and use it with Anaconda Code.

As for saving .dxf files, the security model of Office.js add-ins means they don’t have access to your local computer, so creating and saving files from the add-in isn’t possible (at the moment).

However, a dxf file is a text file. So you can theoretically create the lines that would normally be saved in such a file, spill them to the grid, then save the sheet as a text file with dxf extension.

I think it will work. I was interested to find out, so I created a working example.

Unfortunately I can’t upload an Excel file to this reply, so we’ll have to do it the screenshot and code snippet way!

First, add ezdxf to your environment:

Replace the code in “Imports and Definitions” with this code:

# Add imports
import io
import math
import tempfile
import os
import numpy as np
import pandas as pd
from matplotlib.collections import LineCollection
import matplotlib.pyplot as plt
import ezdxf
from ezdxf.enums import TextEntityAlignment

# Standard functions provided with Anaconda Code
def to_df(data) -> pd.DataFrame:
    """
    Converts a REF (i.e. list of lists) into a pandas DataFrame
    Assumes that the first row contains column headers

    Usage: df = to_df(REF("F15:H22"))
    """
    if isinstance(data, pd.DataFrame):
        return data
    if hasattr(data, '__len__') and len(data) > 0 and hasattr(data[0], '__len__'):
        return pd.DataFrame(data[1:], columns=data[0])
    raise TypeError('Expected list of lists')

def to_array(data) -> np.ndarray:
    """
    Converts a REF (i.e. list of lists) into a 2-dimensional numpy array

    Usage: arr = to_array(REF("B3:C44"))
    """
    if isinstance(data, np.ndarray):
        return data
    return np.array(data)

def to_list(data) -> list:
    """
    Converts a REF (i.e. list of lists) into a 1-d list.
    Handles wide (1xn) or tall (nx1) data.

    Usage: x = to_list(REF("G1:G55"))
    """
    if not hasattr(data, '__len__'): # data is scalar
        return [data]
    if len(data) == 0 or not hasattr(data[0], '__len__'): # data is already a list
        return list(data)
    if len(data) == 1: # data is wide
        return list(data[0])
    elif len(data[0]) == 1: #data is tall
        return [x[0] for x in data]
    else:
        raise TypeError(f"data must be 1xn or nx1, not {len(data)}x{len(data[0])}")



# Functions for generating .dxf contour plots and associated lines for saving as dxf files
def generate_synthetic_contour_data(grid_size=100, num_peaks=3):
    """Generate synthetic terrain-like contour data with multiple peaks."""
    x = np.linspace(0, 1, grid_size)
    y = np.linspace(0, 1, grid_size)
    xx, yy = np.meshgrid(x, y)
    
    # Create a height map with multiple peaks
    z = np.zeros((grid_size, grid_size))
    for _ in range(num_peaks):
        # Random peak location and height
        peak_x = np.random.random()
        peak_y = np.random.random()
        peak_height = np.random.uniform(0.5, 1.0)
        
        # Add a gaussian peak
        z += peak_height * np.exp(-((xx - peak_x)**2 + (yy - peak_y)**2) / 0.05)
    
    return xx, yy, z

def contours_to_dxf_lines(contours, levels):
    """
    Convert matplotlib contour data to DXF text content as a list of lines.
    
    Args:
        contours: List of contour line coordinates
        levels: Elevation levels corresponding to each contour
        
    Returns:
        List of strings, each representing a line in the DXF file
    """
    # Create a new DXF document in memory
    doc = ezdxf.new('R2010')
    
    # Add a new layer for each contour level
    msp = doc.modelspace()
    
    # Process each contour level
    for i, level in enumerate(levels):
        layer_name = f"CONTOUR_{int(level*100)}"
        doc.layers.add(name=layer_name)
        
        # Process each contour line at this level
        for contour in contours[i]:
            # Extract x and y coordinates
            x_coords = contour[:, 0]
            y_coords = contour[:, 1]
            
            # Create polyline for this contour
            polyline = msp.add_lwpolyline([(x, y) for x, y in zip(x_coords, y_coords)], 
                                         dxfattribs={'layer': layer_name})
            
            # Add elevation label at the midpoint
            mid_idx = len(x_coords) // 2
            text = msp.add_text(
                f"Elevation: {level:.2f}",
                dxfattribs={
                    'layer': layer_name,
                    'height': 0.01,
                    'style': 'Standard'
                }
            )
            # Position the text
            text.set_placement((x_coords[mid_idx], y_coords[mid_idx]), 
                               align=TextEntityAlignment.MIDDLE_CENTER)
    
    # Instead of saving to a file, write to a string buffer
    string_buffer = io.StringIO()
    doc.write(string_buffer)
    
    # Convert the buffer contents to a list of lines
    string_buffer.seek(0)
    dxf_lines = string_buffer.readlines()
    
    return dxf_lines

def contours_to_excel_data(contours, levels):
    """
    Convert contour data to a format suitable for Excel export.
    This creates a list where each item is a line of text from the DXF file.
    Each line will be placed in a single cell in Excel.
    
    Args:
        contours: Contour data from matplotlib
        levels: Elevation levels
        
    Returns:
        List of strings, each representing a line in the DXF file
    """
    # Get the DXF content as lines
    dxf_lines = contours_to_dxf_lines(contours, levels)
    
    # Clean up the lines (remove any trailing newlines)
    cleaned_lines = [line.rstrip('\n') for line in dxf_lines]
    
    return cleaned_lines

def plot_dxf_from_lines(dxf_lines):
    """
    Takes a list of strings representing a DXF file, 
    processes it and plots the contours using matplotlib.
    
    Args:
        dxf_lines: List of strings, each representing a line in the DXF file
    """
    # Create a temporary file to write the DXF content
    with tempfile.NamedTemporaryFile(suffix='.dxf', delete=False) as temp_file:
        temp_path = temp_file.name
        # Write all lines to the temporary file
        for line in dxf_lines:
            temp_file.write((str(line) + '\n').encode('utf-8'))
    
    try:
        # Load the DXF file with ezdxf
        doc = ezdxf.readfile(temp_path)
        msp = doc.modelspace()
        
        # Extract all polylines (contours)
        contour_layers = {}
        elevation_data = {}
        
        for entity in msp:
            if entity.dxftype() == 'LWPOLYLINE':
                layer_name = entity.dxf.layer
                
                # Extract elevation from layer name (format: CONTOUR_XX)
                if layer_name.startswith('CONTOUR_'):
                    try:
                        elevation = float(layer_name.split('_')[1]) / 100  # Convert back to original scale
                    except (IndexError, ValueError):
                        elevation = 0
                else:
                    elevation = 0
                
                # Get vertices
                points = list(entity.vertices())
                
                # Add to the appropriate layer
                if layer_name not in contour_layers:
                    contour_layers[layer_name] = []
                    elevation_data[layer_name] = elevation
                
                contour_layers[layer_name].append(points)
        
        # Create a matplotlib figure
        fig, ax = plt.subplots(figsize=(10, 8))
        
        # Plot each contour layer with a different color based on elevation
        cmap = plt.cm.viridis
        elevations = sorted(elevation_data.values())
        norm = plt.Normalize(min(elevations), max(elevations))
        
        # Plot each contour
        for layer_name, contours in contour_layers.items():
            elevation = elevation_data[layer_name]
            color = cmap(norm(elevation))
            
            # Create a line collection for all contours in this layer
            lines = []
            for contour in contours:
                lines.append(contour)
            
            line_collection = LineCollection(lines, colors=color, linewidths=1.5, label=f"{elevation:.2f}")
            ax.add_collection(line_collection)
        
        # Set axis limits
        all_x = []
        all_y = []
        for contours in contour_layers.values():
            for contour in contours:
                x, y = zip(*contour)
                all_x.extend(x)
                all_y.extend(y)
        
        if all_x and all_y:
            ax.set_xlim(min(all_x), max(all_x))
            ax.set_ylim(min(all_y), max(all_y))
        
        # Add a title and legend
        ax.set_title("Contour Plot from DXF Data")
        
        # Add a colorbar
        sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
        sm.set_array([])
        plt.colorbar(sm, ax=ax, label="Elevation")
        
        # Set axis labels
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        
    finally:
        # Always clean up the temporary file
        os.unlink(temp_path)
    
    # No need to call plt.show() here as your add-in will handle that
    # Just return the figure for display
    return fig

Create a Code cell with this code. It just generates some fake contours and creates a plot. Ensure that the cell is in Linked Mode (see the button in the image below next to the text that says “Run Linked”).

x, y, z = generate_synthetic_contour_data(grid_size=100, num_peaks=3)
fig, ax = plt.subplots(figsize=(10, 8))
levels = np.linspace(0.1, 1.0, 10)
contour_set = ax.contour(x, y, z, levels=levels)
plt.show()

Optionally you can hit the ‘Create reference’ button on the spreadsheet to see a bigger image of the plot. For example:

On a new sheet to the right of the sheet mentioned above, create a new code cell in A1 with this code. Again, ensure the cell is in Linked Mode:

contour_paths = []
for collection in contour_set.collections:
    paths = []
    for path in collection.get_paths():
        paths.append(path.vertices)
    contour_paths.append(paths)

# Convert contours to DXF text content
print("Converting contours to DXF text content...")
dxf_lines = contours_to_excel_data(contour_paths, levels)

That code creates the text lines that would normally be saved in the dxf file (which is just a text file). You can then save your workbook, then select this sheet and use ‘Save As’ to save the sheet as a text file, giving it a file name with the dxf extension.

To verify that the lines produced in the step above are valid dxf file lines, you can create a third sheet and use this code in a Code cell which is again in Linked Mode:

fig = plot_dxf_from_lines(dxf_lines)

That code reads the dxf lines from the second sheet and recreates the plot from scratch. The created plot should be the same as the original plot on the first sheet.

dfx files aren’t my area of expertise, so it would be interesting to find out if you are able to use a dxf file created from the dxf lines sheet in some other program with any degree of success.

Thanks and let me know!

Owen

1 Like

Owen, you’re a legend, mate!

Unfortunately, I can’t try it out right now because my IT department blocked Python functionality for me last night. But I’ll give it a go as soon as it’s back up and running. Can’t wait!