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!