Voxel Price Changes

Tracking Price History in Voxel Theme: A Comprehensive Guide

Keeping track of price changes can be essential for various use cases, whether you’re running a marketplace, showcasing products, or simply maintaining historical price records. In this guide, we will walk you through a practical solution to automatically track price changes on your WordPress site using the Voxel theme and a custom code snippet. By the end, you will be able to store and display price history with timestamps, allowing for a dynamic and insightful way to showcase changes over time.

Use Case: Tracking Price Changes

Imagine you run an online marketplace for real estate, events, or eCommerce products. For each listing (post type), you might want to track price history and changes over time, then display that information to users. This becomes especially important if prices fluctuate frequently, and you want to give users a visual insight into historical pricing.

Required Fields and Keys

To track price changes, we use a repeater field to store a history of price updates, including the price value and timestamp of each change. Below are the necessary fields and their corresponding keys:

Primary Fields

  1. Current Price (Visible to User)
    • Field Type: Number
    • Key: vg_price
    • Purpose: This is where users enter the latest price. (Not required for Products.)
  2. Previous Price (Hidden Field using Visibility rules)
    • Field Type: Number
    • Key: vg_previous_price
    • Purpose: Stores the last recorded price to detect changes. This field is hidden from users.
  3. Price History (Hidden Repeater using Visibility rules)
    • Field Type: Repeater
    • Key: vg_repeater_history
    • Purpose: Stores all past price entries. It contains:
      • Date Field (Key: history_date) – Captures the timestamp when the price was updated. (Timepicker enabled.)
      • Number Field (Key: history_number) – Stores the price value at that specific time.

Unleash Dynamic Data with Voxel!

With Voxel’s design flexibility, the possibilities are endless. Get Voxel here!

Code Snippet for Tracking Price History from a ‘Number field’

Note: Only one of these snippets can be active at a time.

The following code snippet should be added to your Child theme’s functions.php file or use a plugin like Fluent Snippets. It listens for changes to the number field vg_price, and stores a history of price changes along with the timestamp.

*** Important: You must add your post type keys in two places:

// *** EDIT THIS: Allowed post types 1/2 ***
$allowed_post_types = [‘places’, ‘events’, ‘your-other-key’]; // Add or remove post types here

and

// *** EDIT THIS: Allowed post types 2/2 ***
// Hook into Voxel’s frontend submission and update events
$allowed_post_types = [‘places’, ‘events’, ‘your-other-key’]; // Add or remove post types here

*** Disclaimer – Use at your own risk. Always make a backup. ***

function update_vg_post_meta_history_repeater($post_id_or_event) {
    $is_voxel_event = is_object($post_id_or_event) && isset($post_id_or_event->post);

    if ($is_voxel_event) {
        // Handling Voxel event (frontend)
        $post = $post_id_or_event->post;
        $post_id = $post->get_id();
        $post_type = $post->post_type->get_key();
        $current_user_id = get_current_user_id();
    } else {
        // Handling WordPress backend save
        $post_id = intval($post_id_or_event);

        // Prevent running on auto-saves, revisions, or AJAX calls
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if (wp_is_post_revision($post_id)) return;
        if (wp_is_post_autosave($post_id)) return;

        // Verify the nonce (if saving from backend)
        if (!isset($_POST['vg_nonce']) || !wp_verify_nonce($_POST['vg_nonce'], 'vg_save_post_meta')) {
            return;
        }

        // Check if user has permission to edit this post
        if (!current_user_can('edit_post', $post_id)) {
            return;
        }

        // Get post type from WP
        $post_type = get_post_type($post_id);
    }

    // *** EDIT THIS: Allowed post types 1/2 ***
    $allowed_post_types = ['places', 'events', 'your-other-key']; // Add or remove post types here
    if (!in_array($post_type, $allowed_post_types, true)) {
        return;
    }
    // *** End editable ***

    // Get the new and previous price
    $new_price = sanitize_text_field(get_post_meta($post_id, 'vg_price', true));
    $old_price = sanitize_text_field(get_post_meta($post_id, 'vg_previous_price', true));

    // If the price hasn't changed or is empty, do nothing
    if (empty($new_price) || $new_price == $old_price) {
        return;
    }

    // Get existing history and decode JSON
    $repeater_history_data = get_post_meta($post_id, 'vg_repeater_history', true);
    $repeater_history_data = is_string($repeater_history_data) ? json_decode($repeater_history_data, true) : [];

    if (!is_array($repeater_history_data)) {
        $repeater_history_data = [];
    }

    // Add new price entry to history
    $new_entry = [
        'history_number' => $new_price,
        'history_date' => current_time('Y-m-d H:i:s'),
    ];
    $repeater_history_data[] = $new_entry;

    // Use array_slice to keep only the last 30 entries
    $repeater_history_data = array_slice($repeater_history_data, -30);

    // Update the history meta
    update_post_meta($post_id, 'vg_repeater_history', json_encode($repeater_history_data));

    // Store the new price as the previous price
    update_post_meta($post_id, 'vg_previous_price', $new_price);
}


// *** EDIT THIS: Allowed post types 2/2 ***
// Hook into Voxel's frontend submission and update events
$allowed_post_types = ['places', 'events', 'your-other-key']; // Add or remove post types here
foreach ( $allowed_post_types as $type ) {
    add_action("voxel/app-events/post-types/{$type}/post:submitted", 'update_vg_post_meta_history_repeater');
    add_action("voxel/app-events/post-types/{$type}/post:updated", 'update_vg_post_meta_history_repeater');
}
// *** End editable ***


// Hook into WordPress backend post save event
add_action('save_post', 'update_vg_post_meta_history_repeater');

// Add a nonce field in the post edit screen (for backend saves)
function add_vg_nonce_field() {
    wp_nonce_field('vg_save_post_meta', 'vg_nonce');
}
add_action('edit_form_after_title', 'add_vg_nonce_field');

This snippet will save the price change history as a JSON-encoded array, which you can later output and display on the frontend.

Code Snippet for Tracking Price History from a ‘Product’

Note: Only one of these snippets can be active at a time.

The following code snippet should be added to your Child theme’s functions.php file or use a plugin like Fluent Snippets. It listens for changes to the Voxel product field and checks the product meta value for ‘base_price‘ or ‘discount_amount‘, then stores a history of price changes along with the timestamp.

*** Not working with Variable Products

*** Important: You must add your post type keys in two places:

// *** EDIT THIS: Allowed post types 1/2 ***
// Allowed post types — edit this array to include your custom post types.

and

// *** EDIT THIS: Allowed post types 2/2 ***
// Voxel frontend event hooks for each allowed post type

*** Disclaimer – Use at your own risk. Always make a backup. ***

function update_vg_post_meta_history_repeater($post_id_or_event) {
    $is_voxel_event = is_object($post_id_or_event) && isset($post_id_or_event->post);

    if ($is_voxel_event) {
        // Handling Voxel event (frontend)
        $post = $post_id_or_event->post;
        $post_id = $post->get_id();
        $post_type = $post->post_type->get_key();
    } else {
        // Handling WordPress backend save
        $post_id = intval($post_id_or_event);

        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if (wp_is_post_revision($post_id)) return;
        if (wp_is_post_autosave($post_id)) return;

        // Get post type
        $post_type = get_post_type($post_id);
    }

    // *** EDIT THIS: Allowed post types 1/2 ***
    // Allowed post types — edit this array to include your custom post types.
    $allowed_post_types = [
        'places',
        'events',
        'your-other-key', // Add more here as needed
    ];
   // *** End editable ***

    if (!in_array($post_type, $allowed_post_types, true)) {
        return;
    }

    // Try getting updated meta directly from $_POST first (more reliable in backend)
    if (isset($_POST['product'])) {
        $product_data = json_decode(stripslashes($_POST['product']), true);
    } else {
        // Fallback to database value
        $product_meta = get_post_meta($post_id, 'product', true);
        $product_data = is_string($product_meta) ? json_decode($product_meta, true) : null;
    }

    // Ensure we have a valid product array
    if (!is_array($product_data) || !isset($product_data['base_price'])) {
        return;
    }

    // Check for discount amount, otherwise use base price
    $new_price = isset($product_data['base_price']['discount_amount']) 
        ? floatval($product_data['base_price']['discount_amount']) 
        : floatval($product_data['base_price']['amount']);

    // Retrieve previous price
    $old_price = floatval(get_post_meta($post_id, 'vg_previous_price', true));

    // If the price hasn't changed or is empty, do nothing
    if ($new_price <= 0 || $new_price == $old_price) {
        return;
    }

    // Retrieve existing history
    $repeater_history_data = get_post_meta($post_id, 'vg_repeater_history', true);
    $repeater_history_data = is_string($repeater_history_data) ? json_decode($repeater_history_data, true) : [];

    if (!is_array($repeater_history_data)) {
        $repeater_history_data = [];
    }

    // Keep the last 30 entries
    $new_entry = [
        'history_number' => $new_price,
        'history_date' => current_time('Y-m-d H:i:s')
    ];
    $repeater_history_data[] = $new_entry;
    $repeater_history_data = array_slice($repeater_history_data, -30);

    // Update history and previous price
    update_post_meta($post_id, 'vg_repeater_history', json_encode($repeater_history_data));
    update_post_meta($post_id, 'vg_previous_price', $new_price);
}

// *** EDIT THIS: Allowed post types 2/2 ***
// Voxel frontend event hooks for each allowed post type
$allowed_post_types = [
    'places',
    'events',
    'your-other-key', // Add more here as needed
];
// *** End editable ***

foreach ($allowed_post_types as $post_type) {
    add_action("voxel/app-events/post-types/{$post_type}/post:submitted", 'update_vg_post_meta_history_repeater');
    add_action("voxel/app-events/post-types/{$post_type}/post:updated", 'update_vg_post_meta_history_repeater');
}

// WordPress backend hook
add_action('save_post', 'update_vg_post_meta_history_repeater', 20);

This snippet will save the price change history as a JSON-encoded array, which you can later output and display on the frontend.

Displaying Price History on the Frontend

Now that the price history is tracked, it’s time to display it on the frontend. You can use the Elementor HTML widget and Voxel tags to output the price history as a dynamic chart. Below is an example of how to integrate a Plotly.js chart for visualizing the price history.

Change the variable under ‘// *** Define variables here start’ as needed.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://cdn.plot.ly/plotly-2.32.0.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
        }
        #vg-chart-container {
            width: 100%;
            margin: auto;
            padding: 10px;
            border-radius: 10px;
            box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
            background-color: white;
        }
        #vg-chart {
            width: 100%;
            height: 300px;
        }
    </style>
</head>
<body>

    <!-- Responsive container for the chart -->
    <div id="vg-chart-container">
        <div id="vg-chart"></div>
    </div>

    <script>
        window.addEventListener('load', () => {
            // Extract data from Voxel repeater
            const vg_date = '@post(vg_repeater_history.history_date.date).list()';
            const vg_price = [@post(vg_repeater_history.history_number).list()];
            
            // *** Define variables here start
            const vg_axis_format = '%-d %b %Y'; // Default format: 9 Mar 2025
            
            // Axis labels
            const xAxisTitle = 'Date';
            const yAxisTitle = 'Price ($)';

            // Line labels
            const priceHistoryLabel = 'Price History';
            const minPriceLabel = 'Min Price';
            const maxPriceLabel = 'Max Price';

            // Number of data entries to plot (max 30)
            const n = 12;
            // *** Define variables here end
            
            // Convert the comma-separated list to an array of dates
            const dates = vg_date.split(',');
            const prices = vg_price;
            
            // Get the last 'n' dates and prices
            const datesToShow = dates.slice(-n);
            const pricesToShow = prices.slice(-n);
            
            // Find the minimum and maximum prices for the dashed lines
            const minPrice = Math.min(...pricesToShow);
            const maxPrice = Math.max(...pricesToShow);
            
            // Calculate 5% margin for the Y-axis
            const yMargin = (maxPrice - minPrice) * 0.05;
            const yMin = minPrice - yMargin;
            const yMax = maxPrice + yMargin;
            
            // Define the traces for Plotly
            const trace = {
                x: datesToShow,
                y: pricesToShow,
                type: 'scatter',
                mode: 'lines+markers',
                name: priceHistoryLabel,
                line: {
                    shape: 'hv',
                    width: 3
                },
                text: pricesToShow.map(p => `$${p.toFixed(2)}`),
                textposition: 'top right',
                hoverinfo: 'x+y+text'
            };

            // Define min price line
            const minPriceLine = {
                x: datesToShow,
                y: new Array(n).fill(minPrice),
                type: 'scatter',
                mode: 'lines',
                name: minPriceLabel,
                line: {
                    dash: 'dash',
                    color: 'green',
                    width: 2
                }
            };

            // Define max price line
            const maxPriceLine = {
                x: datesToShow,
                y: new Array(n).fill(maxPrice),
                type: 'scatter',
                mode: 'lines',
                name: maxPriceLabel,
                line: {
                    dash: 'dash',
                    color: 'red',
                    width: 2
                }
            };

            // Define the layout for the chart
            const layout = {
                xaxis: {
                    title: xAxisTitle,
                    type: 'date',
                    tickmode: 'array',
                    tickvals: datesToShow,
                    tickformat: vg_axis_format
                },
                yaxis: {
                    title: yAxisTitle,
                    autorange: false,
                    range: [yMin, yMax],
                    tickformat: '$,.2f'
                },
                margin: {
                    t: 50,
                    b: 70,
                    l: 80,
                    r: 50
                },
                dragmode: false,
                displayModeBar: false,
                showlegend: false
            };

            // Plot the chart
            Plotly.newPlot('vg-chart', [trace, minPriceLine, maxPriceLine], layout);
        });

        // Resize plot on window resize for responsiveness
        window.addEventListener('resize', () => {
            Plotly.Plots.resize('vg-chart');
        });
    </script>

</body>
</html>

Conclusion

With this solution, you can easily track price changes, store them securely in the backend, and display a dynamic, interactive price history chart to your site’s visitors. Whether you’re showcasing real estate listings, events, or products, this functionality adds a valuable layer of transparency to your offerings. By using Voxel tags and Plotly, you can create a seamless and informative user experience that highlights price fluctuations in an intuitive and visually appealing manner.

Discover the Voxel theme here!

Price History Chart

More Articles

Displaying parent categories with their child categories grouped underneath in Voxel is difficult and not directly supported by default term feed options. To achieve this, you need to use a term loop workaround that allows child terms to be looped inside a parent term preview. The steps below show how to do this using native […]
Voxel Framework is a new WordPress plugin in development that brings Voxel’s features to Bricks Builder and Gutenberg. While Voxel can create custom post types, taxonomies, and custom fields — and supports powerful dynamic data, conditional logic, and visibility rules — its real value goes far beyond content structure. Details below are speculative based on […]